001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software GmbH & Co. KG, please see the 018 * company website: http://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: http://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.i18n; 029 030import org.opencms.file.CmsObject; 031import org.opencms.file.CmsProject; 032import org.opencms.file.CmsPropertyDefinition; 033import org.opencms.file.CmsResource; 034import org.opencms.file.CmsUser; 035import org.opencms.main.CmsEvent; 036import org.opencms.main.CmsException; 037import org.opencms.main.CmsLog; 038import org.opencms.main.I_CmsEventListener; 039import org.opencms.main.OpenCms; 040import org.opencms.monitor.CmsMemoryMonitor; 041import org.opencms.util.CmsStringUtil; 042import org.opencms.xml.I_CmsXmlDocument; 043 044import java.io.InputStream; 045import java.util.ArrayList; 046import java.util.Collections; 047import java.util.Iterator; 048import java.util.List; 049import java.util.Locale; 050import java.util.TimeZone; 051 052import javax.servlet.http.HttpServletRequest; 053 054import org.apache.commons.io.IOUtils; 055import org.apache.commons.lang3.LocaleUtils; 056import org.apache.commons.lang3.StringUtils; 057import org.apache.commons.logging.Log; 058 059import com.cybozu.labs.langdetect.DetectorFactory; 060 061/** 062 * Manages the locales configured for this OpenCms installation.<p> 063 * 064 * Locale configuration is done in the configuration file <code>opencms-system.xml</code> 065 * in the <code>opencms/system/internationalization</code> node and it's sub-nodes.<p> 066 * 067 * @since 6.0.0 068 */ 069public class CmsLocaleManager implements I_CmsEventListener { 070 071 /** Runtime property name for locale handler. */ 072 public static final String LOCALE_HANDLER = "class_locale_handler"; 073 074 /** Locale to use for storing locale-independent XML contents. */ 075 public static final Locale MASTER_LOCALE = Locale.ENGLISH; 076 077 /** Request parameter to force encoding selection. */ 078 public static final String PARAMETER_ENCODING = "__encoding"; 079 080 /** Request parameter to force locale selection. */ 081 public static final String PARAMETER_LOCALE = "__locale"; 082 083 /** The log object for this class. */ 084 private static final Log LOG = CmsLog.getLog(CmsLocaleManager.class); 085 086 /** The default locale, this is the first configured locale. */ 087 private static Locale m_defaultLocale = Locale.ENGLISH; 088 089 /** 090 * Required for setting the default locale on the first possible time.<p> 091 */ 092 static { 093 setDefaultLocale(); 094 } 095 096 /** The set of available locale names. */ 097 private List<Locale> m_availableLocales; 098 099 /** The default locale names (must be a subset of the available locale names). */ 100 private List<Locale> m_defaultLocales; 101 102 /** Indicates if the locale manager is fully initialized. */ 103 private boolean m_initialized; 104 105 /** The configured locale handler. */ 106 private I_CmsLocaleHandler m_localeHandler; 107 108 /** The string value of the 'reuse-elements' option. */ 109 private String m_reuseElementsStr; 110 111 /** The OpenCms default time zone. */ 112 private TimeZone m_timeZone; 113 114 /** 115 * Initializes a new CmsLocaleManager, called from the configuration.<p> 116 */ 117 public CmsLocaleManager() { 118 119 setDefaultLocale(); 120 setTimeZone("GMT"); 121 m_availableLocales = new ArrayList<Locale>(); 122 m_defaultLocales = new ArrayList<Locale>(); 123 m_localeHandler = new CmsDefaultLocaleHandler(); 124 if (CmsLog.INIT.isInfoEnabled()) { 125 CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_I18N_CONFIG_START_0)); 126 } 127 // register this object as event listener 128 OpenCms.addCmsEventListener(this, new int[] {I_CmsEventListener.EVENT_CLEAR_CACHES}); 129 } 130 131 /** 132 * Initializes a new CmsLocaleManager, used for OpenCms runlevel 1 (unit tests) only.<p> 133 * 134 * @param defaultLocale the default locale to use 135 */ 136 public CmsLocaleManager(Locale defaultLocale) { 137 138 setDefaultLocale(); 139 setTimeZone("GMT"); 140 m_initialized = false; 141 142 m_availableLocales = new ArrayList<Locale>(); 143 m_defaultLocales = new ArrayList<Locale>(); 144 m_localeHandler = new CmsDefaultLocaleHandler(); 145 146 m_defaultLocale = defaultLocale; 147 m_defaultLocales.add(defaultLocale); 148 m_availableLocales.add(defaultLocale); 149 } 150 151 /** 152 * Returns the default locale configured in <code>opencms-system.xml</code>, 153 * that is the first locale from the list provided 154 * in the <code>opencms/system/internationalization/localesdefault</code> node.<p> 155 * 156 * @return the default locale configured in <code>opencms-system.xml</code> 157 */ 158 public static Locale getDefaultLocale() { 159 160 return m_defaultLocale; 161 } 162 163 /** 164 * Returns a locale created from the given full name.<p> 165 * 166 * The full name must consist of language code, 167 * country code(optional), variant(optional) separated by "_".<p> 168 * 169 * This method will always return a valid Locale! 170 * If the provided locale name is not valid (i.e. leads to an Exception 171 * when trying to create the Locale, then the configured default Locale is returned.<p> 172 * 173 * @param localeName the full locale name 174 * @return the locale or <code>null</code> if not available 175 */ 176 public static Locale getLocale(String localeName) { 177 178 if (CmsStringUtil.isEmpty(localeName)) { 179 return getDefaultLocale(); 180 } 181 182 Locale locale = null; 183 if (OpenCms.getMemoryMonitor() != null) { 184 // this may be used AFTER shutdown 185 locale = OpenCms.getMemoryMonitor().getCachedLocale(localeName); 186 } 187 if (locale != null) { 188 return locale; 189 } 190 try { 191 if ("all".equals(localeName)) { 192 locale = new Locale("all"); 193 } else { 194 locale = LocaleUtils.toLocale(localeName); 195 } 196 } catch (Throwable t) { 197 LOG.debug(Messages.get().getBundle().key(Messages.LOG_CREATE_LOCALE_FAILED_1, localeName), t); 198 // map this error to the default locale 199 locale = getDefaultLocale(); 200 } 201 if (OpenCms.getMemoryMonitor() != null) { 202 // this may be used AFTER shutdown 203 OpenCms.getMemoryMonitor().cacheLocale(localeName, locale); 204 } 205 return locale; 206 } 207 208 /** 209 * Returns the locale names from the given List of locales as a comma separated String.<p> 210 * 211 * For example, if the input List contains <code>{@link Locale#ENGLISH}</code> and 212 * <code>{@link Locale#GERMANY}</code>, the result will be <code>"en, de_DE"</code>.<p> 213 * 214 * An empty String is returned if the input is <code>null</code>, or contains no elements.<p> 215 * 216 * @param locales the locales to generate a String from 217 * 218 * @return the locale names from the given List of locales as a comma separated String 219 */ 220 public static String getLocaleNames(List<Locale> locales) { 221 222 StringBuffer result = new StringBuffer(); 223 if (locales != null) { 224 Iterator<Locale> i = locales.iterator(); 225 while (i.hasNext()) { 226 result.append(i.next().toString()); 227 if (i.hasNext()) { 228 result.append(", "); 229 } 230 } 231 } 232 return result.toString(); 233 } 234 235 /** 236 * Returns a List of locales from an array of locale names.<p> 237 * 238 * @param localeNames array of locale names 239 * @return a List of locales derived from the given locale names 240 */ 241 public static List<Locale> getLocales(List<String> localeNames) { 242 243 List<Locale> result = new ArrayList<Locale>(localeNames.size()); 244 for (int i = 0; i < localeNames.size(); i++) { 245 result.add(getLocale(localeNames.get(i).toString().trim())); 246 } 247 return result; 248 } 249 250 /** 251 * Returns a List of locales from a comma-separated string of locale names.<p> 252 * 253 * @param localeNames a comma-separated string of locale names 254 * @return a List of locales derived from the given locale names 255 */ 256 public static List<Locale> getLocales(String localeNames) { 257 258 if (localeNames == null) { 259 return null; 260 } 261 return getLocales(CmsStringUtil.splitAsList(localeNames, ',')); 262 } 263 264 /** 265 * <p> 266 * Extends a base name with locale suffixes and yields the list of extended names 267 * in the order they typically should be used according to the given locale. 268 * </p> 269 * <p> 270 * <strong>Example</strong>: If you have base name <code>base</code> and the locale with {@link String} representation <code>de_DE</code>, 271 * the result will be (assuming <code>en</code> is the default locale): 272 * <ul> 273 * <li> for <code>wantBase == false</code> and <code>defaultAsBase == false</code>: <code> [base_de_DE, base_de]</li> 274 * <li> for <code>wantBase == true</code> and <code>defaultAsBase == false</code>: <code> [base_de_DE, base_de, base]</li> 275 * <li> for <code>wantBase == false</code> and <code>defaultAsBase == true</code>: <code> [base_de_DE, base_de, base_en]</li> 276 * <li> for <code>wantBase == true</code> and <code>defaultAsBase == true</code>: <code> [base_de_DE, base_de, base, base_en]</li> 277 * </ul> 278 * If the requested locale is a variant of the default locale, 279 * the list will never contain the default locale as last element because it appears already earlier. 280 * 281 * @param basename the base name that should be extended by locale post-fixes 282 * @param locale the locale for which the list of extensions should be generated. 283 * @param wantBase flag, indicating if the base name without locale post-fix should be yielded as well. 284 * @param defaultAsBase flag, indicating, if the variant with the default locale should be used as base. 285 * @return the list of locale variants of the base name in the order they should be used. 286 */ 287 public static List<String> getLocaleVariants( 288 String basename, 289 Locale locale, 290 boolean wantBase, 291 boolean defaultAsBase) { 292 293 List<String> result = new ArrayList<String>(); 294 if (null == basename) { 295 return result; 296 } else { 297 String localeString = null == locale ? "" : "_" + locale.toString(); 298 boolean wantDefaultAsBase = defaultAsBase 299 && !(localeString.startsWith("_" + getDefaultLocale().toString())); 300 while (!localeString.isEmpty()) { 301 result.add(basename + localeString); 302 localeString = localeString.substring(0, localeString.lastIndexOf('_')); 303 } 304 if (wantBase) { 305 result.add(basename); 306 } 307 if (wantDefaultAsBase) { 308 result.add(basename + "_" + getDefaultLocale().toString()); 309 } 310 return result; 311 } 312 } 313 314 /** 315 * Utility method to get the primary locale for a given resource.<p> 316 * 317 * @param cms the current CMS context 318 * @param res the resource for which the locale should be retrieved 319 * 320 * @return the primary locale 321 */ 322 public static Locale getMainLocale(CmsObject cms, CmsResource res) { 323 324 CmsLocaleManager localeManager = OpenCms.getLocaleManager(); 325 List<Locale> defaultLocales = null; 326 // must switch project id in stored Admin context to match current project 327 String defaultNames = null; 328 try { 329 defaultNames = cms.readPropertyObject(res, CmsPropertyDefinition.PROPERTY_LOCALE, true).getValue(); 330 } catch (CmsException e) { 331 LOG.warn(e.getLocalizedMessage(), e); 332 } 333 if (defaultNames != null) { 334 defaultLocales = localeManager.getAvailableLocales(defaultNames); 335 } 336 337 if ((defaultLocales == null) || (defaultLocales.isEmpty())) { 338 // no default locales could be determined 339 defaultLocales = localeManager.getDefaultLocales(); 340 } 341 Locale locale; 342 // return the first default locale name 343 if ((defaultLocales != null) && (defaultLocales.size() > 0)) { 344 locale = defaultLocales.get(0); 345 } else { 346 locale = CmsLocaleManager.getDefaultLocale(); 347 } 348 return locale; 349 } 350 351 /** 352 * Returns the content encoding set for the given resource.<p> 353 * 354 * The content encoding is controlled by the property {@link CmsPropertyDefinition#PROPERTY_CONTENT_ENCODING}, 355 * which can be set on the resource or on a parent folder for all resources in this folder.<p> 356 * 357 * In case no encoding has been set, the default encoding from 358 * {@link org.opencms.main.CmsSystemInfo#getDefaultEncoding()} is returned.<p> 359 * 360 * @param cms the current OpenCms user context 361 * @param res the resource to read the encoding for 362 * 363 * @return the content encoding set for the given resource 364 */ 365 public static final String getResourceEncoding(CmsObject cms, CmsResource res) { 366 367 String encoding = null; 368 // get the encoding 369 try { 370 encoding = cms.readPropertyObject(res, CmsPropertyDefinition.PROPERTY_CONTENT_ENCODING, true).getValue(); 371 if (encoding != null) { 372 encoding = CmsEncoder.lookupEncoding(encoding.trim(), encoding); 373 } 374 } catch (CmsException e) { 375 if (LOG.isInfoEnabled()) { 376 LOG.info(Messages.get().getBundle().key(Messages.ERR_READ_ENCODING_PROP_1, res.getRootPath()), e); 377 } 378 } 379 if (encoding == null) { 380 encoding = OpenCms.getSystemInfo().getDefaultEncoding(); 381 } 382 return encoding; 383 } 384 385 /** 386 * Sets the default locale of the Java VM to <code>{@link Locale#ENGLISH}</code> if the 387 * current default has any other language then English set.<p> 388 * 389 * This is required because otherwise the default (English) resource bundles 390 * would not be displayed for the English locale if a translated default locale exists.<p> 391 * 392 * Here's an example of how this issues shows up: 393 * On a German server, the default locale usually is <code>{@link Locale#GERMAN}</code>. 394 * All English translations for OpenCms are located in the "default" message files, for example 395 * <code>org.opencms.i18n.message.properties</code>. If the German localization is installed, it will be 396 * located in <code>org.opencms.i18n.message_de.properties</code>. If user has English selected 397 * as his locale, the default Java lookup mechanism first tries to find 398 * <code>org.opencms.i18n.message_en.properties</code>. However, this file does not exist, since the 399 * English localization is kept in the default file. Next, the Java lookup mechanism tries to find the servers 400 * default locale, which in this example is German. Since there is a German message file, the Java lookup mechanism 401 * is finished and uses this German localization, not the default file. Therefore the 402 * user get the German localization, not the English one. 403 * Setting the default locale explicitly to English avoids this issue.<p> 404 */ 405 private static void setDefaultLocale() { 406 407 // set the default locale to english 408 // this is required because otherwise the default (english) resource bundles 409 // would not be displayed for the english locale if a translated locale exists 410 411 Locale oldLocale = Locale.getDefault(); 412 if (!(Locale.ENGLISH.getLanguage().equals(oldLocale.getLanguage()))) { 413 // default language is not English 414 try { 415 Locale.setDefault(Locale.ENGLISH); 416 if (CmsLog.INIT.isInfoEnabled()) { 417 CmsLog.INIT.info( 418 Messages.get().getBundle().key(Messages.INIT_I18N_DEFAULT_LOCALE_2, Locale.ENGLISH, oldLocale)); 419 } 420 } catch (Exception e) { 421 // any Exception: the locale has not been changed, so there may be issues with the English 422 // localization but OpenCms will run in general 423 CmsLog.INIT.error( 424 Messages.get().getBundle().key( 425 Messages.LOG_UNABLE_TO_SET_DEFAULT_LOCALE_2, 426 Locale.ENGLISH, 427 oldLocale), 428 e); 429 } 430 } else { 431 if (CmsLog.INIT.isInfoEnabled()) { 432 CmsLog.INIT.info( 433 Messages.get().getBundle().key(Messages.INIT_I18N_KEEPING_DEFAULT_LOCALE_1, oldLocale)); 434 } 435 } 436 437 // initialize the static member with the new default 438 m_defaultLocale = Locale.getDefault(); 439 } 440 441 /** 442 * Adds a locale to the list of available locales.<p> 443 * 444 * @param localeName the locale to add 445 */ 446 public void addAvailableLocale(String localeName) { 447 448 Locale locale = getLocale(localeName); 449 // add full variation (language / country / variant) 450 if (!m_availableLocales.contains(locale)) { 451 m_availableLocales.add(locale); 452 if (CmsLog.INIT.isInfoEnabled()) { 453 CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_I18N_CONFIG_ADD_LOCALE_1, locale)); 454 } 455 } 456 // add variation with only language and country 457 locale = new Locale(locale.getLanguage(), locale.getCountry()); 458 if (!m_availableLocales.contains(locale)) { 459 m_availableLocales.add(locale); 460 if (CmsLog.INIT.isInfoEnabled()) { 461 CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_I18N_CONFIG_ADD_LOCALE_1, locale)); 462 } 463 } 464 // add variation with language only 465 locale = new Locale(locale.getLanguage()); 466 if (!m_availableLocales.contains(locale)) { 467 m_availableLocales.add(locale); 468 if (CmsLog.INIT.isInfoEnabled()) { 469 CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_I18N_CONFIG_ADD_LOCALE_1, locale)); 470 } 471 } 472 } 473 474 /** 475 * Adds a locale to the list of default locales.<p> 476 * 477 * @param localeName the locale to add 478 */ 479 public void addDefaultLocale(String localeName) { 480 481 Locale locale = getLocale(localeName); 482 if (!m_defaultLocales.contains(locale)) { 483 m_defaultLocales.add(locale); 484 if (CmsLog.INIT.isInfoEnabled()) { 485 CmsLog.INIT.info( 486 Messages.get().getBundle().key( 487 Messages.INIT_I18N_CONFIG_DEFAULT_LOCALE_2, 488 new Integer(m_defaultLocales.size()), 489 locale)); 490 491 } 492 } 493 } 494 495 /** 496 * Implements the CmsEvent interface, 497 * the locale manager the events to clear 498 * the list of cached keys .<p> 499 * 500 * @param event CmsEvent that has occurred 501 */ 502 public void cmsEvent(CmsEvent event) { 503 504 switch (event.getType()) { 505 case I_CmsEventListener.EVENT_CLEAR_CACHES: 506 clearCaches(); 507 break; 508 default: // no operation 509 } 510 } 511 512 /** 513 * Returns the list of available {@link Locale}s configured in <code>opencms-system.xml</code>, 514 * in the <code>opencms/system/internationalization/localesconfigured</code> node.<p> 515 * 516 * The list of configured available locales contains all locales that are allowed to be used in the VFS, 517 * for example as languages in XML content files.<p> 518 * 519 * The available locales are a superset of the default locales, see {@link #getDefaultLocales()}.<p> 520 * 521 * It's possible to reduce the system default by setting the propery 522 * <code>{@link CmsPropertyDefinition#PROPERTY_AVAILABLE_LOCALES}</code> 523 * to a comma separated list of locale names. However, you can not add new available locales, 524 * only remove from the configured list.<p> 525 * 526 * @return the list of available locale names, e.g. <code>en, de</code> 527 * 528 * @see #getDefaultLocales() 529 */ 530 public List<Locale> getAvailableLocales() { 531 532 return Collections.unmodifiableList(m_availableLocales); 533 } 534 535 /** 536 * Returns an array of available locale names for the given resource.<p> 537 * 538 * @param cms the current cms permission object 539 * @param resource the resource 540 * 541 * @return an array of available locale names 542 * 543 * @see #getAvailableLocales() 544 */ 545 public List<Locale> getAvailableLocales(CmsObject cms, CmsResource resource) { 546 547 String availableNames = null; 548 try { 549 availableNames = cms.readPropertyObject( 550 resource, 551 CmsPropertyDefinition.PROPERTY_AVAILABLE_LOCALES, 552 true).getValue(); 553 } catch (CmsException exc) { 554 LOG.debug("Could not read available locales property for resource " + resource.getRootPath(), exc); 555 } 556 557 List<Locale> result = null; 558 if (availableNames != null) { 559 result = getAvailableLocales(availableNames); 560 } 561 if ((result == null) || (result.size() == 0)) { 562 return Collections.unmodifiableList(m_availableLocales); 563 } else { 564 return result; 565 } 566 } 567 568 /** 569 * Returns an array of available locale names for the given resource.<p> 570 * 571 * @param cms the current cms permission object 572 * @param resourceName the name of the resource 573 * 574 * @return an array of available locale names 575 * 576 * @see #getAvailableLocales() 577 */ 578 public List<Locale> getAvailableLocales(CmsObject cms, String resourceName) { 579 580 String availableNames = null; 581 try { 582 availableNames = cms.readPropertyObject( 583 resourceName, 584 CmsPropertyDefinition.PROPERTY_AVAILABLE_LOCALES, 585 true).getValue(); 586 } catch (CmsException exc) { 587 LOG.debug("Could not read available locales property for resource " + resourceName, exc); 588 } 589 590 List<Locale> result = null; 591 if (availableNames != null) { 592 result = getAvailableLocales(availableNames); 593 } 594 if ((result == null) || (result.size() == 0)) { 595 return Collections.unmodifiableList(m_availableLocales); 596 } else { 597 return result; 598 } 599 } 600 601 /** 602 * Returns a List of available locales from a comma separated string of locale names.<p> 603 * 604 * All names are filtered against the allowed available locales 605 * configured in <code>opencms-system.xml</code>.<P> 606 * 607 * @param names a comma-separated String of locale names 608 * @return List of locales created from the given locale names 609 * 610 * @see #getAvailableLocales() 611 */ 612 public List<Locale> getAvailableLocales(String names) { 613 614 return checkLocaleNames(getLocales(names)); 615 } 616 617 /** 618 * Returns the best available locale present in the given XML content, or the default locale.<p> 619 * 620 * @param cms the current OpenCms user context 621 * @param resource the resource 622 * @param content the XML content 623 * 624 * @return the locale 625 */ 626 public Locale getBestAvailableLocaleForXmlContent(CmsObject cms, CmsResource resource, I_CmsXmlDocument content) { 627 628 Locale locale = getDefaultLocale(cms, resource); 629 if (!content.hasLocale(locale)) { 630 // if the requested locale is not available, get the first matching default locale, 631 // or the first matching available locale 632 boolean foundLocale = false; 633 if (content.getLocales().size() > 0) { 634 List<Locale> locales = getDefaultLocales(cms, resource); 635 for (Locale defaultLocale : locales) { 636 if (content.hasLocale(defaultLocale)) { 637 locale = defaultLocale; 638 foundLocale = true; 639 break; 640 } 641 } 642 if (!foundLocale) { 643 locales = getAvailableLocales(cms, resource); 644 for (Locale availableLocale : locales) { 645 if (content.hasLocale(availableLocale)) { 646 locale = availableLocale; 647 foundLocale = true; 648 break; 649 } 650 } 651 } 652 } 653 } 654 return locale; 655 } 656 657 /** 658 * Tries to find the given requested locale (eventually simplified) in the collection of available locales, 659 * if the requested locale is not found it will return the first match from the given list of default locales.<p> 660 * 661 * @param requestedLocale the requested locale, if this (or a simplified version of it) is available it will be returned 662 * @param defaults a list of default locales to use in case the requested locale is not available 663 * @param available the available locales to find a match in 664 * 665 * @return the best matching locale name or null if no name matches 666 */ 667 public Locale getBestMatchingLocale(Locale requestedLocale, List<Locale> defaults, List<Locale> available) { 668 669 if ((available == null) || available.isEmpty()) { 670 // no locales are available at all 671 return null; 672 } 673 674 // the requested locale is the match we want to find most 675 if (available.contains(requestedLocale)) { 676 // check if the requested locale is directly available 677 return requestedLocale; 678 } 679 if (requestedLocale.getVariant().length() > 0) { 680 // locale has a variant like "en_EN_whatever", try only with language and country 681 Locale check = new Locale(requestedLocale.getLanguage(), requestedLocale.getCountry(), ""); 682 if (available.contains(check)) { 683 return check; 684 } 685 } 686 if (requestedLocale.getCountry().length() > 0) { 687 // locale has a country like "en_EN", try only with language 688 Locale check = new Locale(requestedLocale.getLanguage(), "", ""); 689 if (available.contains(check)) { 690 return check; 691 } 692 } 693 694 // available locales do not match the requested locale 695 if ((defaults == null) || defaults.isEmpty()) { 696 // if we have no default locales we are out of luck 697 return null; 698 } 699 700 // no match found for the requested locale, return the first match from the default locales 701 return getFirstMatchingLocale(defaults, available); 702 } 703 704 /** 705 * Returns the "the" default locale for the given resource.<p> 706 * 707 * It's possible to override the system default (see {@link #getDefaultLocale()}) by setting the property 708 * <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> to a comma separated list of locale names. 709 * This property is inherited from the parent folders. 710 * This method will return the first locale from that list.<p> 711 * 712 * The default locale must be contained in the set of configured available locales, 713 * see {@link #getAvailableLocales()}. 714 * In case an invalid locale has been set with the property, this locale is ignored and the 715 * same result as {@link #getDefaultLocale()} is returned.<p> 716 * 717 * In case the property <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> has not been set 718 * on the resource or a parent folder, 719 * this method returns the same result as {@link #getDefaultLocale()}.<p> 720 * 721 * @param cms the current cms permission object 722 * @param resource the resource 723 * @return an array of default locale names 724 * 725 * @see #getDefaultLocales() 726 * @see #getDefaultLocales(CmsObject, String) 727 */ 728 public Locale getDefaultLocale(CmsObject cms, CmsResource resource) { 729 730 List<Locale> defaultLocales = getDefaultLocales(cms, resource); 731 Locale result; 732 if (defaultLocales.size() > 0) { 733 result = defaultLocales.get(0); 734 } else { 735 result = getDefaultLocale(); 736 } 737 return result; 738 } 739 740 /** 741 * Returns the "the" default locale for the given resource.<p> 742 * 743 * It's possible to override the system default (see {@link #getDefaultLocale()}) by setting the property 744 * <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> to a comma separated list of locale names. 745 * This property is inherited from the parent folders. 746 * This method will return the first locale from that list.<p> 747 * 748 * The default locale must be contained in the set of configured available locales, 749 * see {@link #getAvailableLocales()}. 750 * In case an invalid locale has been set with the property, this locale is ignored and the 751 * same result as {@link #getDefaultLocale()} is returned.<p> 752 * 753 * In case the property <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> has not been set 754 * on the resource or a parent folder, 755 * this method returns the same result as {@link #getDefaultLocale()}.<p> 756 * 757 * @param cms the current cms permission object 758 * @param resourceName the name of the resource 759 * @return an array of default locale names 760 * 761 * @see #getDefaultLocales() 762 * @see #getDefaultLocales(CmsObject, String) 763 */ 764 public Locale getDefaultLocale(CmsObject cms, String resourceName) { 765 766 List<Locale> defaultLocales = getDefaultLocales(cms, resourceName); 767 Locale result; 768 if (defaultLocales.size() > 0) { 769 result = defaultLocales.get(0); 770 } else { 771 result = getDefaultLocale(); 772 } 773 return result; 774 } 775 776 /** 777 * Returns the list of default {@link Locale}s configured in <code>opencms-system.xml</code>, 778 * in the <code>opencms/system/internationalization/localesdefault</code> node.<p> 779 * 780 * Since the default locale is always available, the result list will always contain at least one Locale.<p> 781 * 782 * It's possible to override the system default by setting the property 783 * <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> to a comma separated list of locale names. 784 * This property is inherited from the parent folders.<p> 785 * 786 * The default locales must be a subset of the configured available locales, see {@link #getAvailableLocales()}. 787 * In case an invalid locale has been set with the property, this locale is ignored.<p> 788 * 789 * The default locale names are used as a fallback mechanism in case a locale is requested 790 * that can not be found, for example when delivering content form an XML content.<p> 791 * 792 * There is a list of default locales (instead of just one default locale) since there 793 * are scenarios when one default is not enough. Consider the following example:<i> 794 * The main default locale is set to "en". An example XML content file contains just one language, 795 * in this case "de" and not "en". Now a request is made to the file for the locale "fr". If 796 * there would be only one default locale ("en"), we would have to give up. But since we allow more then 797 * one default, we can deliver the "de" content instead of a blank page.</I><p> 798 * 799 * @return the list of default locale names, e.g. <code>en, de</code> 800 * 801 * @see #getAvailableLocales() 802 */ 803 public List<Locale> getDefaultLocales() { 804 805 return m_defaultLocales; 806 } 807 808 /** 809 * Returns an array of default locales for the given resource.<p> 810 * 811 * Since the default locale is always available, the result list will always contain at least one Locale.<p> 812 * 813 * It's possible to override the system default (see {@link #getDefaultLocales()}) by setting the property 814 * <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> to a comma separated list of locale names. 815 * This property is inherited from the parent folders.<p> 816 * 817 * The default locales must be a subset of the configured available locales, see {@link #getAvailableLocales()}. 818 * In case an invalid locale has been set with the property, this locale is ignored.<p> 819 * 820 * In case the property <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> has not been set 821 * on the resource or a parent folder, 822 * this method returns the same result as {@link #getDefaultLocales()}.<p> 823 * 824 * Use this method in case you need to get all configured default options for a resource, 825 * if you just need the "the" default locale for a resource, 826 * use <code>{@link #getDefaultLocale(CmsObject, String)}</code>.<p> 827 * 828 * @param cms the current cms permission object 829 * @param resource the resource to read the default locale properties for 830 * @return an array of default locale names 831 * 832 * @see #getDefaultLocales() 833 * @see #getDefaultLocale(CmsObject, String) 834 * @see #getDefaultLocales(CmsObject, String) 835 * 836 * @since 7.0.2 837 */ 838 public List<Locale> getDefaultLocales(CmsObject cms, CmsResource resource) { 839 840 String defaultNames = null; 841 try { 842 defaultNames = cms.readPropertyObject(resource, CmsPropertyDefinition.PROPERTY_LOCALE, true).getValue(); 843 } catch (CmsException e) { 844 LOG.warn(Messages.get().getBundle().key(Messages.ERR_READ_ENCODING_PROP_1, cms.getSitePath(resource)), e); 845 } 846 return getDefaultLocales(defaultNames); 847 } 848 849 /** 850 * Returns an array of default locales for the given resource.<p> 851 * 852 * Since the default locale is always available, the result list will always contain at least one Locale.<p> 853 * 854 * It's possible to override the system default (see {@link #getDefaultLocales()}) by setting the property 855 * <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> to a comma separated list of locale names. 856 * This property is inherited from the parent folders.<p> 857 * 858 * The default locales must be a subset of the configured available locales, see {@link #getAvailableLocales()}. 859 * In case an invalid locale has been set with the property, this locale is ignored.<p> 860 * 861 * In case the property <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> has not been set 862 * on the resource or a parent folder, 863 * this method returns the same result as {@link #getDefaultLocales()}.<p> 864 * 865 * Use this method in case you need to get all configured default options for a resource, 866 * if you just need the "the" default locale for a resource, 867 * use <code>{@link #getDefaultLocale(CmsObject, String)}</code>.<p> 868 * 869 * @param cms the current cms permission object 870 * @param resourceName the name of the resource 871 * @return an array of default locale names 872 * 873 * @see #getDefaultLocales() 874 * @see #getDefaultLocale(CmsObject, String) 875 * @see #getDefaultLocales(CmsObject, CmsResource) 876 */ 877 public List<Locale> getDefaultLocales(CmsObject cms, String resourceName) { 878 879 String defaultNames = null; 880 try { 881 defaultNames = cms.readPropertyObject(resourceName, CmsPropertyDefinition.PROPERTY_LOCALE, true).getValue(); 882 } catch (CmsException e) { 883 LOG.warn(Messages.get().getBundle().key(Messages.ERR_READ_ENCODING_PROP_1, resourceName), e); 884 } 885 return getDefaultLocales(defaultNames); 886 } 887 888 /** 889 * Returns the first matching locale (eventually simplified) from the available locales.<p> 890 * 891 * In case no match is found, code <code>null</code> is returned.<p> 892 * 893 * @param locales must be an ascending sorted list of locales in order of preference 894 * @param available the available locales to find a match in 895 * 896 * @return the first precise or simplified match, or <code>null</code> in case no match is found 897 */ 898 public Locale getFirstMatchingLocale(List<Locale> locales, List<Locale> available) { 899 900 Iterator<Locale> i; 901 // first try a precise match 902 i = locales.iterator(); 903 while (i.hasNext()) { 904 Locale locale = i.next(); 905 if (available.contains(locale)) { 906 // precise match 907 return locale; 908 } 909 } 910 911 // now try a match only with language and country 912 i = locales.iterator(); 913 while (i.hasNext()) { 914 Locale locale = i.next(); 915 if (locale.getVariant().length() > 0) { 916 // the locale has a variant, try to match without the variant 917 locale = new Locale(locale.getLanguage(), locale.getCountry(), ""); 918 if (available.contains(locale)) { 919 // match 920 return locale; 921 } 922 } 923 } 924 925 // finally try a match only with language 926 i = locales.iterator(); 927 while (i.hasNext()) { 928 Locale locale = i.next(); 929 if (locale.getCountry().length() > 0) { 930 // the locale has a country, try to match without the country 931 locale = new Locale(locale.getLanguage(), "", ""); 932 if (available.contains(locale)) { 933 // match 934 return locale; 935 } 936 } 937 } 938 939 // no match 940 return null; 941 } 942 943 /** 944 * Returns the the appropriate locale/encoding for a request, 945 * using the "right" locale handler for the given resource.<p> 946 * 947 * Certain system folders (like the Workplace) require a special 948 * locale handler different from the configured handler. 949 * Use this method if you want to resolve locales exactly like 950 * the system does for a request.<p> 951 * 952 * @param req the current http request 953 * @param user the current user 954 * @param project the current project 955 * @param resource the URI of the requested resource (with full site root added) 956 * 957 * @return the i18n information to use for the given request context 958 */ 959 public CmsI18nInfo getI18nInfo(HttpServletRequest req, CmsUser user, CmsProject project, String resource) { 960 961 CmsI18nInfo i18nInfo = null; 962 963 // check if this is a request against a Workplace folder 964 if (OpenCms.getSiteManager().isWorkplaceRequest(req)) { 965 // The list of configured localized workplace folders 966 List<String> wpLocalizedFolders = OpenCms.getWorkplaceManager().getLocalizedFolders(); 967 for (int i = wpLocalizedFolders.size() - 1; i >= 0; i--) { 968 if (resource.startsWith(wpLocalizedFolders.get(i))) { 969 // use the workplace locale handler for this resource 970 i18nInfo = OpenCms.getWorkplaceManager().getI18nInfo(req, user, project, resource); 971 break; 972 } 973 } 974 } 975 if (i18nInfo == null) { 976 // use default locale handler 977 i18nInfo = m_localeHandler.getI18nInfo(req, user, project, resource); 978 } 979 980 // check the request for special parameters overriding the locale handler 981 Locale locale = null; 982 String encoding = null; 983 if (req != null) { 984 String localeParam = req.getParameter(CmsLocaleManager.PARAMETER_LOCALE); 985 // check request for parameters 986 if (localeParam != null) { 987 // "__locale" parameter found in request 988 locale = CmsLocaleManager.getLocale(localeParam); 989 } 990 // check for "__encoding" parameter in request 991 encoding = req.getParameter(CmsLocaleManager.PARAMETER_ENCODING); 992 } 993 994 // merge values from request with values from locale handler 995 if (locale == null) { 996 locale = i18nInfo.getLocale(); 997 } 998 if (encoding == null) { 999 encoding = i18nInfo.getEncoding(); 1000 } 1001 1002 // still some values might be "null" 1003 if (locale == null) { 1004 locale = getDefaultLocale(); 1005 if (LOG.isDebugEnabled()) { 1006 LOG.debug(Messages.get().getBundle().key(Messages.LOG_LOCALE_NOT_FOUND_1, locale)); 1007 } 1008 } 1009 if (encoding == null) { 1010 encoding = OpenCms.getSystemInfo().getDefaultEncoding(); 1011 if (LOG.isDebugEnabled()) { 1012 LOG.debug(Messages.get().getBundle().key(Messages.LOG_ENCODING_NOT_FOUND_1, encoding)); 1013 } 1014 } 1015 1016 // return the merged values 1017 return new CmsI18nInfo(locale, encoding); 1018 } 1019 1020 /** 1021 * Returns the configured locale handler.<p> 1022 * 1023 * This handler is used to derive the appropriate locale/encoding for a request.<p> 1024 * 1025 * @return the locale handler 1026 */ 1027 public I_CmsLocaleHandler getLocaleHandler() { 1028 1029 return m_localeHandler; 1030 } 1031 1032 /** 1033 * Gets the string value of the 'reuse-elements' option.<p> 1034 * 1035 * @return the string value of the 'reuse-elements' option 1036 */ 1037 public String getReuseElementsStr() { 1038 1039 return m_reuseElementsStr; 1040 } 1041 1042 /** 1043 * Returns the OpenCms default the time zone.<p> 1044 * 1045 * @return the OpenCms default the time zone 1046 */ 1047 public TimeZone getTimeZone() { 1048 1049 return m_timeZone; 1050 } 1051 1052 /** 1053 * Initializes this locale manager with the OpenCms system configuration.<p> 1054 * 1055 * @param cms an OpenCms context object that must have been initialized with "Admin" permissions 1056 */ 1057 public void initialize(CmsObject cms) { 1058 1059 if (!m_availableLocales.contains(Locale.ENGLISH)) { 1060 throw new RuntimeException("The locale 'en' must be configured in opencms-system.xml."); 1061 } 1062 // init the locale handler 1063 m_localeHandler.initHandler(cms); 1064 // set default locale 1065 m_defaultLocale = m_defaultLocales.get(0); 1066 initLanguageDetection(); 1067 // set initialized status 1068 m_initialized = true; 1069 if (CmsLog.INIT.isInfoEnabled()) { 1070 CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_I18N_CONFIG_VFSACCESS_0)); 1071 } 1072 } 1073 1074 /** 1075 * Returns <code>true</code> if this locale manager is fully initialized.<p> 1076 * 1077 * This is required to prevent errors during unit tests, 1078 * simple unit tests will usually not have a fully 1079 * initialized locale manager available.<p> 1080 * 1081 * @return true if the locale manager is fully initialized 1082 */ 1083 public boolean isInitialized() { 1084 1085 return m_initialized; 1086 } 1087 1088 /** 1089 * Sets the configured locale handler.<p> 1090 * 1091 * @param localeHandler the locale handler to set 1092 */ 1093 public void setLocaleHandler(I_CmsLocaleHandler localeHandler) { 1094 1095 if (localeHandler != null) { 1096 m_localeHandler = localeHandler; 1097 } 1098 if (CmsLog.INIT.isInfoEnabled()) { 1099 CmsLog.INIT.info( 1100 Messages.get().getBundle().key( 1101 Messages.INIT_I18N_CONFIG_LOC_HANDLER_1, 1102 m_localeHandler.getClass().getName())); 1103 } 1104 } 1105 1106 /** 1107 * Sets the 'reuse-elemnts option value.<p> 1108 * 1109 * @param reuseElements the option value 1110 */ 1111 public void setReuseElements(String reuseElements) { 1112 1113 m_reuseElementsStr = reuseElements; 1114 } 1115 1116 /** 1117 * Sets OpenCms default the time zone.<p> 1118 * 1119 * If the name can not be resolved as time zone ID, then "GMT" is used.<p> 1120 * 1121 * @param timeZoneName the name of the time zone to set, for example "GMT" 1122 */ 1123 public void setTimeZone(String timeZoneName) { 1124 1125 // according to JavaDoc, "GMT" is the default time zone if the name can not be resolved 1126 m_timeZone = TimeZone.getTimeZone(timeZoneName); 1127 } 1128 1129 /** 1130 * Returns true if the 'copy page' dialog should reuse elements in auto mode when copying to a different locale.<p> 1131 * 1132 * @return true if auto mode of the 'copy page' dialog should reuse elements 1133 */ 1134 public boolean shouldReuseElements() { 1135 1136 boolean isFalseInConfig = Boolean.FALSE.toString().equalsIgnoreCase(StringUtils.trim(m_reuseElementsStr)); 1137 return !isFalseInConfig; 1138 } 1139 1140 /** 1141 * Returns a list of available locale names derived from the given locale names.<p> 1142 * 1143 * Each name in the given list is checked against the internal hash map of allowed locales, 1144 * and is appended to the resulting list only if the locale exists.<p> 1145 * 1146 * @param locales List of locales to check 1147 * @return list of available locales derived from the given locale names 1148 */ 1149 private List<Locale> checkLocaleNames(List<Locale> locales) { 1150 1151 if (locales == null) { 1152 return null; 1153 } 1154 List<Locale> result = new ArrayList<Locale>(); 1155 Iterator<Locale> i = locales.iterator(); 1156 while (i.hasNext()) { 1157 Locale locale = i.next(); 1158 if (m_availableLocales.contains(locale)) { 1159 result.add(locale); 1160 } 1161 } 1162 return result; 1163 } 1164 1165 /** 1166 * Clears the caches in the locale manager.<p> 1167 */ 1168 private void clearCaches() { 1169 1170 // flush all caches 1171 OpenCms.getMemoryMonitor().flushCache(CmsMemoryMonitor.CacheType.LOCALE); 1172 CmsResourceBundleLoader.flushBundleCache(); 1173 1174 if (LOG.isDebugEnabled()) { 1175 LOG.debug(Messages.get().getBundle().key(Messages.LOG_LOCALE_MANAGER_FLUSH_CACHE_1, "EVENT_CLEAR_CACHES")); 1176 } 1177 } 1178 1179 /** 1180 * Internal helper, returns an array of default locales for the given default names.<p> 1181 * 1182 * If required returns the system configured default locales.<p> 1183 * 1184 * @param defaultNames the default locales to use, can be <code>null</code> or a comma separated list 1185 * of locales, for example <code>"en, de"</code> 1186 * 1187 * @return an array of default locales for the given default names 1188 */ 1189 private List<Locale> getDefaultLocales(String defaultNames) { 1190 1191 List<Locale> result = null; 1192 if (defaultNames != null) { 1193 result = getAvailableLocales(defaultNames); 1194 } 1195 if ((result == null) || (result.size() == 0)) { 1196 return getDefaultLocales(); 1197 } else { 1198 return result; 1199 } 1200 } 1201 1202 /** 1203 * Initializes the language detection.<p> 1204 */ 1205 private void initLanguageDetection() { 1206 1207 try { 1208 // use a seed for initializing the language detection for making sure the 1209 // same probabilities are detected for the same document contents 1210 DetectorFactory.clear(); 1211 DetectorFactory.setSeed(42L); 1212 DetectorFactory.loadProfile(loadProfiles(getAvailableLocales())); 1213 } catch (Exception e) { 1214 LOG.error(Messages.get().getBundle().key(Messages.INIT_I18N_LANG_DETECT_FAILED_0), e); 1215 } 1216 } 1217 1218 /** 1219 * Load the profiles from the classpath.<p> 1220 * 1221 * @param locales the locales to initialize.<p> 1222 * 1223 * @return a list of profiles 1224 * 1225 * @throws Exception if something goes wrong 1226 */ 1227 private List<String> loadProfiles(List<Locale> locales) throws Exception { 1228 1229 List<String> profiles = new ArrayList<String>(); 1230 List<String> languagesAdded = new ArrayList<String>(); 1231 for (Locale locale : locales) { 1232 try { 1233 String lang = locale.getLanguage(); 1234 // make sure not to add a profile twice 1235 if (!languagesAdded.contains(lang)) { 1236 languagesAdded.add(lang); 1237 String profileFile = "profiles" + "/" + lang; 1238 InputStream is = getClass().getClassLoader().getResourceAsStream(profileFile); 1239 if (is != null) { 1240 String profile = IOUtils.toString(is, "UTF-8"); 1241 if ((profile != null) && (profile.length() > 0)) { 1242 profiles.add(profile); 1243 } 1244 is.close(); 1245 } else { 1246 LOG.warn( 1247 Messages.get().getBundle().key( 1248 Messages.INIT_I18N_LAND_DETECT_PROFILE_NOT_AVAILABLE_1, 1249 locale)); 1250 } 1251 } 1252 } catch (Exception e) { 1253 LOG.error( 1254 Messages.get().getBundle().key(Messages.INIT_I18N_LAND_DETECT_LOADING_PROFILE_FAILED_1, locale), 1255 e); 1256 } 1257 } 1258 return profiles; 1259 } 1260}