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 java.lang.reflect.AnnotatedElement;
37  import java.lang.reflect.Field;
38  import java.lang.reflect.InvocationTargetException;
39  import java.lang.reflect.Method;
40  import java.util.ArrayList;
41  import java.util.Arrays;
42  import java.util.Iterator;
43  import java.util.List;
44  import java.util.Set;
45  
46  import org.apache.commons.lang3.StringUtils;
47  import org.reflections.ReflectionUtils;
48  
49  import com.google.common.base.Function;
50  import com.google.common.base.Predicate;
51  import com.google.common.collect.Lists;
52  
53  /**
54   * Abstract implementation of {@link I18nKeyGenerator} which provides a number of helper methods to generate keys.
55   * By casting objects to {@link I18nParentable} (see {@link #getParentViaCast(Object)}), implementations are
56   * able to generate context-dependent keys for any given object.
57   *
58   * @param <T> the type of Object this {@link I18nKeyGenerator} generates keys for.
59   */
60  public abstract class AbstractI18nKeyGenerator<T> implements I18nKeyGenerator<T> {
61  
62      protected static final String FIELDS = "fields";
63      private static final String LABEL = "label";
64  
65      /**
66       * Default implementation for {@link #keysFor(String, Object, java.lang.reflect.AnnotatedElement)}, which sets
67       * the undecoratedResult value if it is not null, and delegates to {@link #keysFor(java.util.List, Object, java.lang.reflect.AnnotatedElement)}.
68       */
69      @Override
70      public String[] keysFor(String undecoratedResult, T object, AnnotatedElement el) {
71          final List<String> keys = new ArrayList<>();
72          keysFor(keys, object, el);
73          if (undecoratedResult != null) {
74              keys.add(0, undecoratedResult);
75          }
76          return keys.toArray(new String[keys.size()]);
77      }
78  
79      /**
80       * @deprecated since 5.4.5. Will be removed in a future version.
81       */
82      @Deprecated
83      @Override
84      public String messageBundleNameFor(T object) {
85          return resolveMessageBundleNameUpwards(object);
86      }
87  
88      /**
89       * Protected only to be able to test - ancestors should NOT override this method, unless they have <b>very</b> good reason to do this.
90       * @deprecated since 5.4.5. Will be removed in a future version.
91       */
92      @Deprecated
93      protected String resolveMessageBundleNameUpwards(Object object) {
94          String bundleName = null;
95  
96          try {
97              Method getI18nBasename = object.getClass().getMethod("getI18nBasename");
98              bundleName = (String) getI18nBasename.invoke(object);
99          } catch (NoSuchMethodException e) {
100             // expected - not every parent has to have such method
101         } catch (SecurityException e) {
102             throw new RuntimeException(e);
103         } catch (IllegalArgumentException e) {
104             throw new RuntimeException(e);
105         } catch (IllegalAccessException e) {
106             throw new RuntimeException(e);
107         } catch (InvocationTargetException e) {
108             throw new RuntimeException(e);
109         }
110 
111         if (bundleName == null) {
112             Object parent = getParentViaCast(object);
113             if (parent != null) {
114                 bundleName = resolveMessageBundleNameUpwards(parent);
115             }
116         }
117 
118         return bundleName;
119     }
120 
121     protected abstract void keysFor(List<String> keys, T object, AnnotatedElement el);
122 
123     /**
124      * Returns the name of the field or method passed to {@link I18nKeyGenerator#keysFor(String, T, java.lang.reflect.AnnotatedElement)}.
125      */
126     protected String fieldOrGetterName(AnnotatedElement el) {
127         if (el instanceof Field) {
128             return ((Field) el).getName();
129         } else if (el instanceof Method) {
130             return getterToField((Method) el);
131         } else {
132             throw new IllegalArgumentException("Can't derive i18n key suffix from " + el);
133         }
134     }
135 
136     /**
137      * Returns the parent object that was set via the {@link I18nParentable} interface
138      * when the object was decorated by {@link I18nizer}.
139      */
140     protected <P, C> P getParentViaCast(C obj) {
141         if (!I18nParentable.class.isInstance(obj)) {
142             throw new IllegalStateException("Can't reach parent of " + obj);
143         }
144         final I18nParentable<P> cast = I18nParentable.class.cast(obj);
145         return cast.getI18nContextParent();
146     }
147 
148     /**
149      * Returns the list of parent objects as set via the {@link I18nParentable} interface
150      * when the object was decorated by {@link I18nizer}.
151      *
152      * @see #getParentViaCast(Object)
153      */
154     protected <C> List<Object> getAncestors(C obj) {
155         final ArrayList<Object> ancestors = new ArrayList<Object>();
156         Object p = getParentViaCast(obj);
157         while (p != null) {
158             ancestors.add(p);
159             p = getParentViaCast(p);
160         }
161         return ancestors;
162     }
163 
164     /**
165      * Returns the root object that of the {@link I18nParentable} hierarchy.
166      *
167      * @see #getParentViaCast(Object)
168      */
169     protected <C> Object getRoot(C obj) {
170         Object root = null;
171         Object p = getParentViaCast(obj);
172         while (p != null) {
173             root = p;
174             p = getParentViaCast(p);
175         }
176         return root;
177     }
178 
179     /**
180      * Returns the {@link I18nKeyGenerator}s corresponding to the parent objects of the given object.
181      *
182      * @see #getAncestors(Object)
183      */
184     protected <C> List<I18nKeyGenerator> getAncestorKeyGenerators(C obj) {
185         return Lists.transform(getAncestors(obj), new Function<Object, I18nKeyGenerator>() {
186             @Override
187             public I18nKeyGenerator apply(Object input) {
188                 return getKeyGenerator(input);
189             }
190         });
191     }
192 
193     /**
194      * Returns the {@link I18nKeyGenerator}s corresponding to the root object of the given object.
195      *
196      * @see #getRoot(Object)
197      */
198     protected <C> I18nKeyGenerator getRootKeyGenerator(C obj) {
199         return getKeyGenerator(getRoot(obj));
200     }
201 
202     /**
203      * Returns the {@link I18nKeyGenerator}s corresponding to the given object.
204      */
205     protected <P> I18nKeyGenerator<P> getKeyGenerator(P obj) {
206         return I18nKeyGeneratorFactory.newKeyGeneratorFor(obj);
207     }
208 
209     /**
210      * Adds a key to the given list; if the last part is "label", adds another key without it.
211      *
212      * @see #keyify(String...)
213      */
214     protected void addKey(List<String> keys, boolean stripLabelSuffix, String... parts) {
215         this.addKey(keys, keyify(parts));
216         if (stripLabelSuffix && LABEL.equals(parts[parts.length - 1])) { //this unnecessary increases count of keys in memory, kept for compatibility
217             this.addKey(keys, keyify(Arrays.copyOfRange(parts, 0, parts.length - 1)));
218         }
219     }
220 
221     protected void addKey(List<String> keys, String... parts) {
222         this.addKey(keys, true, parts);
223     }
224 
225     private void addKey(List<String> keys, String key) {
226         if (!keys.contains(key)) {
227             keys.add(key);
228         }
229     }
230 
231     /**
232      * Makes up a key from the given parts.
233      */
234     protected String keyify(String... parts) {
235         return StringUtils.join(parts, '.');
236     }
237 
238     /**
239      * Replaces the characters like ':' or '/' with dots to create i18n key.
240      */
241     protected String keyify(String id) {
242         return StringUtils.replaceChars(id, ":/", "..");
243     }
244 
245     /**
246      * 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 [.].
247      * <p>
248      * 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.
249      */
250     protected String getIdOrNameForUnknownRoot(Object obj, boolean keyify) {
251         // passed object might already be root
252         final Object root = getParentViaCast(obj) != null ? getRoot(obj) : obj;
253         @SuppressWarnings("unchecked")
254         final Set<Method> methods = ReflectionUtils.getMethods(root.getClass(), new Predicate<Method>() {
255 
256             @Override
257             public boolean apply(Method input) {
258                 if ("getId".equals(input.getName()) || "getName".equals(input.getName())) {
259                     return true;
260                 }
261                 return false;
262             }
263         });
264 
265         try {
266             String idOrName = null;
267             Iterator<Method> iterator = methods.iterator();
268             // first method returning a non-null result is taken
269             while (iterator.hasNext()) {
270                 idOrName = (String) iterator.next().invoke(root);
271                 if (StringUtils.isNotBlank(idOrName)) {
272                     return keyify ? keyify(idOrName) : idOrName;
273                 }
274             }
275             return null;
276 
277         } catch (IllegalArgumentException e) {
278             throw new RuntimeException(e);
279         } catch (IllegalAccessException e) {
280             throw new RuntimeException(e);
281         } catch (InvocationTargetException e) {
282             throw new RuntimeException(e);
283         }
284     }
285 
286     protected String getIdOrNameForUnknownRoot(Object obj) {
287         return getIdOrNameForUnknownRoot(obj, true);
288     }
289 
290     protected String getModuleName(String id) {
291         return StringUtils.contains(id, ":") ? StringUtils.substringBefore(id, ":") : null;
292     }
293 
294     protected String getIdWithoutModuleName(String id) {
295         return keyify(StringUtils.contains(id, ":") ? StringUtils.substringAfter(id, ":") : id);
296     }
297 
298     /**
299      * Returns the property name corresponding to a given "getter" method.
300      */
301     private String getterToField(Method method) {
302         final String methodName = method.getName();
303         if (methodName.startsWith("get")) {
304             return methodName.substring(3, 4).toLowerCase() + methodName.substring(4);
305         } else {
306             throw new IllegalArgumentException(method + " is not a getter method");
307         }
308     }
309 
310 }