diff --git a/dotCMS/src/main/java/com/dotcms/languagevariable/business/LanguageVariableAPI.java b/dotCMS/src/main/java/com/dotcms/languagevariable/business/LanguageVariableAPI.java index eb04286cd570..7e12ee971afc 100644 --- a/dotCMS/src/main/java/com/dotcms/languagevariable/business/LanguageVariableAPI.java +++ b/dotCMS/src/main/java/com/dotcms/languagevariable/business/LanguageVariableAPI.java @@ -8,6 +8,7 @@ import com.liferay.portal.model.User; import java.util.List; import java.util.Map; +import java.util.Optional; /** * Provides access to Language Variable objects in the system, which allow you to associate a key to @@ -146,6 +147,15 @@ public List getAllLanguageVariablesKeyStartsWith(final String key, fin */ List findVariables(final long langId) throws DotDataException; + /** + * Returns an Optional of {@link LanguageVariable} matching the specified language ID and key. + * @param languageId - The ID of the language that the variable was created for. + * @param key - The key to the Language Variable that starts with. + * @return Optional of Language Variables. + * @throws DotDataException - If an error occurs while retrieving the Language Variables. + */ + Optional findVariable(final long languageId, final String key) throws DotDataException; + /** * Returns a list of {@link LanguageVariable} that the key starts with the specified key and * diff --git a/dotCMS/src/main/java/com/dotcms/languagevariable/business/LanguageVariableAPIImpl.java b/dotCMS/src/main/java/com/dotcms/languagevariable/business/LanguageVariableAPIImpl.java index 401b2c2ffb3f..6537785eef21 100644 --- a/dotCMS/src/main/java/com/dotcms/languagevariable/business/LanguageVariableAPIImpl.java +++ b/dotCMS/src/main/java/com/dotcms/languagevariable/business/LanguageVariableAPIImpl.java @@ -21,6 +21,7 @@ import io.vavr.Lazy; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; /** @@ -171,6 +172,25 @@ public List findVariables(final long langId) throws DotDataExc }); } + /** + * {@inheritDoc} + */ + @CloseDBIfOpened + @Override + public Optional findVariable(final long languageId, final String key) throws DotDataException { + final LanguageVariableFactory factory = FactoryLocator.getLanguageVariableFactory(); + final ContentType contentType = langVarContentType.get(); + final LanguageCache languageCache = CacheLocator.getLanguageCache(); + return languageCache.ifPresentGetOrElseFetch(languageId, key, ()->{ + try { + return factory.findVariables(contentType, languageId, 0, 0, null); + } catch (DotDataException e) { + Logger.error(this, "Error finding language variables", e); + return List.of(); + } + }); + } + /** * {@inheritDoc} */ diff --git a/dotCMS/src/main/java/com/dotcms/languagevariable/business/LanguageVariableFactory.java b/dotCMS/src/main/java/com/dotcms/languagevariable/business/LanguageVariableFactory.java index 37ba4b681228..10eb70b45480 100644 --- a/dotCMS/src/main/java/com/dotcms/languagevariable/business/LanguageVariableFactory.java +++ b/dotCMS/src/main/java/com/dotcms/languagevariable/business/LanguageVariableFactory.java @@ -3,6 +3,7 @@ import com.dotcms.contenttype.model.type.ContentType; import com.dotmarketing.exception.DotDataException; import java.util.List; +import java.util.Optional; /** * LanguageVariableFactory is the Data Access Object (DAO) for LanguageVariable diff --git a/dotCMS/src/main/java/com/dotcms/languagevariable/business/LanguageVariableFactoryImpl.java b/dotCMS/src/main/java/com/dotcms/languagevariable/business/LanguageVariableFactoryImpl.java index ef215ba417a7..4902ad4b31ed 100644 --- a/dotCMS/src/main/java/com/dotcms/languagevariable/business/LanguageVariableFactoryImpl.java +++ b/dotCMS/src/main/java/com/dotcms/languagevariable/business/LanguageVariableFactoryImpl.java @@ -9,6 +9,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; /** * Implementation class for the {@link LanguageVariableFactory}. diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v2/languages/LanguagesResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v2/languages/LanguagesResource.java index 042b3f0c709e..da3023654e50 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v2/languages/LanguagesResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v2/languages/LanguagesResource.java @@ -387,7 +387,7 @@ public Response getAllMessages ( if(UtilMethods.isSet(language1)) { final LanguageVariableAPI languageVariableAPI = APILocator.getLanguageVariableAPI(); - if(languageAPI.isLocalizationEnhancementsEnabled()) { + if(isLocalizationEnhancementsEnabled()) { final Language matchingLang = languageAPI.getLanguage(currentLocale.getLanguage(),currentLocale.getCountry()); // Enhanced Language Vars final List variables = languageVariableAPI.findVariables(matchingLang.getId()); diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/languagesmanager/business/LanguageAPI.java b/dotCMS/src/main/java/com/dotmarketing/portlets/languagesmanager/business/LanguageAPI.java index 1f39391f7a1e..a6fe655b8a77 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/languagesmanager/business/LanguageAPI.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/languagesmanager/business/LanguageAPI.java @@ -1,7 +1,12 @@ package com.dotmarketing.portlets.languagesmanager.business; import com.dotcms.content.elasticsearch.business.DotIndexException; +import com.dotmarketing.exception.DotDataException; +import com.dotmarketing.exception.DotSecurityException; +import com.dotmarketing.portlets.languagesmanager.model.Language; +import com.dotmarketing.portlets.languagesmanager.model.LanguageKey; import com.dotmarketing.util.Config; +import com.liferay.portal.model.User; import io.vavr.Lazy; import java.util.Collection; import java.util.List; @@ -10,12 +15,6 @@ import java.util.Optional; import java.util.Set; -import com.dotmarketing.exception.DotDataException; -import com.dotmarketing.exception.DotSecurityException; -import com.dotmarketing.portlets.languagesmanager.model.Language; -import com.dotmarketing.portlets.languagesmanager.model.LanguageKey; -import com.liferay.portal.model.User; - /** * Provides access to information related to the different languages that can be added to the * system. @@ -32,10 +31,18 @@ */ public interface LanguageAPI { + String LOCALIZATION_ENHANCEMENTS_ENABLED = "LOCALIZATION_ENHANCEMENTS_ENABLED"; Lazy localizationEnhancementsEnabled = Lazy.of( - () -> Config.getBooleanProperty("LOCALIZATION_ENHANCEMENTS_ENABLED", true)); + () -> Config.getBooleanProperty(LOCALIZATION_ENHANCEMENTS_ENABLED, true)); + + static boolean isLocalizationEnhancementsEnabled() { + //this system property is used to enable/disable the localization enhancements from any integration context + // since the Config class is wrapped within a Lazy object and once it is loaded it is not possible to change the value + final String enabled = System.getProperty( + LOCALIZATION_ENHANCEMENTS_ENABLED); - boolean isLocalizationEnhancementsEnabled(); + return enabled != null ? Boolean.parseBoolean(enabled) : localizationEnhancementsEnabled.get(); + } /** * diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/languagesmanager/business/LanguageAPIImpl.java b/dotCMS/src/main/java/com/dotmarketing/portlets/languagesmanager/business/LanguageAPIImpl.java index 7c91c804b847..4c051b58cd4e 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/languagesmanager/business/LanguageAPIImpl.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/languagesmanager/business/LanguageAPIImpl.java @@ -3,6 +3,7 @@ import com.dotcms.business.CloseDBIfOpened; import com.dotcms.business.WrapInTransaction; import com.dotcms.content.elasticsearch.business.DotIndexException; +import com.dotcms.languagevariable.business.LanguageVariable; import com.dotcms.languagevariable.business.LanguageVariableAPI; import com.dotcms.rendering.velocity.util.VelocityUtil; import com.dotcms.system.event.local.business.LocalSystemEventsAPI; @@ -28,7 +29,7 @@ import com.liferay.portal.language.LanguageException; import com.liferay.portal.language.LanguageUtil; import com.liferay.portal.model.User; -import io.vavr.Lazy; +import io.vavr.control.Try; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -38,7 +39,6 @@ import java.util.Optional; import java.util.Set; import java.util.TreeSet; -import java.util.function.BooleanSupplier; import javax.servlet.http.HttpServletRequest; import org.apache.commons.lang.math.NumberUtils; import org.apache.velocity.tools.view.context.ViewContext; @@ -53,23 +53,14 @@ */ public class LanguageAPIImpl implements LanguageAPI { - private static final LanguageKeyComparator LANGUAGE_KEY_COMPARATOR = new LanguageKeyComparator(); + + private final static LanguageKeyComparator LANGUAGE_KEY_COMPARATOR = new LanguageKeyComparator(); private HttpServletRequest request; // todo: this should be decouple from the api private LanguageFactory factory; private LanguageVariableAPI languageVariableAPI; private final LocalSystemEventsAPI localSystemEventsAPI = APILocator.getLocalSystemEventsAPI(); - private BooleanSupplier localizationEnhancementsEnabledOverride; - - public LanguageAPIImpl( - LanguageFactory factory, - LanguageVariableAPI languageVariableAPI, - BooleanSupplier localizationEnhancementsEnabledOverride) { - this.factory = factory; - this.languageVariableAPI = languageVariableAPI; - this.localizationEnhancementsEnabledOverride = localizationEnhancementsEnabledOverride; - } /** * Inits the service with the user {@link ViewContext} * @param obj @@ -79,31 +70,13 @@ public void init(final Object obj) { this.request = context.getRequest(); // todo: this is just getting it for the user, so instead of getting the request should get the user. } - @Override - public boolean isLocalizationEnhancementsEnabled() { - if(localizationEnhancementsEnabledOverride != null) { - return localizationEnhancementsEnabledOverride.getAsBoolean(); - } - return localizationEnhancementsEnabled.get(); - } - /** * Creates a new instance of the {@link LanguageAPI}. */ public LanguageAPIImpl() { - this(FactoryLocator.getLanguageFactory(), APILocator.getLanguageVariableAPI(), null); - } - - /** - * Creates a new instance of the {@link LanguageAPI}. - * @param localizationEnhancementsEnabledOverride - */ - @VisibleForTesting - public LanguageAPIImpl(BooleanSupplier localizationEnhancementsEnabledOverride) { - this(FactoryLocator.getLanguageFactory(), APILocator.getLanguageVariableAPI(), localizationEnhancementsEnabledOverride); + factory = FactoryLocator.getLanguageFactory(); } - @Override @WrapInTransaction public void deleteLanguage(final Language language) { @@ -317,18 +290,22 @@ public void saveLanguageKeys(final Language lang, final Map gene @CloseDBIfOpened @Override public Map getStringsAsMap(final Locale locale, final Collection keys) { + final LanguageVariableAPI langVarsAPI = getLanguageVariableAPI(); final Map messagesMap = new HashMap<>(); if (null != keys) { final Language lang = APILocator.getLanguageAPI().getLanguage(locale.getLanguage(), locale.getCountry()); keys.forEach(messageKey -> { - - String message = (lang != null) - ? getStringKey(lang, messageKey) - : getStringFromPropertiesFile(locale, messageKey) ; - message = (message == null) ? messageKey : message; - messagesMap.put(messageKey, message); - + final Optional variable = Try.of(()->langVarsAPI.findVariable(lang.getId(), messageKey)).getOrElse(Optional.empty()); + if(variable.isPresent()){ + messagesMap.put(messageKey, variable.get().value()); + } else { + String message = (lang != null) + ? getStringKey(lang, messageKey) + : getStringFromPropertiesFile(locale, messageKey); + message = (message == null) ? messageKey : message; + messagesMap.put(messageKey, message); + } }); } @@ -338,15 +315,29 @@ public Map getStringsAsMap(final Locale locale, final Collection @CloseDBIfOpened @Override public String getStringKey ( final Language lang, final String key ) { + final LanguageVariableAPI langVarsAPI = getLanguageVariableAPI(); - final User user = getUser(); + final Optional variable = Try.of(()->langVarsAPI.findVariable(lang.getId(), key)).getOrElse(Optional.empty()); + if(variable.isPresent()){ + return variable.get().value(); + } + + final User user = getUser(); // First, look it up using the new Language Variable API - final String value = getLanguageVariableAPI().getLanguageVariableRespectingFrontEndRoles(key, lang.getId(), user); + final String value = langVarsAPI.getLanguageVariableRespectingFrontEndRoles(key, lang.getId(), user); // If not found, retrieve value from legacy Language Variables or the appropriate final String countryCode = null == lang.getCountryCode()?"":lang.getCountryCode(); return (UtilMethods.isNotSet(value) || value.equals(key)) ? this.getStringFromPropertiesFile(new Locale( lang.getLanguageCode(), countryCode ), key) : value; } + /** + * This method internally uses MultiLanguageResourceBundle to get the value of the key. + * if the enhancedLanguageVariables is enabled, it will use the LanguageVariableAPI to get the value of the key. + * Therefore, it won't necessarily load the value from the properties file. And we should probably rename this method to getStringKeyFromLanguageVariable + * @param locale + * @param key + * @return + */ private String getStringFromPropertiesFile (final Locale locale, final String key) { String value = null; try { diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/languagesmanager/business/LanguageCache.java b/dotCMS/src/main/java/com/dotmarketing/portlets/languagesmanager/business/LanguageCache.java index 38cda133ddd9..cd00419ad7af 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/languagesmanager/business/LanguageCache.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/languagesmanager/business/LanguageCache.java @@ -8,6 +8,7 @@ import com.dotmarketing.business.DotCacheException; import com.dotmarketing.portlets.languagesmanager.model.Language; import com.dotmarketing.portlets.languagesmanager.model.LanguageKey; +import java.util.Optional; import java.util.concurrent.Callable; /** @@ -86,7 +87,7 @@ public abstract class LanguageCache implements Cachable{ * @return */ public abstract List ifPresentGetOrElseFetch(final long languageId, - Callable> fetch) throws DotDataException; + Callable> fetchFunction) throws DotDataException; /** * Removes the language stored under the key LANGUAGE_KEY_PREFIX + languageId @@ -99,4 +100,23 @@ public abstract List ifPresentGetOrElseFetch(final long langua * @param languageId the language id */ public abstract List getVars(final long languageId); + + /** + * Removes the language stored under the key LANGUAGE_KEY_PREFIX + languageId + * @param languageId the language id + * @param key the key + * @return the language variable + */ + public abstract Optional getVar(final long languageId, final String key); + + /** + * Removes the language stored under the key LANGUAGE_KEY_PREFIX + languageId + * @param languageId the language id + * @param key the key + * @param fetchFunction the fetch function + * @return the language variable + * @throws DotDataException if an error occurs + */ + public abstract Optional ifPresentGetOrElseFetch(final long languageId, final String key, + final Callable> fetchFunction) throws DotDataException; } diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/languagesmanager/business/LanguageCacheImpl.java b/dotCMS/src/main/java/com/dotmarketing/portlets/languagesmanager/business/LanguageCacheImpl.java index 930d757fe22d..111d52f0be55 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/languagesmanager/business/LanguageCacheImpl.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/languagesmanager/business/LanguageCacheImpl.java @@ -1,18 +1,22 @@ package com.dotmarketing.portlets.languagesmanager.business; import com.dotcms.languagevariable.business.LanguageVariable; -import com.dotmarketing.exception.DotDataException; -import java.util.List; - import com.dotmarketing.business.CacheLocator; import com.dotmarketing.business.DotCacheAdministrator; import com.dotmarketing.business.DotCacheException; +import com.dotmarketing.exception.DotDataException; import com.dotmarketing.portlets.languagesmanager.model.Language; import com.dotmarketing.portlets.languagesmanager.model.LanguageKey; import com.dotmarketing.util.Logger; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import java.util.function.BinaryOperator; +import java.util.stream.Collectors; /** * @author David @@ -261,7 +265,7 @@ public void clearVarsByLang(final long languageId){ return; } @SuppressWarnings("unchecked") - final ConcurrentMap> langVarCache = (ConcurrentMap>) perLangCache; + final ConcurrentMap> langVarCache = (ConcurrentMap>) perLangCache; final String languageIdStr = String.valueOf(languageId); langVarCache.remove(languageIdStr); Logger.debug(this, LANGUAGE_VARIABLES_FOR_LANGUAGE_WITH_ID + languageId + " have been removed from cache."); @@ -284,7 +288,7 @@ public void clearVariables(){ */ @Override public List ifPresentGetOrElseFetch(final long languageId, - final Callable> fetch) throws DotDataException { + final Callable> fetchFunction) throws DotDataException { final String languageIdStr = String.valueOf(languageId); final String group = getSecondaryGroup(); final DotCacheAdministrator cache = CacheLocator.getCacheAdministrator(); @@ -292,16 +296,16 @@ public List ifPresentGetOrElseFetch(final long languageId, try { final Object perLangCache = cache.getNoThrow(LANG_VARIABLES_CACHE, group); if (perLangCache == null) { - result = fetch.call(); + result = fetchFunction.call(); Logger.debug(this, LANGUAGE_VARIABLES_FOR_LANGUAGE_WITH_ID + languageId + " has been fetched from the database."); putVars(languageId, result); } else { - @SuppressWarnings("unchecked") final ConcurrentMap> langVarCache = (ConcurrentMap>) perLangCache; - final List variables = langVarCache.get(languageIdStr); + @SuppressWarnings("unchecked") final ConcurrentMap> langVarCache = (ConcurrentMap>) perLangCache; + final Map variables = langVarCache.get(languageIdStr); if (variables != null) { - result = variables; + result = List.copyOf(variables.values()); } else { - result = fetch.call(); + result = fetchFunction.call(); Logger.debug(this, LANGUAGE_VARIABLES_FOR_LANGUAGE_WITH_ID + languageId + " has been fetched from the database."); putVars(languageId, result); } @@ -327,17 +331,21 @@ public void putVars(final long languageId, final List vars){ Object perLangCache = cache.getNoThrow(LANG_VARIABLES_CACHE, group); if (perLangCache == null) { // A map of LanguageVariables is stored per language - cache.put(LANG_VARIABLES_CACHE, new ConcurrentHashMap>(), group); + cache.put(LANG_VARIABLES_CACHE, new ConcurrentHashMap>(), group); perLangCache = cache.getNoThrow(LANG_VARIABLES_CACHE, group); } @SuppressWarnings("unchecked") - final ConcurrentMap> langVarCache = (ConcurrentMap>) perLangCache; + final ConcurrentMap> langVarCache = (ConcurrentMap>) perLangCache; //Now we use the crafted (specific) key to store the list of LanguageVariables //So that if we want to invalidate only the list of LanguageVariables for a specific language it is possible - langVarCache.put(languageIdStr, vars); + langVarCache.put(languageIdStr, vars.stream().collect(Collectors.toMap(LanguageVariable::key, o -> o, mergeFunction()))); cache.put(languageIdStr, langVarCache, group); } + private static BinaryOperator mergeFunction() { + return (languageVariable, languageVariable2) -> languageVariable; + } + /** * This method is used to retrieve a list of LanguageVariables from cache * @param languageId the language id @@ -353,13 +361,74 @@ public List getVars(final long languageId){ return List.of(); } @SuppressWarnings("unchecked") - final ConcurrentMap> langVarCache = (ConcurrentMap>) perLangCache; + final ConcurrentMap> langVarCache = (ConcurrentMap>) perLangCache; //Now we use the crafted (specific) key to access the list of LanguageVariables - final List variables = langVarCache.get(languageIdStr); + final Map variables = langVarCache.get(languageIdStr); if (variables == null) { return List.of(); } - return variables; + return List.copyOf(variables.values()); } + /** + * This method is used to retrieve a LanguageVariable from cache + * @param languageId the language id + * @param key the key of the LanguageVariable + * @return a LanguageVariable + */ + public Optional getVar(final long languageId, final String key){ + final String group = getSecondaryGroup(); + final DotCacheAdministrator cache = CacheLocator.getCacheAdministrator(); + final String languageIdStr = String.valueOf(languageId); + final Object perLangCache = cache.getNoThrow(LANG_VARIABLES_CACHE, group); + if (perLangCache == null) { + return Optional.empty(); + } + @SuppressWarnings("unchecked") + final ConcurrentMap> langVarCache = (ConcurrentMap>) perLangCache; + final Map variableMap = langVarCache.get(languageIdStr); + if (variableMap == null) { + return Optional.empty(); + } + return Optional.ofNullable(variableMap.get(key)); + } + + + /** + * This method is used to retrieve a LanguageVariable from cache + * @param languageId the language id + * @param key the key + * @param fetchFunction the fetch function + * @return a LanguageVariable + * @throws DotDataException + */ + + public Optional ifPresentGetOrElseFetch(final long languageId, final String key, + final Callable> fetchFunction) throws DotDataException { + final String group = getSecondaryGroup(); + final DotCacheAdministrator cache = CacheLocator.getCacheAdministrator(); + try { + final Object perLangCache = cache.getNoThrow(LANG_VARIABLES_CACHE, group); + @SuppressWarnings("unchecked") + final ConcurrentMap> langVarCache = (ConcurrentMap>) perLangCache; + if (langVarCache == null) { + List result = fetchFunction.call(); + Logger.debug(this, LANGUAGE_VARIABLES_FOR_LANGUAGE_WITH_ID + languageId + " has been fetched from the database."); + putVars(languageId, result); + } else { + final String languageIdStr = String.valueOf(languageId); + final Map variableMap = langVarCache.get(languageIdStr); + if (variableMap == null || variableMap.get(key) == null) { + List result = fetchFunction.call(); + Logger.debug(this, LANGUAGE_VARIABLES_FOR_LANGUAGE_WITH_ID + languageId + " has been fetched from the database."); + putVars(languageId, result); + } + } + return getVar(languageId, key); + } catch (Exception e) { + throw new DotDataException(e); + } + } + + } diff --git a/dotCMS/src/main/java/com/liferay/portal/struts/MultiMessageResources.java b/dotCMS/src/main/java/com/liferay/portal/struts/MultiMessageResources.java index e33069ddf5c5..6fee64907fcf 100644 --- a/dotCMS/src/main/java/com/liferay/portal/struts/MultiMessageResources.java +++ b/dotCMS/src/main/java/com/liferay/portal/struts/MultiMessageResources.java @@ -156,7 +156,8 @@ public void setServletContext(ServletContext servletContext) { _servletContext = servletContext; } - protected void loadLocale(String localeKey) { + @Override + protected synchronized void loadLocale(String localeKey) { synchronized (locales) { if (locales.get(localeKey) != null) { @@ -166,151 +167,196 @@ protected void loadLocale(String localeKey) { locales.put(localeKey, localeKey); } - String[] names = StringUtil.split(config.replace('.', '/')); + // Load the properties files for the specified locale + final String[] names = StringUtil.split(config.replace('.', '/')); - for (int i = 0; i < names.length; i++) { - String name = names[i]; + for (String name : names) { if (localeKey.length() > 0) { name += "_" + localeKey; } name += ".properties"; - - _loadProps(name, localeKey); + internalLoadProps(name, localeKey); } - } - private void _loadProps(String name, String localeKey) { - - final LanguageAPI langAPI = APILocator.getLanguageAPI(); - if (name.contains("cms_language")) { - - if(langAPI.isLocalizationEnhancementsEnabled()){ - - final LanguageVariableAPI languageVariableAPI = APILocator.getLanguageVariableAPI(); - final long languageId = LanguageUtil.getLanguageId(localeKey, false); - final List vars = Try.of(()->languageVariableAPI.findVariables(languageId)).getOrElse(List.of()); - if (vars.isEmpty()){ + // Load the language variables if the localization enhancements are enabled + if (isLocalizationEnhancementsEnabled()) { + final LanguageVariableAPI languageVariableAPI = APILocator.getLanguageVariableAPI(); + final long languageId = LanguageUtil.getLanguageId(localeKey, false); + if (languageId > 0) { + final List variables = Try.of( + () -> languageVariableAPI.findVariables(languageId)).getOrElse(List.of()); + if (variables.isEmpty()) { return; } synchronized (messages) { - for (LanguageVariable var : vars) { - messages.put(messageKey(localeKey, var.key()), var.value()); + for (LanguageVariable variable : variables) { + messages.put(messageKey(localeKey, variable.key()), variable.value()); } } + } + } - } else{ - - List keys; - if (localeKey.split("_").length > 1) { - keys = langAPI.getLanguageKeys(localeKey.split("_")[0], localeKey.split("_")[1]); - } else { - keys = langAPI.getLanguageKeys(localeKey.split("_")[0]); - } - - if (keys.size() < 1) { - return; - } + } - synchronized (messages) { - Iterator names = keys.iterator(); + /** + * Load the properties file + * @param name the name of the properties file + * @param localeKey the locale key + */ + private void internalLoadProps(String name, String localeKey) { + //These are user provided properties, and we should only look at them if the localization enhancements are disabled + if (name.contains("cms_language") && !isLocalizationEnhancementsEnabled()) { + loadUserProvidedKeys(localeKey); + } else { + // These source base properties are not user provided, we always want to load them + loadSystemProperties(name, localeKey); + } + } - while (names.hasNext()) { - LanguageKey langkey = (LanguageKey) names.next(); - String key = langkey.getKey(); - messages.put(messageKey(localeKey, key), langkey.getValue()); - } + /** + * Load the system properties meaning all property files provided in our source base that end up living in the WEB-INF directory + * @param name the name of the properties file + * @param localeKey the locale key + */ + private void loadSystemProperties(String name, String localeKey) { + final Properties props = new Properties(); + try { + final URL url = _servletContext.getResource("/WEB-INF/" + name); + if (url != null) { + try (InputStream is = url.openStream(); BufferedReader buffy = new BufferedReader(new InputStreamReader(is))) { + parseProps(props, buffy); } } + } catch (Exception e) { + Logger.error(this, "Error loading system properties", e); + } - } else { - Properties props = new Properties(); + if (props.isEmpty()) { + return; + } - try { - URL url = null; + synchronized (messages) { + Enumeration names = props.keys(); - url = _servletContext.getResource("/WEB-INF/" + name); + while (names.hasMoreElements()) { + String key = (String) names.nextElement(); - if (url != null) { - try (InputStream is = url.openStream(); BufferedReader buffy = new BufferedReader(new InputStreamReader(is))) { - buildProps(props, buffy); - } - } - } catch (Exception e) { - Logger.error(this, e.getMessage(), e); + messages.put(messageKey(localeKey, key), props.getProperty(key)); } + } + } - if (props.size() < 1) { - return; - } + /** + * Load user provided keys from the database + * @param localeKey the locale key + * @param langAPI the language API + */ + private void loadUserProvidedKeys(String localeKey) { + final LanguageAPI langAPI = APILocator.getLanguageAPI(); + List keys; + if (localeKey.split("_").length > 1) { + keys = langAPI.getLanguageKeys(localeKey.split("_")[0], localeKey.split("_")[1]); + } else { + keys = langAPI.getLanguageKeys(localeKey.split("_")[0]); + } - synchronized (messages) { - Enumeration names = props.keys(); + if (keys.isEmpty()) { + return; + } - while (names.hasMoreElements()) { - String key = (String) names.nextElement(); + synchronized (messages) { + Iterator names = keys.iterator(); - messages.put(messageKey(localeKey, key), props.getProperty(key)); - } + while (names.hasNext()) { + LanguageKey langKey = names.next(); + String key = langKey.getKey(); + messages.put(messageKey(localeKey, key), langKey.getValue()); } } } - private void buildProps(Properties props, BufferedReader buffy) throws IOException { + /** + * Parses the properties from the given buffered reader and puts them into the given properties object. + * @param props the properties object + * @param buffy the buffered reader + * @throws IOException if an error occurs + */ + private void parseProps(Properties props, BufferedReader buffy) throws IOException { String line = null; - while ((line = buffy.readLine()) != null) { if (UtilMethods.isSet(line) && line.contains("=") && !line.startsWith("#")) { - String[] arr = line.split("=", 2); + final String[] arr = line.split("=", 2); if (arr.length > 1) { String key = arr[0].trim(); String val = arr[1].trim(); if (val.contains("\\u")) { - - if (val.contains("\\u")) { - - StringBuffer buffer = new StringBuffer(val.length()); - boolean precedingBackslash = false; - for (int i = 0; i < val.length(); i++) { - char c = val.charAt(i); - if (precedingBackslash) { - switch (c) { - case 'f': - c = '\f'; - break; - case 'n': - c = '\n'; - break; - case 'r': - c = '\r'; - break; - case 't': - c = '\t'; - break; - case 'u': - String hex = val.substring(i + 1, i + 5); - c = (char) Integer.parseInt(hex, 16); - i += 4; - } - precedingBackslash = false; - } else { - precedingBackslash = (c == '\\'); - } - if (!precedingBackslash) { - buffer.append(c); - } - } - val = buffer.toString(); - } - } - if (props.containsKey(key)) { - Logger.warn(this.getClass(), String.format( - "Duplicate resource property definition (key=was ==> is now): %s=%s ==> %s", - key, props.get(key), val)); + val = parseVal(val); } - props.put(key, val); + putOrUpdate(props, key, val); + } + } + } + } + + /** + * Parses the given value and returns the parsed value. + * @param val the value + * @return the parsed value + */ + private static String parseVal(String val) { + StringBuilder buffer = new StringBuilder(val.length()); + boolean precedingBackslash = false; + for (int i = 0; i < val.length(); i++) { + char c = val.charAt(i); + if (precedingBackslash) { + switch (c) { + case 'f': + c = '\f'; + break; + case 'n': + c = '\n'; + break; + case 'r': + c = '\r'; + break; + case 't': + c = '\t'; + break; + case 'u': + String hex = val.substring(i + 1, i + 5); + c = (char) Integer.parseInt(hex, 16); + i += 4; + break; + default: + break; } + precedingBackslash = false; + } else { + precedingBackslash = (c == '\\'); + } + if (!precedingBackslash) { + buffer.append(c); } } + val = buffer.toString(); + return val; + } + + /** + * Puts or updates a property in the given properties object. + * @param props the properties object + * @param key the key + * @param val the value + */ + private void putOrUpdate(final Properties props, final String key, final String val) { + props.compute(key, (k, existingVal) -> { + if (existingVal != null) { + Logger.warn(this.getClass(), String.format( + "Duplicate resource property definition (key=was ==> is now): %s=%s ==> %s", + key, existingVal, val)); + } + return val; + }); } public synchronized void reload() { diff --git a/dotcms-integration/src/test/java/com/dotmarketing/portlets/languagesmanager/business/LanguageAPITest.java b/dotcms-integration/src/test/java/com/dotmarketing/portlets/languagesmanager/business/LanguageAPITest.java index f42766cbb21d..aab1238cdae7 100644 --- a/dotcms-integration/src/test/java/com/dotmarketing/portlets/languagesmanager/business/LanguageAPITest.java +++ b/dotcms-integration/src/test/java/com/dotmarketing/portlets/languagesmanager/business/LanguageAPITest.java @@ -3,6 +3,7 @@ import static com.dotcms.datagen.TestDataUtils.getNewsLikeContentType; import static com.dotcms.integrationtestutil.content.ContentUtils.createTestKeyValueContent; import static com.dotcms.integrationtestutil.content.ContentUtils.deleteContentlets; +import static com.dotmarketing.portlets.languagesmanager.business.LanguageAPI.LOCALIZATION_ENHANCEMENTS_ENABLED; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -11,6 +12,7 @@ import com.dotcms.contenttype.model.type.ContentType; import com.dotcms.datagen.ContentTypeDataGen; import com.dotcms.datagen.ContentletDataGen; +import com.dotcms.datagen.LanguageVariableDataGen; import com.dotcms.languagevariable.business.LanguageVariableAPI; import com.dotcms.util.IntegrationTestInitService; import com.dotmarketing.business.APILocator; @@ -21,7 +23,6 @@ import com.dotmarketing.portlets.contentlet.business.ContentletAPI; import com.dotmarketing.portlets.contentlet.model.Contentlet; import com.dotmarketing.portlets.languagesmanager.model.Language; -import com.dotmarketing.util.Config; import com.dotmarketing.util.UUIDGenerator; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -182,46 +183,91 @@ public void getStringKey_returnKey(){ */ @Test public void getStringsAsMap_returnMap() throws Exception { - final String uniq = UUIDGenerator.shorty(); - final String BAD_KEY = "KEY-DOES-NOT-EXIST"; - final String CONTENTLET_KEY = "key-exists-as-contentlet" + uniq; - final String PROPERTYFILE_KEY = "key-exists-in-properties" + uniq; - final String SYSTEM_PROPERTYFILE_KEY = "contenttypes.action.cancel"; - final LanguageAPIImpl languageAPi = new LanguageAPIImpl(); - - final Language language = new LanguageDataGen().nextPersisted(); - final LanguageAPIImpl lapi = Mockito.spy(languageAPi); - Mockito.doReturn(APILocator.systemUser()).when(lapi).getUser(); - final ContentType langKeyType = APILocator.getContentTypeAPI(APILocator.systemUser()).find("Languagevariable"); - - //Create a contentlet with the key - Contentlet con = new ContentletDataGen(langKeyType.id()) - .setProperty("key", CONTENTLET_KEY) - .setProperty("value", CONTENTLET_KEY + "works") - .languageId(language.getId()) - .nextPersisted(); - APILocator.getContentletAPI().publish(con, APILocator.systemUser(), false); - - - // Add Lang Variable to local properties. This shouldn't work if the localization enhancement is enabled. Cause this is legacy deprecated code - lapi.saveLanguageKeys(language, ImmutableMap.of(PROPERTYFILE_KEY, PROPERTYFILE_KEY + "works"), new HashMap<>(), ImmutableSet.of()); - - final List keys = ImmutableList.of(BAD_KEY, CONTENTLET_KEY,PROPERTYFILE_KEY,SYSTEM_PROPERTYFILE_KEY ); - final Locale locale = language.asLocale(); - - final Map translatedMap = lapi.getStringsAsMap(locale, keys); - - // If key does not exist, we just return the key as the value - assertEquals(translatedMap.get(BAD_KEY), BAD_KEY); - - // We should check content based keys - languagevariables - before .properties files - assertEquals(CONTENTLET_KEY+"works", translatedMap.get(CONTENTLET_KEY)); - - // then we should check properties files based on the locale - assertEquals(PROPERTYFILE_KEY+"works",translatedMap.get(PROPERTYFILE_KEY)); - - - assertEquals(LanguageUtil.get( new Locale("en", "us"), SYSTEM_PROPERTYFILE_KEY ), translatedMap.get(SYSTEM_PROPERTYFILE_KEY) ); + System.setProperty(LOCALIZATION_ENHANCEMENTS_ENABLED, "false"); + try { + final String uniq = UUIDGenerator.shorty(); + final String BAD_KEY = "KEY-DOES-NOT-EXIST"; + final String CONTENTLET_KEY = "key-exists-as-contentlet" + uniq; + final String PROPERTYFILE_KEY = "key-exists-in-properties" + uniq; + final String SYSTEM_PROPERTYFILE_KEY = "contenttypes.action.cancel"; + final LanguageAPIImpl languageAPi = new LanguageAPIImpl(); + + final Language language = new LanguageDataGen().nextPersisted(); + final LanguageAPIImpl lapi = Mockito.spy(languageAPi); + Mockito.doReturn(APILocator.systemUser()).when(lapi).getUser(); + final ContentType langKeyType = APILocator.getContentTypeAPI(APILocator.systemUser()) + .find("Languagevariable"); + + //Create a contentlet with the key + Contentlet con = new ContentletDataGen(langKeyType.id()) + .setProperty("key", CONTENTLET_KEY) + .setProperty("value", CONTENTLET_KEY + "works") + .languageId(language.getId()) + .nextPersisted(); + //in new version we won't need to publish the langVar to see it + APILocator.getContentletAPI().publish(con, APILocator.systemUser(), false); + + // Add Lang Variable to local properties. This shouldn't work if the localization enhancement is enabled. Cause this is legacy deprecated code + lapi.saveLanguageKeys(language, + ImmutableMap.of(PROPERTYFILE_KEY, PROPERTYFILE_KEY + "works"), new HashMap<>(), + ImmutableSet.of()); + + final List keys = ImmutableList.of(BAD_KEY, CONTENTLET_KEY, PROPERTYFILE_KEY, + SYSTEM_PROPERTYFILE_KEY); + final Locale locale = language.asLocale(); + + final Map translatedMap = lapi.getStringsAsMap(locale, keys); + + // If key does not exist, we just return the key as the value + assertEquals(translatedMap.get(BAD_KEY), BAD_KEY); + + // We should check content based keys - languagevariables - before .properties files + assertEquals(CONTENTLET_KEY + "works", translatedMap.get(CONTENTLET_KEY)); + + // then we should check properties files based on the locale + assertEquals(PROPERTYFILE_KEY + "works", translatedMap.get(PROPERTYFILE_KEY)); + + assertEquals(LanguageUtil.get(new Locale("en", "us"), SYSTEM_PROPERTYFILE_KEY), + translatedMap.get(SYSTEM_PROPERTYFILE_KEY)); + } finally { + System.clearProperty(LOCALIZATION_ENHANCEMENTS_ENABLED); + } + } + + @Test + public void getStringsAsEnhancementsEnabled() throws Exception { + System.setProperty(LOCALIZATION_ENHANCEMENTS_ENABLED, "true"); + try { + + final Language lang = new com.dotcms.datagen.LanguageDataGen().nextPersisted(); + final String key = "com.dotcoms.cat"; + final String translation = "kato"; + final LanguageAPI languageAPI = APILocator.getLanguageAPI(); + + final Map beforeMap = languageAPI.getStringsAsMap(lang.asLocale(), List.of(key)); + // If key does not exist, we just return the key as the value + Assert.assertEquals(key, beforeMap.get(key)); + final String stringKeyBefore = languageAPI.getStringKey(lang, key); + Assert.assertEquals(key, stringKeyBefore); + + //Now let's create a language variable with the key and translation + //We're NOT going to use the nextPersistedAndPublish here since we want to make sure that + //even if the contentlet is not published, the language variable still makes it into cache + new LanguageVariableDataGen().languageId(lang.getId()).key(key).value(translation).nextPersisted(); + + final Map afterMap = languageAPI.getStringsAsMap(lang.asLocale(), List.of(key)); + // This time around we should get the translated value. + Assert.assertEquals(translation, afterMap.get(key)); + + final String stringKeyAfter = languageAPI.getStringKey(lang, key); + // This time around we should get the translated value. + Assert.assertEquals(translation, stringKeyAfter); + + + } finally { + System.clearProperty(LOCALIZATION_ENHANCEMENTS_ENABLED); + } + } @DataProvider