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 Integer.valueOf(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 * Note that if the <code>localesconfigured<code> node contains a locale variant for a specific country (e.g. de_DE), 527 * then both that locale and the locale without the country suffix will be in the returned list. 528 * 529 * @return the list of available locale names, e.g. <code>en, de</code> 530 * 531 * @see #getDefaultLocales() 532 */ 533 public List<Locale> getAvailableLocales() { 534 535 return Collections.unmodifiableList(m_availableLocales); 536 } 537 538 /** 539 * Returns an array of available locale names for the given resource.<p> 540 * 541 * @param cms the current cms permission object 542 * @param resource the resource 543 * 544 * @return an array of available locale names 545 * 546 * @see #getAvailableLocales() 547 */ 548 public List<Locale> getAvailableLocales(CmsObject cms, CmsResource resource) { 549 550 String availableNames = null; 551 try { 552 availableNames = cms.readPropertyObject( 553 resource, 554 CmsPropertyDefinition.PROPERTY_AVAILABLE_LOCALES, 555 true).getValue(); 556 } catch (CmsException exc) { 557 LOG.debug("Could not read available locales property for resource " + resource.getRootPath(), exc); 558 } 559 560 List<Locale> result = null; 561 if (availableNames != null) { 562 result = getAvailableLocales(availableNames); 563 } 564 if ((result == null) || (result.size() == 0)) { 565 return Collections.unmodifiableList(m_availableLocales); 566 } else { 567 return result; 568 } 569 } 570 571 /** 572 * Returns an array of available locale names for the given resource.<p> 573 * 574 * @param cms the current cms permission object 575 * @param resourceName the name of the resource 576 * 577 * @return an array of available locale names 578 * 579 * @see #getAvailableLocales() 580 */ 581 public List<Locale> getAvailableLocales(CmsObject cms, String resourceName) { 582 583 String availableNames = null; 584 try { 585 availableNames = cms.readPropertyObject( 586 resourceName, 587 CmsPropertyDefinition.PROPERTY_AVAILABLE_LOCALES, 588 true).getValue(); 589 } catch (CmsException exc) { 590 LOG.debug("Could not read available locales property for resource " + resourceName, exc); 591 } 592 593 List<Locale> result = null; 594 if (availableNames != null) { 595 result = getAvailableLocales(availableNames); 596 } 597 if ((result == null) || (result.size() == 0)) { 598 return Collections.unmodifiableList(m_availableLocales); 599 } else { 600 return result; 601 } 602 } 603 604 /** 605 * Returns a List of available locales from a comma separated string of locale names.<p> 606 * 607 * All names are filtered against the allowed available locales 608 * configured in <code>opencms-system.xml</code>.<P> 609 * 610 * @param names a comma-separated String of locale names 611 * @return List of locales created from the given locale names 612 * 613 * @see #getAvailableLocales() 614 */ 615 public List<Locale> getAvailableLocales(String names) { 616 617 return checkLocaleNames(getLocales(names)); 618 } 619 620 /** 621 * Returns the best available locale present in the given XML content, or the default locale.<p> 622 * 623 * @param cms the current OpenCms user context 624 * @param resource the resource 625 * @param content the XML content 626 * 627 * @return the locale 628 */ 629 public Locale getBestAvailableLocaleForXmlContent(CmsObject cms, CmsResource resource, I_CmsXmlDocument content) { 630 631 Locale locale = getDefaultLocale(cms, resource); 632 if (!content.hasLocale(locale)) { 633 // if the requested locale is not available, get the first matching default locale, 634 // or the first matching available locale 635 boolean foundLocale = false; 636 if (content.getLocales().size() > 0) { 637 List<Locale> locales = getDefaultLocales(cms, resource); 638 for (Locale defaultLocale : locales) { 639 if (content.hasLocale(defaultLocale)) { 640 locale = defaultLocale; 641 foundLocale = true; 642 break; 643 } 644 } 645 if (!foundLocale) { 646 locales = getAvailableLocales(cms, resource); 647 for (Locale availableLocale : locales) { 648 if (content.hasLocale(availableLocale)) { 649 locale = availableLocale; 650 foundLocale = true; 651 break; 652 } 653 } 654 } 655 } 656 } 657 return locale; 658 } 659 660 /** 661 * Tries to find the given requested locale (eventually simplified) in the collection of available locales, 662 * if the requested locale is not found it will return the first match from the given list of default locales.<p> 663 * 664 * @param requestedLocale the requested locale, if this (or a simplified version of it) is available it will be returned 665 * @param defaults a list of default locales to use in case the requested locale is not available 666 * @param available the available locales to find a match in 667 * 668 * @return the best matching locale name or null if no name matches 669 */ 670 public Locale getBestMatchingLocale(Locale requestedLocale, List<Locale> defaults, List<Locale> available) { 671 672 if ((available == null) || available.isEmpty()) { 673 // no locales are available at all 674 return null; 675 } 676 677 // the requested locale is the match we want to find most 678 if (available.contains(requestedLocale)) { 679 // check if the requested locale is directly available 680 return requestedLocale; 681 } 682 if (requestedLocale.getVariant().length() > 0) { 683 // locale has a variant like "en_EN_whatever", try only with language and country 684 Locale check = new Locale(requestedLocale.getLanguage(), requestedLocale.getCountry(), ""); 685 if (available.contains(check)) { 686 return check; 687 } 688 } 689 if (requestedLocale.getCountry().length() > 0) { 690 // locale has a country like "en_EN", try only with language 691 Locale check = new Locale(requestedLocale.getLanguage(), "", ""); 692 if (available.contains(check)) { 693 return check; 694 } 695 } 696 697 // available locales do not match the requested locale 698 if ((defaults == null) || defaults.isEmpty()) { 699 // if we have no default locales we are out of luck 700 return null; 701 } 702 703 // no match found for the requested locale, return the first match from the default locales 704 return getFirstMatchingLocale(defaults, available); 705 } 706 707 /** 708 * Returns the "the" default locale for the given resource.<p> 709 * 710 * It's possible to override the system default (see {@link #getDefaultLocale()}) by setting the property 711 * <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> to a comma separated list of locale names. 712 * This property is inherited from the parent folders. 713 * This method will return the first locale from that list.<p> 714 * 715 * The default locale must be contained in the set of configured available locales, 716 * see {@link #getAvailableLocales()}. 717 * In case an invalid locale has been set with the property, this locale is ignored and the 718 * same result as {@link #getDefaultLocale()} is returned.<p> 719 * 720 * In case the property <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> has not been set 721 * on the resource or a parent folder, 722 * this method returns the same result as {@link #getDefaultLocale()}.<p> 723 * 724 * @param cms the current cms permission object 725 * @param resource the resource 726 * @return an array of default locale names 727 * 728 * @see #getDefaultLocales() 729 * @see #getDefaultLocales(CmsObject, String) 730 */ 731 public Locale getDefaultLocale(CmsObject cms, CmsResource resource) { 732 733 List<Locale> defaultLocales = getDefaultLocales(cms, resource); 734 Locale result; 735 if (defaultLocales.size() > 0) { 736 result = defaultLocales.get(0); 737 } else { 738 result = getDefaultLocale(); 739 } 740 return result; 741 } 742 743 /** 744 * Returns the "the" default locale for the given resource.<p> 745 * 746 * It's possible to override the system default (see {@link #getDefaultLocale()}) by setting the property 747 * <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> to a comma separated list of locale names. 748 * This property is inherited from the parent folders. 749 * This method will return the first locale from that list.<p> 750 * 751 * The default locale must be contained in the set of configured available locales, 752 * see {@link #getAvailableLocales()}. 753 * In case an invalid locale has been set with the property, this locale is ignored and the 754 * same result as {@link #getDefaultLocale()} is returned.<p> 755 * 756 * In case the property <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> has not been set 757 * on the resource or a parent folder, 758 * this method returns the same result as {@link #getDefaultLocale()}.<p> 759 * 760 * @param cms the current cms permission object 761 * @param resourceName the name of the resource 762 * @return an array of default locale names 763 * 764 * @see #getDefaultLocales() 765 * @see #getDefaultLocales(CmsObject, String) 766 */ 767 public Locale getDefaultLocale(CmsObject cms, String resourceName) { 768 769 List<Locale> defaultLocales = getDefaultLocales(cms, resourceName); 770 Locale result; 771 if (defaultLocales.size() > 0) { 772 result = defaultLocales.get(0); 773 } else { 774 result = getDefaultLocale(); 775 } 776 return result; 777 } 778 779 /** 780 * Returns the list of default {@link Locale}s configured in <code>opencms-system.xml</code>, 781 * in the <code>opencms/system/internationalization/localesdefault</code> node.<p> 782 * 783 * Since the default locale is always available, the result list will always contain at least one Locale.<p> 784 * 785 * It's possible to override the system default by setting the property 786 * <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> to a comma separated list of locale names. 787 * This property is inherited from the parent folders.<p> 788 * 789 * The default locales must be a subset of the configured available locales, see {@link #getAvailableLocales()}. 790 * In case an invalid locale has been set with the property, this locale is ignored.<p> 791 * 792 * The default locale names are used as a fallback mechanism in case a locale is requested 793 * that can not be found, for example when delivering content form an XML content.<p> 794 * 795 * There is a list of default locales (instead of just one default locale) since there 796 * are scenarios when one default is not enough. Consider the following example:<i> 797 * The main default locale is set to "en". An example XML content file contains just one language, 798 * in this case "de" and not "en". Now a request is made to the file for the locale "fr". If 799 * there would be only one default locale ("en"), we would have to give up. But since we allow more then 800 * one default, we can deliver the "de" content instead of a blank page.</I><p> 801 * 802 * @return the list of default locale names, e.g. <code>en, de</code> 803 * 804 * @see #getAvailableLocales() 805 */ 806 public List<Locale> getDefaultLocales() { 807 808 return m_defaultLocales; 809 } 810 811 /** 812 * Returns an array of default locales for the given resource.<p> 813 * 814 * Since the default locale is always available, the result list will always contain at least one Locale.<p> 815 * 816 * It's possible to override the system default (see {@link #getDefaultLocales()}) by setting the property 817 * <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> to a comma separated list of locale names. 818 * This property is inherited from the parent folders.<p> 819 * 820 * The default locales must be a subset of the configured available locales, see {@link #getAvailableLocales()}. 821 * In case an invalid locale has been set with the property, this locale is ignored.<p> 822 * 823 * In case the property <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> has not been set 824 * on the resource or a parent folder, 825 * this method returns the same result as {@link #getDefaultLocales()}.<p> 826 * 827 * Use this method in case you need to get all configured default options for a resource, 828 * if you just need the "the" default locale for a resource, 829 * use <code>{@link #getDefaultLocale(CmsObject, String)}</code>.<p> 830 * 831 * @param cms the current cms permission object 832 * @param resource the resource to read the default locale properties for 833 * @return an array of default locale names 834 * 835 * @see #getDefaultLocales() 836 * @see #getDefaultLocale(CmsObject, String) 837 * @see #getDefaultLocales(CmsObject, String) 838 * 839 * @since 7.0.2 840 */ 841 public List<Locale> getDefaultLocales(CmsObject cms, CmsResource resource) { 842 843 String defaultNames = null; 844 try { 845 defaultNames = cms.readPropertyObject(resource, CmsPropertyDefinition.PROPERTY_LOCALE, true).getValue(); 846 } catch (CmsException e) { 847 LOG.warn(Messages.get().getBundle().key(Messages.ERR_READ_ENCODING_PROP_1, cms.getSitePath(resource)), e); 848 } 849 return getDefaultLocales(defaultNames); 850 } 851 852 /** 853 * Returns an array of default locales for the given resource.<p> 854 * 855 * Since the default locale is always available, the result list will always contain at least one Locale.<p> 856 * 857 * It's possible to override the system default (see {@link #getDefaultLocales()}) by setting the property 858 * <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> to a comma separated list of locale names. 859 * This property is inherited from the parent folders.<p> 860 * 861 * The default locales must be a subset of the configured available locales, see {@link #getAvailableLocales()}. 862 * In case an invalid locale has been set with the property, this locale is ignored.<p> 863 * 864 * In case the property <code>{@link CmsPropertyDefinition#PROPERTY_LOCALE}</code> has not been set 865 * on the resource or a parent folder, 866 * this method returns the same result as {@link #getDefaultLocales()}.<p> 867 * 868 * Use this method in case you need to get all configured default options for a resource, 869 * if you just need the "the" default locale for a resource, 870 * use <code>{@link #getDefaultLocale(CmsObject, String)}</code>.<p> 871 * 872 * @param cms the current cms permission object 873 * @param resourceName the name of the resource 874 * @return an array of default locale names 875 * 876 * @see #getDefaultLocales() 877 * @see #getDefaultLocale(CmsObject, String) 878 * @see #getDefaultLocales(CmsObject, CmsResource) 879 */ 880 public List<Locale> getDefaultLocales(CmsObject cms, String resourceName) { 881 882 String defaultNames = null; 883 try { 884 defaultNames = cms.readPropertyObject(resourceName, CmsPropertyDefinition.PROPERTY_LOCALE, true).getValue(); 885 } catch (CmsException e) { 886 LOG.warn(Messages.get().getBundle().key(Messages.ERR_READ_ENCODING_PROP_1, resourceName), e); 887 } 888 return getDefaultLocales(defaultNames); 889 } 890 891 /** 892 * Returns the first matching locale (eventually simplified) from the available locales.<p> 893 * 894 * In case no match is found, code <code>null</code> is returned.<p> 895 * 896 * @param locales must be an ascending sorted list of locales in order of preference 897 * @param available the available locales to find a match in 898 * 899 * @return the first precise or simplified match, or <code>null</code> in case no match is found 900 */ 901 public Locale getFirstMatchingLocale(List<Locale> locales, List<Locale> available) { 902 903 Iterator<Locale> i; 904 // first try a precise match 905 i = locales.iterator(); 906 while (i.hasNext()) { 907 Locale locale = i.next(); 908 if (available.contains(locale)) { 909 // precise match 910 return locale; 911 } 912 } 913 914 // now try a match only with language and country 915 i = locales.iterator(); 916 while (i.hasNext()) { 917 Locale locale = i.next(); 918 if (locale.getVariant().length() > 0) { 919 // the locale has a variant, try to match without the variant 920 locale = new Locale(locale.getLanguage(), locale.getCountry(), ""); 921 if (available.contains(locale)) { 922 // match 923 return locale; 924 } 925 } 926 } 927 928 // finally try a match only with language 929 i = locales.iterator(); 930 while (i.hasNext()) { 931 Locale locale = i.next(); 932 if (locale.getCountry().length() > 0) { 933 // the locale has a country, try to match without the country 934 locale = new Locale(locale.getLanguage(), "", ""); 935 if (available.contains(locale)) { 936 // match 937 return locale; 938 } 939 } 940 } 941 942 // no match 943 return null; 944 } 945 946 /** 947 * Returns the the appropriate locale/encoding for a request, 948 * using the "right" locale handler for the given resource.<p> 949 * 950 * Certain system folders (like the Workplace) require a special 951 * locale handler different from the configured handler. 952 * Use this method if you want to resolve locales exactly like 953 * the system does for a request.<p> 954 * 955 * @param req the current http request 956 * @param user the current user 957 * @param project the current project 958 * @param resource the URI of the requested resource (with full site root added) 959 * 960 * @return the i18n information to use for the given request context 961 */ 962 public CmsI18nInfo getI18nInfo(HttpServletRequest req, CmsUser user, CmsProject project, String resource) { 963 964 CmsI18nInfo i18nInfo = null; 965 966 // check if this is a request against a Workplace folder 967 if (OpenCms.getSiteManager().isWorkplaceRequest(req)) { 968 // The list of configured localized workplace folders 969 List<String> wpLocalizedFolders = OpenCms.getWorkplaceManager().getLocalizedFolders(); 970 for (int i = wpLocalizedFolders.size() - 1; i >= 0; i--) { 971 if (resource.startsWith(wpLocalizedFolders.get(i))) { 972 // use the workplace locale handler for this resource 973 i18nInfo = OpenCms.getWorkplaceManager().getI18nInfo(req, user, project, resource); 974 break; 975 } 976 } 977 } 978 if (i18nInfo == null) { 979 // use default locale handler 980 i18nInfo = m_localeHandler.getI18nInfo(req, user, project, resource); 981 } 982 983 // check the request for special parameters overriding the locale handler 984 Locale locale = null; 985 String encoding = null; 986 if (req != null) { 987 String localeParam = req.getParameter(CmsLocaleManager.PARAMETER_LOCALE); 988 // check request for parameters 989 if (localeParam != null) { 990 // "__locale" parameter found in request 991 locale = CmsLocaleManager.getLocale(localeParam); 992 } 993 // check for "__encoding" parameter in request 994 encoding = req.getParameter(CmsLocaleManager.PARAMETER_ENCODING); 995 } 996 997 // merge values from request with values from locale handler 998 if (locale == null) { 999 locale = i18nInfo.getLocale(); 1000 } 1001 if (encoding == null) { 1002 encoding = i18nInfo.getEncoding(); 1003 } 1004 1005 // still some values might be "null" 1006 if (locale == null) { 1007 locale = getDefaultLocale(); 1008 if (LOG.isDebugEnabled()) { 1009 LOG.debug(Messages.get().getBundle().key(Messages.LOG_LOCALE_NOT_FOUND_1, locale)); 1010 } 1011 } 1012 if (encoding == null) { 1013 encoding = OpenCms.getSystemInfo().getDefaultEncoding(); 1014 if (LOG.isDebugEnabled()) { 1015 LOG.debug(Messages.get().getBundle().key(Messages.LOG_ENCODING_NOT_FOUND_1, encoding)); 1016 } 1017 } 1018 1019 // return the merged values 1020 return new CmsI18nInfo(locale, encoding); 1021 } 1022 1023 /** 1024 * Returns the configured locale handler.<p> 1025 * 1026 * This handler is used to derive the appropriate locale/encoding for a request.<p> 1027 * 1028 * @return the locale handler 1029 */ 1030 public I_CmsLocaleHandler getLocaleHandler() { 1031 1032 return m_localeHandler; 1033 } 1034 1035 /** 1036 * Gets the string value of the 'reuse-elements' option.<p> 1037 * 1038 * @return the string value of the 'reuse-elements' option 1039 */ 1040 public String getReuseElementsStr() { 1041 1042 return m_reuseElementsStr; 1043 } 1044 1045 /** 1046 * Returns the OpenCms default the time zone.<p> 1047 * 1048 * @return the OpenCms default the time zone 1049 */ 1050 public TimeZone getTimeZone() { 1051 1052 return m_timeZone; 1053 } 1054 1055 /** 1056 * Initializes this locale manager with the OpenCms system configuration.<p> 1057 * 1058 * @param cms an OpenCms context object that must have been initialized with "Admin" permissions 1059 */ 1060 public void initialize(CmsObject cms) { 1061 1062 if (!m_availableLocales.contains(Locale.ENGLISH)) { 1063 throw new RuntimeException("The locale 'en' must be configured in opencms-system.xml."); 1064 } 1065 // init the locale handler 1066 m_localeHandler.initHandler(cms); 1067 // set default locale 1068 m_defaultLocale = m_defaultLocales.get(0); 1069 initLanguageDetection(); 1070 // set initialized status 1071 m_initialized = true; 1072 if (CmsLog.INIT.isInfoEnabled()) { 1073 CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_I18N_CONFIG_VFSACCESS_0)); 1074 } 1075 } 1076 1077 /** 1078 * Returns <code>true</code> if this locale manager is fully initialized.<p> 1079 * 1080 * This is required to prevent errors during unit tests, 1081 * simple unit tests will usually not have a fully 1082 * initialized locale manager available.<p> 1083 * 1084 * @return true if the locale manager is fully initialized 1085 */ 1086 public boolean isInitialized() { 1087 1088 return m_initialized; 1089 } 1090 1091 /** 1092 * Sets the configured locale handler.<p> 1093 * 1094 * @param localeHandler the locale handler to set 1095 */ 1096 public void setLocaleHandler(I_CmsLocaleHandler localeHandler) { 1097 1098 if (localeHandler != null) { 1099 m_localeHandler = localeHandler; 1100 } 1101 if (CmsLog.INIT.isInfoEnabled()) { 1102 CmsLog.INIT.info( 1103 Messages.get().getBundle().key( 1104 Messages.INIT_I18N_CONFIG_LOC_HANDLER_1, 1105 m_localeHandler.getClass().getName())); 1106 } 1107 } 1108 1109 /** 1110 * Sets the 'reuse-elemnts option value.<p> 1111 * 1112 * @param reuseElements the option value 1113 */ 1114 public void setReuseElements(String reuseElements) { 1115 1116 m_reuseElementsStr = reuseElements; 1117 } 1118 1119 /** 1120 * Sets OpenCms default the time zone.<p> 1121 * 1122 * If the name can not be resolved as time zone ID, then "GMT" is used.<p> 1123 * 1124 * @param timeZoneName the name of the time zone to set, for example "GMT" 1125 */ 1126 public void setTimeZone(String timeZoneName) { 1127 1128 // according to JavaDoc, "GMT" is the default time zone if the name can not be resolved 1129 m_timeZone = TimeZone.getTimeZone(timeZoneName); 1130 } 1131 1132 /** 1133 * Returns true if the 'copy page' dialog should reuse elements in auto mode when copying to a different locale.<p> 1134 * 1135 * @return true if auto mode of the 'copy page' dialog should reuse elements 1136 */ 1137 public boolean shouldReuseElements() { 1138 1139 boolean isFalseInConfig = Boolean.FALSE.toString().equalsIgnoreCase(StringUtils.trim(m_reuseElementsStr)); 1140 return !isFalseInConfig; 1141 } 1142 1143 /** 1144 * Returns a list of available locale names derived from the given locale names.<p> 1145 * 1146 * Each name in the given list is checked against the internal hash map of allowed locales, 1147 * and is appended to the resulting list only if the locale exists.<p> 1148 * 1149 * @param locales List of locales to check 1150 * @return list of available locales derived from the given locale names 1151 */ 1152 private List<Locale> checkLocaleNames(List<Locale> locales) { 1153 1154 if (locales == null) { 1155 return null; 1156 } 1157 List<Locale> result = new ArrayList<Locale>(); 1158 Iterator<Locale> i = locales.iterator(); 1159 while (i.hasNext()) { 1160 Locale locale = i.next(); 1161 if (m_availableLocales.contains(locale)) { 1162 result.add(locale); 1163 } 1164 } 1165 return result; 1166 } 1167 1168 /** 1169 * Clears the caches in the locale manager.<p> 1170 */ 1171 private void clearCaches() { 1172 1173 // flush all caches 1174 OpenCms.getMemoryMonitor().flushCache(CmsMemoryMonitor.CacheType.LOCALE); 1175 CmsResourceBundleLoader.flushBundleCache(); 1176 1177 if (LOG.isDebugEnabled()) { 1178 LOG.debug(Messages.get().getBundle().key(Messages.LOG_LOCALE_MANAGER_FLUSH_CACHE_1, "EVENT_CLEAR_CACHES")); 1179 } 1180 } 1181 1182 /** 1183 * Internal helper, returns an array of default locales for the given default names.<p> 1184 * 1185 * If required returns the system configured default locales.<p> 1186 * 1187 * @param defaultNames the default locales to use, can be <code>null</code> or a comma separated list 1188 * of locales, for example <code>"en, de"</code> 1189 * 1190 * @return an array of default locales for the given default names 1191 */ 1192 private List<Locale> getDefaultLocales(String defaultNames) { 1193 1194 List<Locale> result = null; 1195 if (defaultNames != null) { 1196 result = getAvailableLocales(defaultNames); 1197 } 1198 if ((result == null) || (result.size() == 0)) { 1199 return getDefaultLocales(); 1200 } else { 1201 return result; 1202 } 1203 } 1204 1205 /** 1206 * Initializes the language detection.<p> 1207 */ 1208 private void initLanguageDetection() { 1209 1210 try { 1211 // use a seed for initializing the language detection for making sure the 1212 // same probabilities are detected for the same document contents 1213 DetectorFactory.clear(); 1214 DetectorFactory.setSeed(42L); 1215 DetectorFactory.loadProfile(loadProfiles(getAvailableLocales())); 1216 } catch (Exception e) { 1217 LOG.error(Messages.get().getBundle().key(Messages.INIT_I18N_LANG_DETECT_FAILED_0), e); 1218 } 1219 } 1220 1221 /** 1222 * Load the profiles from the classpath.<p> 1223 * 1224 * @param locales the locales to initialize.<p> 1225 * 1226 * @return a list of profiles 1227 * 1228 * @throws Exception if something goes wrong 1229 */ 1230 private List<String> loadProfiles(List<Locale> locales) throws Exception { 1231 1232 List<String> profiles = new ArrayList<String>(); 1233 List<String> languagesAdded = new ArrayList<String>(); 1234 for (Locale locale : locales) { 1235 try { 1236 String lang = locale.getLanguage(); 1237 // make sure not to add a profile twice 1238 if (!languagesAdded.contains(lang)) { 1239 languagesAdded.add(lang); 1240 String profileFile = "profiles" + "/" + lang; 1241 InputStream is = getClass().getClassLoader().getResourceAsStream(profileFile); 1242 if (is != null) { 1243 String profile = IOUtils.toString(is, "UTF-8"); 1244 if ((profile != null) && (profile.length() > 0)) { 1245 profiles.add(profile); 1246 } 1247 is.close(); 1248 } else { 1249 LOG.warn( 1250 Messages.get().getBundle().key( 1251 Messages.INIT_I18N_LAND_DETECT_PROFILE_NOT_AVAILABLE_1, 1252 locale)); 1253 } 1254 } 1255 } catch (Exception e) { 1256 LOG.error( 1257 Messages.get().getBundle().key(Messages.INIT_I18N_LAND_DETECT_LOADING_PROFILE_FAILED_1, locale), 1258 e); 1259 } 1260 } 1261 return profiles; 1262 } 1263}