From 19c269edebdab8c5cbae746f5e97e067cc8407ce Mon Sep 17 00:00:00 2001 From: Tobias Soloschenko Date: Mon, 8 Feb 2016 17:15:49 +0100 Subject: [PATCH] WICKET-5847 LocalizationSupport Implementation --- .../wicket/DefaultLocalizationSupport.java | 41 ++++++++ .../apache/wicket/ILocalizationSupport.java | 59 ++++++++++++ .../java/org/apache/wicket/Localizer.java | 96 ++++++++++++++++--- .../wicket/NestedKeyLocalizationSupport.java | 57 +++++++++++ .../apache/wicket/model/ResourceModel.java | 4 +- .../wicket/model/StringResourceModel.java | 4 +- .../wicket/settings/ResourceSettings.java | 46 +++++---- .../NestedKeyLocalizingSupportTest.java | 84 ++++++++++++++++ .../resource/DummyApplication.properties | 5 + 9 files changed, 361 insertions(+), 35 deletions(-) create mode 100644 wicket-core/src/main/java/org/apache/wicket/DefaultLocalizationSupport.java create mode 100644 wicket-core/src/main/java/org/apache/wicket/ILocalizationSupport.java create mode 100644 wicket-core/src/main/java/org/apache/wicket/NestedKeyLocalizationSupport.java create mode 100644 wicket-core/src/test/java/org/apache/wicket/NestedKeyLocalizingSupportTest.java diff --git a/wicket-core/src/main/java/org/apache/wicket/DefaultLocalizationSupport.java b/wicket-core/src/main/java/org/apache/wicket/DefaultLocalizationSupport.java new file mode 100644 index 00000000000..6cfedab76f2 --- /dev/null +++ b/wicket-core/src/main/java/org/apache/wicket/DefaultLocalizationSupport.java @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket; + +import java.util.Locale; +import java.util.MissingResourceException; + +import org.apache.wicket.model.IModel; + +/** + * A default localization support implementation delegates to the localizer itself to resolve the + * translated string + * + * @author Tobias Soloschenko + * + */ +public class DefaultLocalizationSupport implements ILocalizationSupport +{ + + @Override + public String resolveString(Localizer localizer, String key, Component component, + IModel model, Locale locale, String style, IModel defaultValue, + String previousTranslatedString) throws MissingResourceException + { + return localizer.resolveString(key, component, model, locale, style, defaultValue); + } +} diff --git a/wicket-core/src/main/java/org/apache/wicket/ILocalizationSupport.java b/wicket-core/src/main/java/org/apache/wicket/ILocalizationSupport.java new file mode 100644 index 00000000000..91c97af21ed --- /dev/null +++ b/wicket-core/src/main/java/org/apache/wicket/ILocalizationSupport.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket; + +import java.util.Locale; +import java.util.MissingResourceException; + +import org.apache.wicket.model.IModel; + +/** + * The localization support is used to translate sentence within the localizer + * + * @author Tobias Soloschenko + * + */ +public interface ILocalizationSupport +{ + /** + * Resolves a translated string + * + * @param localizer + * the localizer + * @param key + * the key + * @param component + * the component the localization belongs to + * @param model + * the model to be used + * @param locale + * the locale to be used + * @param style + * the style to be used + * @param defaultValue + * a default value to be used + * @param previousTranslatedString + * A string that has been translated by a previous localization support + * @return the translated string + * @throws MissingResourceException + * if the resource could'nt be found + */ + public String resolveString(Localizer localizer, final String key, final Component component, + final IModel model, final Locale locale, final String style, + final IModel defaultValue, String previousTranslatedString) + throws MissingResourceException; +} diff --git a/wicket-core/src/main/java/org/apache/wicket/Localizer.java b/wicket-core/src/main/java/org/apache/wicket/Localizer.java index 17c86ef965a..b2438f422f2 100644 --- a/wicket-core/src/main/java/org/apache/wicket/Localizer.java +++ b/wicket-core/src/main/java/org/apache/wicket/Localizer.java @@ -16,6 +16,7 @@ */ package org.apache.wicket; +import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Locale; @@ -49,9 +50,11 @@ * @see org.apache.wicket.settings.ResourceSettings#getLocalizer() * @see org.apache.wicket.resource.loader.IStringResourceLoader * @see org.apache.wicket.settings.ResourceSettings#getStringResourceLoaders() + * @see org.apache.wicket.ILocalizationSupport * * @author Chris Turner * @author Juergen Donnerstag + * @author Tobias Soloschenko */ public class Localizer { @@ -66,6 +69,9 @@ public class Localizer /** Database that maps class names to an integer id. */ private final ClassMetaDatabase metaDatabase = new ClassMetaDatabase(); + /** localization supports to localize strings */ + private List localizationSupports = new ArrayList<>(); + /** * @return Same as Application.get().getResourceSettings().getLocalizer() */ @@ -80,6 +86,7 @@ public static Localizer get() */ public Localizer() { + localizationSupports.add(new DefaultLocalizationSupport()); } /** @@ -195,10 +202,11 @@ public String getString(final String key, final Component component, final IMode */ public String getString(final String key, final Component component, final IModel model, final Locale locale, final String style, final String defaultValue) - throws MissingResourceException + throws MissingResourceException { IModel defaultValueModel = defaultValue != null ? Model.of(defaultValue) : null; - return getString(key, component, model, locale, style, defaultValueModel); + return resolveStringWithLocalizationSupport(key, component, model, locale, style, + defaultValueModel); } /** @@ -223,9 +231,40 @@ public String getString(final String key, final Component component, final IMode * @throws MissingResourceException * If resource not found and configuration dictates that exception should be thrown */ - public String getString(final String key, final Component component, final IModel model, - final Locale locale, final String style, final IModel defaultValue) - throws MissingResourceException + public String resolveStringWithLocalizationSupport(final String key, final Component component, + final IModel model, final Locale locale, final String style, + final IModel defaultValue) throws MissingResourceException + { + String resolvedString = ""; + for (ILocalizationSupport localizationSupport : localizationSupports) + { + resolvedString = localizationSupport.resolveString(this, key, component, model, locale, + style, defaultValue, resolvedString); + } + return resolvedString; + } + + /** + * Resolves the actual translated String + * + * @param key + * The key to obtain the resource for + * @param component + * The component to get the resource for (optional) + * @param model + * The model to use for substitutions in the strings (optional) + * @param locale + * If != null, it'll supersede the component's locale + * @param style + * If != null, it'll supersede the component's style + * @param defaultValue + * The default value (optional) + * @return The string resource + * @throws MissingResourceException + * If resource not found and configuration dictates that exception should be thrown + */ + public String resolveString(final String key, final Component component, final IModel model, + final Locale locale, final String style, final IModel defaultValue) { final ResourceSettings resourceSettings = Application.get().getResourceSettings(); @@ -265,8 +304,8 @@ else if (defaultValue != null && resourceSettings.getUseDefaultOnMissingResource } message.append(". Locale: ").append(locale).append(", style: ").append(style); - throw new MissingResourceException(message.toString(), (component != null - ? component.getClass().getName() : ""), key); + throw new MissingResourceException(message.toString(), + (component != null ? component.getClass().getName() : ""), key); } return "[Warning: Property for '" + key + "' not found]"; @@ -331,10 +370,11 @@ public String getStringIgnoreSettings(final String key, final Component componen if (!addedToPage && log.isWarnEnabled()) { log.warn( - "Tried to retrieve a localized string for a component that has not yet been added to the page. " - + "This can sometimes lead to an invalid or no localized resource returned. " - + "Make sure you are not calling Component#getString() inside your Component's constructor. " - + "Offending component: {}", component); + "Tried to retrieve a localized string for a component that has not yet been added to the page. " + + "This can sometimes lead to an invalid or no localized resource returned. " + + "Make sure you are not calling Component#getString() inside your Component's constructor. " + + "Offending component: {}", + component); } } @@ -546,11 +586,11 @@ protected String getCacheKey(final String key, final Component component, final } } -/** + /** * Helper method to handle property variable substitution in strings. * * @param component - * The component requesting a model value or {@code null] + * The component requesting a model value or {@code null} * @param string * The string to substitute into * @param model @@ -654,4 +694,34 @@ public long id(Class clazz) return id; } } + + /** + * Adds a new localization support. The order is important, so add them wisely!
+ *
+ * Order Example:
+ *
+ * {@link DefaultLocalizationSupport} > MyDataBaseLocalizationSupport > + * {@link NestedKeyLocalizationSupport}
+ *
+ * This chain will cause the {@link NestedKeyLocalizationSupport} to invoke all previous + * localization supports to resolve the keys first. + * + * @param localizationSupport + * a localization support to be added + */ + public void addLocalizationSupport(ILocalizationSupport localizationSupport) + { + localizationSupports.add(localizationSupport); + } + + /** + * Removes the given localization support + * + * @param localizationSupport + * the localization support to be removed + */ + public void removeLocalizationSupport(ILocalizationSupport localizationSupport) + { + localizationSupports.remove(localizationSupport); + } } diff --git a/wicket-core/src/main/java/org/apache/wicket/NestedKeyLocalizationSupport.java b/wicket-core/src/main/java/org/apache/wicket/NestedKeyLocalizationSupport.java new file mode 100644 index 00000000000..530c4139b95 --- /dev/null +++ b/wicket-core/src/main/java/org/apache/wicket/NestedKeyLocalizationSupport.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket; + +import java.util.Locale; +import java.util.MissingResourceException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.wicket.model.IModel; +import org.apache.wicket.model.Model; + +/** + * Finds nested keys and replaces them with the default localizer it uses the pattern {{key}}. Also + * nested keys can be found. + * + * @author Tobias Soloschenko + * + */ +public class NestedKeyLocalizationSupport implements ILocalizationSupport +{ + + private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\{\\{([^ ]*?)\\}\\}"); + + @Override + public String resolveString(Localizer localizer, String key, Component component, IModel model, + Locale locale, String style, IModel defaultValue, String previousTranslatedString) + throws MissingResourceException + { + StringBuffer output = new StringBuffer(); + Matcher matcher = PLACEHOLDER_PATTERN.matcher(previousTranslatedString); + // Search for other nested keys to replace + while (matcher.find()) + { + String replacedPlaceHolder = localizer.resolveStringWithLocalizationSupport(matcher.group(1), + component, model, locale, style, (Model)null); + matcher.appendReplacement(output, replacedPlaceHolder); + } + matcher.appendTail(output); + return output.toString(); + } + +} diff --git a/wicket-core/src/main/java/org/apache/wicket/model/ResourceModel.java b/wicket-core/src/main/java/org/apache/wicket/model/ResourceModel.java index 85ec0db9cb9..dc47fd3d04e 100644 --- a/wicket-core/src/main/java/org/apache/wicket/model/ResourceModel.java +++ b/wicket-core/src/main/java/org/apache/wicket/model/ResourceModel.java @@ -81,7 +81,7 @@ public String getObject() return Application.get() .getResourceSettings() .getLocalizer() - .getString(resourceKey, null, null, null, null, defaultValue); + .resolveStringWithLocalizationSupport(resourceKey, null, null, null, null, defaultValue); } /** @@ -129,7 +129,7 @@ protected String load() return Application.get() .getResourceSettings() .getLocalizer() - .getString(resourceKey, component, null, null, null, defaultValue); + .resolveStringWithLocalizationSupport(resourceKey, component, null, null, null, defaultValue); } @Override diff --git a/wicket-core/src/main/java/org/apache/wicket/model/StringResourceModel.java b/wicket-core/src/main/java/org/apache/wicket/model/StringResourceModel.java index 04935aaf86a..321debc520b 100644 --- a/wicket-core/src/main/java/org/apache/wicket/model/StringResourceModel.java +++ b/wicket-core/src/main/java/org/apache/wicket/model/StringResourceModel.java @@ -436,13 +436,13 @@ protected String getString(final Component component) { // Get the string resource, doing any property substitutions as part // of the get operation - value = localizer.getString(getResourceKey(), component, model, null, null, defaultValue); + value = localizer.resolveStringWithLocalizationSupport(getResourceKey(), component, model, null, null, defaultValue); } else { // Get the string resource, doing not any property substitutions // that has to be done later after MessageFormat - value = localizer.getString(getResourceKey(), component, null, null, null, defaultValue); + value = localizer.resolveStringWithLocalizationSupport(getResourceKey(), component, null, null, null, defaultValue); if (value != null) { // Build the real parameters diff --git a/wicket-core/src/main/java/org/apache/wicket/settings/ResourceSettings.java b/wicket-core/src/main/java/org/apache/wicket/settings/ResourceSettings.java index be2b272b508..08ec6f3f76e 100644 --- a/wicket-core/src/main/java/org/apache/wicket/settings/ResourceSettings.java +++ b/wicket-core/src/main/java/org/apache/wicket/settings/ResourceSettings.java @@ -221,7 +221,8 @@ public ResourceSettings(final Application application) stringResourceLoaders.add(new PackageStringResourceLoader()); stringResourceLoaders.add(new ClassStringResourceLoader(application.getClass())); stringResourceLoaders.add(new ValidatorStringResourceLoader()); - stringResourceLoaders.add(new InitializerStringResourceLoader(application.getInitializers())); + stringResourceLoaders + .add(new InitializerStringResourceLoader(application.getInitializers())); } /** @@ -285,9 +286,10 @@ public IResourceFactory getResourceFactory(final String name) /** * Gets the resource finders to use when searching for resources. By default, a finder that - * looks in the classpath root is configured. {@link org.apache.wicket.protocol.http.WebApplication} adds the classpath - * directory META-INF/resources. To configure additional search paths or filesystem paths, add - * to this list. + * looks in the classpath root is configured. + * {@link org.apache.wicket.protocol.http.WebApplication} adds the classpath directory + * META-INF/resources. To configure additional search paths or filesystem paths, add to this + * list. * * @return Returns the resourceFinders. */ @@ -378,6 +380,11 @@ public List getStringResourceLoaders() return stringResourceLoaders; } + /** + * Gets the exception on missing resource + * + * @return the exception on messing resource + */ public boolean getThrowExceptionOnMissingResource() { return throwExceptionOnMissingResource; @@ -423,7 +430,8 @@ public ResourceSettings setPackageResourceGuard(IPackageResourceGuard packageRes * @param factory * @return {@code this} object for chaining */ - public ResourceSettings setPropertiesFactory(org.apache.wicket.resource.IPropertiesFactory factory) + public ResourceSettings setPropertiesFactory( + org.apache.wicket.resource.IPropertiesFactory factory) { propertiesFactory = factory; return this; @@ -466,12 +474,12 @@ public ResourceSettings setResourcePollFrequency(final Duration resourcePollFreq } /** - * /** - * Sets the resource stream locator for this application + * /** Sets the resource stream locator for this application * * Consider wrapping resourceStreamLocator in {@link CachingResourceStreamLocator}. * This way the locator will not be asked more than once for {@link IResourceStream}s which do * not exist. + * * @param resourceStreamLocator * new resource stream locator * @@ -488,7 +496,8 @@ public ResourceSettings setResourceStreamLocator(IResourceStreamLocator resource * @param throwExceptionOnMissingResource * @return {@code this} object for chaining */ - public ResourceSettings setThrowExceptionOnMissingResource(final boolean throwExceptionOnMissingResource) + public ResourceSettings setThrowExceptionOnMissingResource( + final boolean throwExceptionOnMissingResource) { this.throwExceptionOnMissingResource = throwExceptionOnMissingResource; return this; @@ -499,7 +508,8 @@ public ResourceSettings setThrowExceptionOnMissingResource(final boolean throwEx * Whether to use a default value (if available) when a missing resource is requested * @return {@code this} object for chaining */ - public ResourceSettings setUseDefaultOnMissingResource(final boolean useDefaultOnMissingResource) + public ResourceSettings setUseDefaultOnMissingResource( + final boolean useDefaultOnMissingResource) { this.useDefaultOnMissingResource = useDefaultOnMissingResource; return this; @@ -558,8 +568,7 @@ public IJavaScriptCompressor getJavaScriptCompressor() * * @param compressor * The implementation to be used - * @return The old value - * @return {@code this} object for chaining + * @return {@code IJavaScriptCompressor} object for chaining */ public IJavaScriptCompressor setJavaScriptCompressor(IJavaScriptCompressor compressor) { @@ -588,8 +597,7 @@ public ICssCompressor getCssCompressor() * * @param compressor * The implementation to be used - * @return The old value - * @return {@code this} object for chaining + * @return {@code ICssCompressor} object for chaining */ public ICssCompressor setCssCompressor(ICssCompressor compressor) { @@ -723,17 +731,19 @@ public Comparator getHeaderItemComparator() } /** - * Sets the comparator used by the {@linkplain org.apache.wicket.markup.head.ResourceAggregator resource aggregator} for - * sorting header items. It should be noted that sorting header items may break resource - * dependencies. This comparator should therefore at least respect dependencies declared by - * resource references. By default, items are sorted using the {@link PriorityFirstComparator}. + * Sets the comparator used by the {@linkplain org.apache.wicket.markup.head.ResourceAggregator + * resource aggregator} for sorting header items. It should be noted that sorting header items + * may break resource dependencies. This comparator should therefore at least respect + * dependencies declared by resource references. By default, items are sorted using the + * {@link PriorityFirstComparator}. * * @param headerItemComparator * The comparator used to sort header items, when null, header items will not be * sorted. * @return {@code this} object for chaining */ - public ResourceSettings setHeaderItemComparator(Comparator headerItemComparator) + public ResourceSettings setHeaderItemComparator( + Comparator headerItemComparator) { this.headerItemComparator = headerItemComparator; return this; diff --git a/wicket-core/src/test/java/org/apache/wicket/NestedKeyLocalizingSupportTest.java b/wicket-core/src/test/java/org/apache/wicket/NestedKeyLocalizingSupportTest.java new file mode 100644 index 00000000000..5d8ca34fc52 --- /dev/null +++ b/wicket-core/src/test/java/org/apache/wicket/NestedKeyLocalizingSupportTest.java @@ -0,0 +1,84 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wicket; + +import org.apache.wicket.protocol.http.WebApplication; +import org.apache.wicket.resource.DummyApplication; +import org.apache.wicket.util.tester.WicketTestCase; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; + +/** + * Test for nested keys + * + * @author Tobias Soloschenko + * + */ +public class NestedKeyLocalizingSupportTest extends WicketTestCase +{ + + private Localizer localizer; + + @Override + protected WebApplication newApplication() + { + DummyApplication dummyApplication = new DummyApplication(){ + @Override + protected void init() + { + super.init(); + localizer = this.getResourceSettings().getLocalizer(); + localizer.addLocalizationSupport(new NestedKeyLocalizationSupport()); + } + }; + return dummyApplication; + } + + /** + * Clears up the context + * + * @throws Exception + */ + @After + public void tearDown() throws Exception + { + tester.destroy(); + } + + /** + * Tests a nested key + */ + @Test + public void testGetStringWithNestedKey() + { + Assert.assertEquals("Expected string should be returned", + "This is a test with a nested string", + localizer.getString("test.nested.string", null, null, "DEFAULT")); + } + + /** + * Tests a recursive nested key + */ + @Test + public void testGetStringWithRecursiveNestedKey() + { + Assert.assertEquals("Expected string should be returned", + "Testing multi level nesting: This is a test with a nested string", + localizer.getString("test.nested.nested.string", null, null, "DEFAULT")); + } +} diff --git a/wicket-core/src/test/java/org/apache/wicket/resource/DummyApplication.properties b/wicket-core/src/test/java/org/apache/wicket/resource/DummyApplication.properties index 30dbe86aca3..0ed52f9e161 100644 --- a/wicket-core/src/test/java/org/apache/wicket/resource/DummyApplication.properties +++ b/wicket-core/src/test/java/org/apache/wicket/resource/DummyApplication.properties @@ -18,3 +18,8 @@ # test.string=This is a test test.substitute=${user} gives ${rating} stars + +test.nested.string=This is a test with a {{test.other}} string +test.other=nested + +test.nested.nested.string=Testing multi level nesting: {{test.nested.string}} \ No newline at end of file