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