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 javax.annotation.Nullable;
47  
48  import org.apache.commons.lang.StringUtils;
49  import org.reflections.ReflectionUtils;
50  
51  import com.google.common.base.Function;
52  import com.google.common.base.Predicate;
53  import com.google.common.collect.Lists;
54  
55  /**
56   * Abstract implementation of {@link I18nKeyGenerator} which provides a number of helper methods to generate keys.
57   * By casting objects to {@link I18nParentable} (see {@link #getParentViaCast(Object)}), implementations are
58   * able to generate context-dependent keys for any given object.
59   * 
60   * @param <T> the type of Object this {@link I18nKeyGenerator} generates keys for.
61   */
62  public abstract class AbstractI18nKeyGenerator<T> implements I18nKeyGenerator<T> {
63  
64      /**
65       * Default implementation for {@link #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<String>();
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      @Override
79      public String messageBundleNameFor(T object) {
80          return resolveMessageBundleNameUpwards(object);
81      }
82  
83      /**
84       * Protected only to be able to test - ancestors should NOT override this method, unless they have <b>very</b> good reason to do this.
85       */
86      protected String resolveMessageBundleNameUpwards(Object object) {
87          String bundleName = null;
88  
89          try {
90              Method getI18nBasename = object.getClass().getMethod("getI18nBasename");
91              bundleName = (String) getI18nBasename.invoke(object);
92          } catch (NoSuchMethodException e) {
93              // expected - not every parent has to have such method
94          } catch (SecurityException e) {
95              throw new RuntimeException(e);
96          } catch (IllegalArgumentException e) {
97              throw new RuntimeException(e);
98          } catch (IllegalAccessException e) {
99              throw new RuntimeException(e);
100         } catch (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<Object>();
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 Lists.transform(getAncestors(obj), new Function<Object, I18nKeyGenerator>() {
179             @Override
180             public I18nKeyGenerator apply(@Nullable Object input) {
181                 return getKeyGenerator(input);
182             }
183         });
184     }
185 
186     /**
187      * Returns the {@link I18nKeyGenerator}s corresponding to the root object of the given object.
188      * 
189      * @see #getRoot(Object)
190      */
191     protected <C> I18nKeyGenerator getRootKeyGenerator(C obj) {
192         return getKeyGenerator(getRoot(obj));
193     }
194 
195     /**
196      * Returns the {@link I18nKeyGenerator}s corresponding to the given object.
197      */
198     protected <P> I18nKeyGenerator<P> getKeyGenerator(P obj) {
199         return I18nKeyGeneratorFactory.newKeyGeneratorFor(obj);
200     }
201 
202     /**
203      * Adds a key to the given list; if the last part is "label", adds another key with it.
204      * 
205      * @see #keyify(String...)
206      */
207     protected void addKey(List<String> keys, String... parts) {
208         keys.add(keyify(parts));
209         if ("label".equals(parts[parts.length - 1])) {
210             keys.add(keyify(Arrays.copyOfRange(parts, 0, parts.length - 1)));
211         }
212     }
213 
214     /**
215      * Makes up a key from the given parts.
216      */
217     protected String keyify(String... parts) {
218         return StringUtils.join(parts, '.');
219     }
220 
221     /**
222      * 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 [.].
223      * <p>
224      * 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.
225      */
226     protected String getIdOrNameForUnknownRoot(Object obj) {
227         // passed object might already be root
228         final Object root = getParentViaCast(obj) != null ? getRoot(obj) : obj;
229         @SuppressWarnings("unchecked")
230         final Set<Method> methods = ReflectionUtils.getMethods(root.getClass(), new Predicate<Method>() {
231 
232             @Override
233             public boolean apply(Method input) {
234                 if ("getId".equals(input.getName()) || "getName".equals(input.getName())) {
235                     return true;
236                 }
237                 return false;
238             }
239         });
240 
241         try {
242             String idOrName = null;
243             Iterator<Method> iterator = methods.iterator();
244             // first method returning a non-null result is taken
245             while (iterator.hasNext()) {
246                 idOrName = (String) iterator.next().invoke(root);
247                 if (StringUtils.isNotBlank(idOrName)) {
248                     return idOrName.replaceAll(":", ".").replaceAll("/", ".");
249                 }
250             }
251             return null;
252 
253         } catch (IllegalArgumentException e) {
254             throw new RuntimeException(e);
255         } catch (IllegalAccessException e) {
256             throw new RuntimeException(e);
257         } catch (InvocationTargetException e) {
258             throw new RuntimeException(e);
259         }
260     }
261 
262     /**
263      * Returns the property name corresponding to a given "getter" method.
264      */
265     private String getterToField(Method method) {
266         final String methodName = method.getName();
267         if (methodName.startsWith("get")) {
268             return methodName.substring(3, 4).toLowerCase() + methodName.substring(4);
269         } else {
270             throw new IllegalArgumentException(method + " is not a getter method");
271         }
272     }
273 
274 }