View Javadoc
1   /**
2    * This file Copyright (c) 2013-2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.i18nsystem;
35  
36  import static java.util.stream.Collectors.toList;
37  
38  import java.lang.reflect.AnnotatedElement;
39  import java.lang.reflect.Field;
40  import java.lang.reflect.InvocationTargetException;
41  import java.lang.reflect.Method;
42  import java.util.ArrayList;
43  import java.util.Arrays;
44  import java.util.Iterator;
45  import java.util.List;
46  import java.util.Optional;
47  import java.util.Set;
48  
49  import org.apache.commons.lang3.StringUtils;
50  import org.reflections.ReflectionUtils;
51  
52  /**
53   * Abstract implementation of {@link I18nKeyGenerator} which provides a number of helper methods to generate keys.
54   * By casting objects to {@link I18nParentable} (see {@link #getParentViaCast(Object)}), implementations are
55   * able to generate context-dependent keys for any given object.
56   *
57   * @param <T> the type of Object this {@link I18nKeyGenerator} generates keys for.
58   */
59  public abstract class AbstractI18nKeyGenerator<T> implements I18nKeyGenerator<T> {
60  
61      protected static final String FIELDS = "fields";
62      private static final String LABEL = "label";
63  
64      /**
65       * Default implementation for {@link I18nKeyGenerator#keysFor(String, Object, java.lang.reflect.AnnotatedElement)}, which sets
66       * the undecoratedResult value if it is not null, and delegates to {@link #keysFor(java.util.List, Object, java.lang.reflect.AnnotatedElement)}.
67       */
68      @Override
69      public String[] keysFor(String undecoratedResult, T object, AnnotatedElement el) {
70          final List<String> keys = new ArrayList<>();
71          keysFor(keys, object, el);
72          if (undecoratedResult != null) {
73              keys.add(0, undecoratedResult);
74          }
75          return keys.toArray(new String[keys.size()]);
76      }
77  
78      /**
79       * @deprecated since 5.4.5. Will be removed in a future version.
80       */
81      @Deprecated
82      @Override
83      public String messageBundleNameFor(T object) {
84          return resolveMessageBundleNameUpwards(object);
85      }
86  
87      /**
88       * Protected only to be able to test - ancestors should NOT override this method, unless they have <b>very</b> good reason to do this.
89       * @deprecated since 5.4.5. Will be removed in a future version.
90       */
91      @Deprecated
92      protected String resolveMessageBundleNameUpwards(Object object) {
93          String bundleName = null;
94  
95          try {
96              Method getI18nBasename = object.getClass().getMethod("getI18nBasename");
97              bundleName = (String) getI18nBasename.invoke(object);
98          } catch (NoSuchMethodException e) {
99              // expected - not every parent has to have such method
100         } catch (SecurityException | IllegalArgumentException | IllegalAccessException | InvocationTargetException e) {
101             throw new RuntimeException(e);
102         }
103 
104         if (bundleName == null) {
105             Object parent = getParentViaCast(object);
106             if (parent != null) {
107                 bundleName = resolveMessageBundleNameUpwards(parent);
108             }
109         }
110 
111         return bundleName;
112     }
113 
114     protected abstract void keysFor(List<String> keys, T object, AnnotatedElement el);
115 
116     /**
117      * Returns the name of the field or method passed to {@link I18nKeyGenerator#keysFor(String, T, java.lang.reflect.AnnotatedElement)}.
118      */
119     protected String fieldOrGetterName(AnnotatedElement el) {
120         if (el instanceof Field) {
121             return ((Field) el).getName();
122         } else if (el instanceof Method) {
123             return getterToField((Method) el);
124         } else {
125             throw new IllegalArgumentException("Can't derive i18n key suffix from " + el);
126         }
127     }
128 
129     /**
130      * Returns the parent object that was set via the {@link I18nParentable} interface
131      * when the object was decorated by {@link I18nizer}.
132      */
133     protected <P, C> P getParentViaCast(C obj) {
134         if (!I18nParentable.class.isInstance(obj)) {
135             throw new IllegalStateException("Can't reach parent of " + obj);
136         }
137         final I18nParentable<P> cast = I18nParentable.class.cast(obj);
138         return cast.getI18nContextParent();
139     }
140 
141     /**
142      * Returns the list of parent objects as set via the {@link I18nParentable} interface
143      * when the object was decorated by {@link I18nizer}.
144      *
145      * @see #getParentViaCast(Object)
146      */
147     protected <C> List<Object> getAncestors(C obj) {
148         final ArrayList<Object> ancestors = new ArrayList<>();
149         Object p = getParentViaCast(obj);
150         while (p != null) {
151             ancestors.add(p);
152             p = getParentViaCast(p);
153         }
154         return ancestors;
155     }
156 
157     /**
158      * Returns the root object that of the {@link I18nParentable} hierarchy.
159      *
160      * @see #getParentViaCast(Object)
161      */
162     protected <C> Object getRoot(C obj) {
163         Object root = null;
164         Object p = getParentViaCast(obj);
165         while (p != null) {
166             root = p;
167             p = getParentViaCast(p);
168         }
169         return root;
170     }
171 
172     /**
173      * Returns the {@link I18nKeyGenerator}s corresponding to the parent objects of the given object.
174      *
175      * @see #getAncestors(Object)
176      */
177     protected <C> List<I18nKeyGenerator> getAncestorKeyGenerators(C obj) {
178         return getAncestors(obj).stream()
179                 .map(this::getKeyGenerator)
180                 .collect(toList());
181     }
182 
183     /**
184      * Returns the {@link I18nKeyGenerator}s corresponding to the root object of the given object.
185      *
186      * @see #getRoot(Object)
187      */
188     protected <C> I18nKeyGenerator getRootKeyGenerator(C obj) {
189         return getKeyGenerator(getRoot(obj));
190     }
191 
192     /**
193      * Returns the {@link I18nKeyGenerator}s corresponding to the given object.
194      */
195     protected <P> I18nKeyGenerator<P> getKeyGenerator(P obj) {
196         return I18nKeyGeneratorFactory.newKeyGeneratorFor(obj);
197     }
198 
199     /**
200      * Adds a key to the given list; if the last part is "label", adds another key without it.
201      *
202      * @see #keyify(String...)
203      */
204     protected void addKey(List<String> keys, boolean stripLabelSuffix, String... parts) {
205         this.addKey(keys, keyify(parts));
206         if (stripLabelSuffix && LABEL.equals(parts[parts.length - 1])) { //this unnecessary increases count of keys in memory, kept for compatibility
207             this.addKey(keys, keyify(Arrays.copyOfRange(parts, 0, parts.length - 1)));
208         }
209     }
210 
211     protected void addKey(List<String> keys, String... parts) {
212         this.addKey(keys, true, parts);
213     }
214 
215     private void addKey(List<String> keys, String key) {
216         if (!keys.contains(key)) {
217             keys.add(key);
218         }
219     }
220 
221     /**
222      * Makes up a key from the given parts.
223      */
224     protected String keyify(String... parts) {
225         return StringUtils.join(parts, '.');
226     }
227 
228     /**
229      * Replaces the characters like ':' or '/' with dots to create i18n key.
230      */
231     protected String keyify(String id) {
232         return StringUtils.replaceChars(id, ":/", "..");
233     }
234 
235     /**
236      * Will try to call either <code>getId()</code> or <code>getName()</code> on a root object of unknown type. The result is normalized by replacing colon [:] and slash [/] characters with dots [.].
237      * <p>
238      * Should the object not have the above methods, <code>null</code> is returned. Should the methods both return <code>null</code> or an empty string, <code>null</code> is returned.
239      */
240     protected String getIdOrNameForUnknownRoot(Object obj, boolean keyify) {
241         // passed object might already be root
242         final Object root = getParentViaCast(obj) != null ? getRoot(obj) : obj;
243         final Set<Method> methods = ReflectionUtils.getMethods(root.getClass(),
244                 method -> "getId".equals(method.getName()) || "getName".equals(method.getName()));
245 
246         try {
247             String idOrName = null;
248             Iterator<Method> iterator = methods.iterator();
249             // first method returning a non-null result is taken
250             while (iterator.hasNext()) {
251                 idOrName = (String) iterator.next().invoke(root);
252                 if (StringUtils.isNotBlank(idOrName)) {
253                     return keyify ? keyify(idOrName) : idOrName;
254                 }
255             }
256             return null;
257 
258         } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) {
259             throw new RuntimeException(e);
260         }
261     }
262 
263     protected String getIdOrNameForUnknownRoot(Object obj) {
264         return getIdOrNameForUnknownRoot(obj, true);
265     }
266 
267     protected String getModuleName(String id) {
268         return StringUtils.contains(id, ":") ? StringUtils.substringBefore(id, ":") : StringUtils.EMPTY;
269     }
270 
271     protected String getIdWithoutModuleName(String id) {
272         return keyify(StringUtils.contains(id, ":") ? StringUtils.substringAfter(id, ":") : id);
273     }
274 
275     protected String replaceColons(String e) {
276         return Optional.ofNullable(StringUtils.replaceChars(e, ':', '-')).orElse(StringUtils.EMPTY);
277     }
278 
279     /**
280      * Returns the property name corresponding to a given "getter" method.
281      */
282     private String getterToField(Method method) {
283         final String methodName = method.getName();
284         if (methodName.startsWith("get")) {
285             return methodName.substring(3, 4).toLowerCase() + methodName.substring(4);
286         } else {
287             throw new IllegalArgumentException(method + " is not a getter method");
288         }
289     }
290 
291 }