/********************************************************************** * Copyright (c) 2005-2009 ant4eclipse project team. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Nils Hartmann, Daniel Kasmeroglu, Gerd Wuetherich **********************************************************************/ package org.ant4eclipse.lib.core.nls; import org.ant4eclipse.lib.core.exception.ExceptionCode; import org.ant4eclipse.lib.core.util.Utilities; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Enumeration; import java.util.Hashtable; import java.util.Locale; import java.util.Map; import java.util.Properties; /** * <p> * <b>Support for I18N</b> * <p> * This class sets <tt>public static</tt> fields of a class with values read from (internationalized) properties files * * <p> * To initialize the fields, a class needs to call {@link #initialize(Class)}, passing itself as the parameter. This can * be done in the <code>static</code> block of a class: * * <pre> * package org.ant4eclipse; * * public class MyClass { * public static String myMessage; * public static ExceptionCode myExceptionCode; * * static { * initialize(MyClass.class) * } * } * </pre> * * <p> * To get the localized message a Property file will be read at runtime. * * <p> * It is possible to specify default messages using the {@link NLSMessage} annotation: * * <pre> * package org.ant4eclipse; * * public class MyClass { * @NLSMessage("My Default Message") * public static String myMessage; * @NLSMessage("My default exception code") * public static ExceptionCode myExceptionCode; * * static { * initialize(MyClass.class) * } * } * </pre> * * <p> * The NLSInitializer class has built-in support for Strings, Exception Codes and any <tt>public static</tt> fields that * are marked with {@link NLSMessage} and have a type with a single-argument constructor taking a String. * * @todo [10-Dec-2009:KASI] I need to recheck this. I suspect that the properties should be loaded completely for the * setup so applying the values would not happen within a custom Properties implementation (allows to remove some * checking code and to simplify this code). * * @author Nils Hartmann (nils@nilshartmann.net) */ public abstract class NLS { /** - */ private static final String MSG_MISUSEDNLSANNOTATION = "NLS-Annotation detected on field with wrong modifiers '%s.%s'. Field is %s"; /** - */ private static final String MSG_DEFAULTMESSAGE = "[WARN: No (default) message for field '%s' found]"; /** - */ private static final String MSG_MISSINGCONSTRUCTOR = "Could not find constructor '%s' (String) on type : %s"; /** - */ private static final String MSG_COULDNOTINSTANTIATECLASS = "The class '%s' could not be instantiated using constructor '%s'"; /** - */ private static final String MSG_UNKNOWNPROPERTY = "Message-Property '%s' does not exist at class '%s'\n"; /** - */ private static final String MSG_COULDNOTSETFIELD = "Could not set field '%s': %s\n"; /** - */ private static final String MSG_COULDNOTREADPROPERTIES = "Could not read properties file '%s': %s\n"; /** The file extensions for files that contain messages */ private static final String EXTENSION = ".properties"; /** all suffixes for current locale ("en_En", "en", "") */ private static String[] nlSuffixes; /** * Initializes all (NLS) fields of the given class * * @param clazz * The class which field values will be setup with internationalised information. Not <code>null</code>. */ public static final void initialize(Class<?> clazz) { Field[] fields = clazz.getDeclaredFields(); Map<String, Field> nlsFields = new Hashtable<String, Field>(); // Detect NLS fields (public String fields) for (Field field : fields) { if (isNLSField(field)) { nlsFields.put(field.getName(), field); } } // get a list of potential property files accoring to the current locale // for this class (MyClass_en_EN.properties, MyClass_en.properties, MyClass.properties) String baseName = clazz.getName(); String[] variants = buildVariants(baseName); // Create holder for the messages. The put()-method of MessageProperties // will set a new property not only to the properties instance but // also to the appropriate field on "clazz" Properties messages = new Properties(); // Load messages from properties files and set them (via MessageProperties) // to the appropriate fields on clazz loadProperties(messages, variants); applyProperties(messages, clazz, nlsFields); } /** * Applies all values located within a properties file, so the fields will get translated values. * * @param messages * The properties providing all messages. Not <code>null</code>. * @param clazz * The class which is only used for reporting. Not <code>null</code>. * @param fields * The fields which have to be recognized for the changes. Not <code>null</code>. */ @SuppressWarnings("unchecked") private static final void applyProperties(Properties messages, Class<?> clazz, Map<String, Field> fields) { for (Map.Entry<String, Field> entry : fields.entrySet()) { String key = entry.getKey(); Field field = entry.getValue(); String value = null; if (messages.containsKey(key)) { value = messages.getProperty(key); } else { // no value found within the properties, so generate a default message in order // to prevent npe's. value = getDefaultMessage(field); } Object fieldValue = getFieldValue(field, value); try { field.set(null, fieldValue); } catch (Exception ex) { /** * @todo [13-Dec-2009:KASI] This should cause a RuntimeException as the code cannot rely on an initialised field * for this case. */ System.err.printf(MSG_COULDNOTSETFIELD, field.getName(), ex.getMessage()); } messages.remove(key); } Enumeration<String> unset = (Enumeration<String>) messages.propertyNames(); while (unset.hasMoreElements()) { /** * @todo [13-Dec-2009:KASI] This should cause a RuntimeException as this is the result of misconfiguration (and * it's easily fixable, too). */ System.out.printf(MSG_UNKNOWNPROPERTY, unset.nextElement(), clazz.getName()); } } /** * Returns a default message that will be set to the given field. * * <p> * The default message is read from the field's {@link NLSMessage} annotation. If there is no annotation or if the * annotation has no value, a dummy message will be returned to avoid null pointer exceptions later at runtime when * the field get accessed * * @param field * The field * @return A message for the field. Never null */ private static final String getDefaultMessage(Field field) { NLSMessage nlsMessage = field.getAnnotation(NLSMessage.class); if ((nlsMessage == null) || (nlsMessage.value() == null) || (nlsMessage.value().trim().length() == 0)) { return String.format(MSG_DEFAULTMESSAGE, field); } return nlsMessage.value(); } /** * Converts the given String <tt>value</tt> to an object that can be set to the given field * * @param field * The field that should take the converted object * @param value * The value to convert * @return */ private static final Object getFieldValue(Field field, String value) { if (String.class == field.getType()) { // for fields of type String return value as-is return value; } else { // in all other cases try to construct an object using it's class single-arg constructor return newObjectFromString(field.getType(), value); } } /** * Instantiates a new object of the given type with the given message * * <p> * The type must have a (declared) constructor that takes a single string parameter * * @param type * The concrete type that should be instantiated. * @param message * The message for the ExceptionCodes's constructor * @return the instantiated ExceptionCode */ private static final Object newObjectFromString(Class<?> type, String message) { Constructor<?> constructor; try { constructor = type.getDeclaredConstructor(String.class); constructor.setAccessible(true); } catch (Exception ex) { throw new RuntimeException(String.format(MSG_MISSINGCONSTRUCTOR, type.getSimpleName(), ex.getMessage()), ex); } try { return constructor.newInstance(message); } catch (Exception ex) { throw new RuntimeException(String.format(MSG_COULDNOTINSTANTIATECLASS, type.getName(), type.getSimpleName()), ex); } } /** * Checks if the given field is an "NLS field". * * A NLS field must be public static and not final. * * @param field * the field to check * @return true if it is a "NLS field" that should be set to a localized value */ private static final boolean isNLSField(Field field) { int modifier = field.getModifiers(); String problem = null; // check if modifiers are correct if (!Modifier.isStatic(modifier)) { problem = "not static"; } else if (Modifier.isFinal(modifier)) { problem = "final"; } else if (!Modifier.isPublic(modifier)) { problem = "not public"; } NLSMessage nlsMessage = field.getAnnotation(NLSMessage.class); if (problem != null) { if (nlsMessage == null) { // not an NLS field, no problem, just ignore it return false; } // NLS-annotation on a field with wrong modifiers (not public static non-final) throw new RuntimeException(String.format(MSG_MISUSEDNLSANNOTATION, field.getDeclaringClass().getName(), field .getName(), problem)); } // NLS fields are fields with @NLSMessage annotation and fields with type String and Exception code (and subclasses) return (nlsMessage != null) || (field.getType() == String.class) || ExceptionCode.class.isAssignableFrom(field.getType()); } /** * Build an array of property files to search. The returned array contains the property fields in order from most * specific to most generic. So, in the FR_fr locale, it will return file_fr_FR.properties, then file_fr.properties, * and finally file.properties. */ private static final String[] buildVariants(String root) { if (nlSuffixes == null) { // build list of suffixes for loading resource bundles String nl = Locale.getDefault().toString(); ArrayList<String> result = new ArrayList<String>(4); int lastSeparator; while (true) { result.add('_' + nl + EXTENSION); lastSeparator = nl.lastIndexOf('_'); if (lastSeparator == -1) { break; } nl = nl.substring(0, lastSeparator); } // add the empty suffix last (most general) result.add(EXTENSION); nlSuffixes = result.toArray(new String[result.size()]); } root = root.replace('.', '/'); String[] variants = new String[nlSuffixes.length]; for (int i = 0; i < variants.length; i++) { variants[i] = root + nlSuffixes[i]; } return variants; } /** * Load all (existing) properties files, that are specified in <tt>variants</tt>. * <p> * Properties files listed in variants, that are not existing, are ignored * </p> * * @param messages * A properties object that will contain the properties * @param variants * file names to read */ private static final void loadProperties(Properties messages, String[] variants) { for (String variant : variants) { InputStream is = null; try { is = Thread.currentThread().getContextClassLoader().getResourceAsStream(variant); if (is != null) { messages.load(is); } } catch (IOException ex) { /** * @todo [13-Dec-2009:KASI] This should cause a RuntimeException as there's something wrong in the application * setup. */ System.err.printf(MSG_COULDNOTREADPROPERTIES, variant, ex.getMessage()); } finally { Utilities.close(is); } } } } /* ENDCLASS */