View Javadoc
1   /**
2    * This file Copyright (c) 2013-2015 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      /**
63       * Default implementation for {@link #keysFor(String, Object, java.lang.reflect.AnnotatedElement)}, which sets
64       * the undecoratedResult value if it is not null, and delegates to {@link #keysFor(java.util.List, Object, java.lang.reflect.AnnotatedElement)}.
65       */
66      @Override
67      public String[] keysFor(String undecoratedResult, T object, AnnotatedElement el) {
68          final List<String> keys = new ArrayList<String>();
69          keysFor(keys, object, el);
70          if (undecoratedResult != null) {
71              keys.add(0, undecoratedResult);
72          }
73          return keys.toArray(new String[keys.size()]);
74      }
75  
76      @Override
77      public String messageBundleNameFor(T object) {
78          return resolveMessageBundleNameUpwards(object);
79      }
80  
81      /**
82       * Protected only to be able to test - ancestors should NOT override this method, unless they have <b>very</b> good reason to do this.
83       */
84      protected String resolveMessageBundleNameUpwards(Object object) {
85          String bundleName = null;
86  
87          try {
88              Method getI18nBasename = object.getClass().getMethod("getI18nBasename");
89              bundleName = (String) getI18nBasename.invoke(object);
90          } catch (NoSuchMethodException e) {
91              // expected - not every parent has to have such method
92          } catch (SecurityException e) {
93              throw new RuntimeException(e);
94          } catch (IllegalArgumentException e) {
95              throw new RuntimeException(e);
96          } catch (IllegalAccessException e) {
97              throw new RuntimeException(e);
98          } catch (InvocationTargetException e) {
99              throw new RuntimeException(e);
100         }
101 
102         if (bundleName == null) {
103             Object parent = getParentViaCast(object);
104             if (parent != null) {
105                 bundleName = resolveMessageBundleNameUpwards(parent);
106             }
107         }
108 
109         return bundleName;
110     }
111 
112     protected abstract void keysFor(List<String> keys, T object, AnnotatedElement el);
113 
114     /**
115      * Returns the name of the field or method passed to {@link I18nKeyGenerator#keysFor(String, T, java.lang.reflect.AnnotatedElement)}.
116      */
117     protected String fieldOrGetterName(AnnotatedElement el) {
118         if (el instanceof Field) {
119             return ((Field) el).getName();
120         } else if (el instanceof Method) {
121             return getterToField((Method) el);
122         } else {
123             throw new IllegalArgumentException("Can't derive i18n key suffix from " + el);
124         }
125     }
126 
127     /**
128      * Returns the parent object that was set via the {@link I18nParentable} interface
129      * when the object was decorated by {@link I18nizer}.
130      */
131     protected <P, C> P getParentViaCast(C obj) {
132         if (!I18nParentable.class.isInstance(obj)) {
133             throw new IllegalStateException("Can't reach parent of " + obj);
134         }
135         final I18nParentable<P> cast = I18nParentable.class.cast(obj);
136         return cast.getI18nContextParent();
137     }
138 
139     /**
140      * Returns the list of parent objects as set via the {@link I18nParentable} interface
141      * when the object was decorated by {@link I18nizer}.
142      * 
143      * @see #getParentViaCast(Object)
144      */
145     protected <C> List<Object> getAncestors(C obj) {
146         final ArrayList<Object> ancestors = new ArrayList<Object>();
147         Object p = getParentViaCast(obj);
148         while (p != null) {
149             ancestors.add(p);
150             p = getParentViaCast(p);
151         }
152         return ancestors;
153     }
154 
155     /**
156      * Returns the root object that of the {@link I18nParentable} hierarchy.
157      * 
158      * @see #getParentViaCast(Object)
159      */
160     protected <C> Object getRoot(C obj) {
161         Object root = null;
162         Object p = getParentViaCast(obj);
163         while (p != null) {
164             root = p;
165             p = getParentViaCast(p);
166         }
167         return root;
168     }
169 
170     /**
171      * Returns the {@link I18nKeyGenerator}s corresponding to the parent objects of the given object.
172      * 
173      * @see #getAncestors(Object)
174      */
175     protected <C> List<I18nKeyGenerator> getAncestorKeyGenerators(C obj) {
176         return Lists.transform(getAncestors(obj), new Function<Object, I18nKeyGenerator>() {
177             @Override
178             public I18nKeyGenerator apply(Object input) {
179                 return getKeyGenerator(input);
180             }
181         });
182     }
183 
184     /**
185      * Returns the {@link I18nKeyGenerator}s corresponding to the root object of the given object.
186      * 
187      * @see #getRoot(Object)
188      */
189     protected <C> I18nKeyGenerator getRootKeyGenerator(C obj) {
190         return getKeyGenerator(getRoot(obj));
191     }
192 
193     /**
194      * Returns the {@link I18nKeyGenerator}s corresponding to the given object.
195      */
196     protected <P> I18nKeyGenerator<P> getKeyGenerator(P obj) {
197         return I18nKeyGeneratorFactory.newKeyGeneratorFor(obj);
198     }
199 
200     /**
201      * Adds a key to the given list; if the last part is "label", adds another key with it.
202      * 
203      * @see #keyify(String...)
204      */
205     protected void addKey(List<String> keys, String... parts) {
206         keys.add(keyify(parts));
207         if ("label".equals(parts[parts.length - 1])) {
208             keys.add(keyify(Arrays.copyOfRange(parts, 0, parts.length - 1)));
209         }
210     }
211 
212     /**
213      * Makes up a key from the given parts.
214      */
215     protected String keyify(String... parts) {
216         return StringUtils.join(parts, '.');
217     }
218 
219     /**
220      * 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 [.].
221      * <p>
222      * 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.
223      */
224     protected String getIdOrNameForUnknownRoot(Object obj) {
225         // passed object might already be root
226         final Object root = getParentViaCast(obj) != null ? getRoot(obj) : obj;
227         @SuppressWarnings("unchecked")
228         final Set<Method> methods = ReflectionUtils.getMethods(root.getClass(), new Predicate<Method>() {
229 
230             @Override
231             public boolean apply(Method input) {
232                 if ("getId".equals(input.getName()) || "getName".equals(input.getName())) {
233                     return true;
234                 }
235                 return false;
236             }
237         });
238 
239         try {
240             String idOrName = null;
241             Iterator<Method> iterator = methods.iterator();
242             // first method returning a non-null result is taken
243             while (iterator.hasNext()) {
244                 idOrName = (String) iterator.next().invoke(root);
245                 if (StringUtils.isNotBlank(idOrName)) {
246                     return idOrName.replaceAll(":", ".").replaceAll("/", ".");
247                 }
248             }
249             return null;
250 
251         } catch (IllegalArgumentException e) {
252             throw new RuntimeException(e);
253         } catch (IllegalAccessException e) {
254             throw new RuntimeException(e);
255         } catch (InvocationTargetException e) {
256             throw new RuntimeException(e);
257         }
258     }
259 
260     /**
261      * Returns the property name corresponding to a given "getter" method.
262      */
263     private String getterToField(Method method) {
264         final String methodName = method.getName();
265         if (methodName.startsWith("get")) {
266             return methodName.substring(3, 4).toLowerCase() + methodName.substring(4);
267         } else {
268             throw new IllegalArgumentException(method + " is not a getter method");
269         }
270     }
271 
272 }