view org.eclipse.osgi/supplement/src/org/eclipse/osgi/util/NLS.d @ 105:bbe49769ec18

...
author Frank Benoit <benoit@tionex.de>
date Sun, 08 Nov 2009 12:42:30 +0100
parents 8594250b1d1c
children
line wrap: on
line source

/*******************************************************************************
 * Copyright (c) 2005, 2008 IBM Corporation and others.
 * 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:
 *     IBM - Initial API and implementation
 *******************************************************************************/
// Port to the D programming language:
//     Frank Benoit <benoit@tionex.de>
module org.eclipse.osgi.util.NLS;

import java.lang.all;


import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;

import org.eclipse.osgi.framework.debug_.Debug;
import org.eclipse.osgi.framework.log.FrameworkLog;
import org.eclipse.osgi.framework.log.FrameworkLogEntry;

/**
 * Common superclass for all message bundle classes.  Provides convenience
 * methods for manipulating messages.
 * <p>
 * The <code>#bind</code> methods perform string substitution and should be considered a
 * convenience and <em>not</em> a full substitute replacement for <code>MessageFormat#format</code>
 * method calls.
 * </p>
 * <p>
 * Text appearing within curly braces in the given message, will be interpreted
 * as a numeric index to the corresponding substitution object in the given array. Calling
 * the <code>#bind</code> methods with text that does not map to an integer will result in an
 * {@link IllegalArgumentException}.
 * </p>
 * <p>
 * Text appearing within single quotes is treated as a literal. A single quote is escaped by
 * a preceeding single quote.
 * </p>
 * <p>
 * Clients who wish to use the full substitution power of the <code>MessageFormat</code> class should
 * call that class directly and not use these <code>#bind</code> methods.
 * </p>
 * <p>
 * Clients may subclass this type.
 * </p>
 *
 * @since 3.1
 */
public abstract class NLS {

    private static final Object[] EMPTY_ARGS = null;
    private static final String EXTENSION = ".properties"; //$NON-NLS-1$
    private static String[] nlSuffixes;
    /*
     * NOTE do not change the name of this field; it is set by the Framework using reflection
     */
    private static FrameworkLog frameworkLog;

    static final int SEVERITY_ERROR = 0x04;
    static final int SEVERITY_WARNING = 0x02;
    /*
     * This object is assigned to the value of a field map to indicate
     * that a translated message has already been assigned to that field.
     */
    static final Object ASSIGNED = new Object();

    /**
     * Creates a new NLS instance.
     */
    protected this() {
        super();
    }

    /**
     * Bind the given message's substitution locations with the given string value.
     *
     * @param message the message to be manipulated
     * @param binding the object to be inserted into the message
     * @return the manipulated String
     * @throws IllegalArgumentException if the text appearing within curly braces in the given message does not map to an integer
     */
    public static String bind(String message, Object binding) {
        return internalBind(message, null, String.valueOf(binding), null);
    }

    /**
     * Bind the given message's substitution locations with the given string values.
     *
     * @param message the message to be manipulated
     * @param binding1 An object to be inserted into the message
     * @param binding2 A second object to be inserted into the message
     * @return the manipulated String
     * @throws IllegalArgumentException if the text appearing within curly braces in the given message does not map to an integer
     */
    public static String bind(String message, Object binding1, Object binding2) {
        return internalBind(message, null, String.valueOf(binding1), String.valueOf(binding2));
    }

    /**
     * Bind the given message's substitution locations with the given string values.
     *
     * @param message the message to be manipulated
     * @param bindings An array of objects to be inserted into the message
     * @return the manipulated String
     * @throws IllegalArgumentException if the text appearing within curly braces in the given message does not map to an integer
     */
    public static String bind(String message, Object[] bindings) {
        return internalBind(message, bindings, null, null);
    }

    /**
     * Initialize the given class with the values from the specified message bundle.
     *
     * @param bundleName fully qualified path of the class name
     * @param clazz the class where the constants will exist
     */
    public static void initializeMessages(/+FIXFINAL+/ String bundleName, /+FIXFINAL+/ Class clazz) {
        if (System.getSecurityManager() is null) {
            load(bundleName, clazz);
            return;
        }
        AccessController.doPrivileged(new class() PrivilegedAction {
            public Object run() {
                load(bundleName, clazz);
                return null;
            }
        });
    }

    /*
     * Perform the string substitution on the given message with the specified args.
     * See the class comment for exact details.
     */
    private static String internalBind(String message, Object[] args, String argZero, String argOne) {
        if (message is null)
            return "No message available."; //$NON-NLS-1$
        if (args is null || args.length_ is 0)
            args = EMPTY_ARGS;

        int length_ = message.length();
        //estimate correct size of string buffer to avoid growth
        int bufLen = length_ + (args.length_ * 5);
        if (argZero !is null)
            bufLen += argZero.length() - 3;
        if (argOne !is null)
            bufLen += argOne.length() - 3;
        StringBuffer buffer = new StringBuffer(bufLen < 0 ? 0 : bufLen);
        for (int i = 0; i < length_; i++) {
            char c = message.charAt(i);
            switch (c) {
                case '{' :
                    int index = message.indexOf('}', i);
                    // if we don't have a matching closing brace then...
                    if (index is -1) {
                        buffer.append(c);
                        break;
                    }
                    i++;
                    if (i >= length_) {
                        buffer.append(c);
                        break;
                    }
                    // look for a substitution
                    int number = -1;
                    try {
                        number = Integer.parseInt(message.substring(i, index));
                    } catch (NumberFormatException e) {
                        throw new IllegalArgumentException();
                    }
                    if (number is 0 && argZero !is null)
                        buffer.append(argZero);
                    else if (number is 1 && argOne !is null)
                        buffer.append(argOne);
                    else {
                        if (number >= args.length_ || number < 0) {
                            buffer.append("<missing argument>"); //$NON-NLS-1$
                            i = index;
                            break;
                        }
                        buffer.append(args[number]);
                    }
                    i = index;
                    break;
                case '\'' :
                    // if a single quote is the last char on the line then skip it
                    int nextIndex = i + 1;
                    if (nextIndex >= length_) {
                        buffer.append(c);
                        break;
                    }
                    char next = message.charAt(nextIndex);
                    // if the next char is another single quote then write out one
                    if (next is '\'') {
                        i++;
                        buffer.append(c);
                        break;
                    }
                    // otherwise we want to read until we get to the next single quote
                    index = message.indexOf('\'', nextIndex);
                    // if there are no more in the string, then skip it
                    if (index is -1) {
                        buffer.append(c);
                        break;
                    }
                    // otherwise write out the chars inside the quotes
                    buffer.append(message.substring(nextIndex, index));
                    i = index;
                    break;
                default :
                    buffer.append(c);
            }
        }
        return buffer.toString();
    }

    /*
     * 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 String[] buildVariants(String root) {
        if (nlSuffixes is null) {
            //build list of suffixes for loading resource bundles
            String nl = Locale.getDefault().toString();
            ArrayList result = new ArrayList(4);
            int lastSeparator;
            while (true) {
                result.add('_' + nl + EXTENSION);
                lastSeparator = nl.lastIndexOf('_');
                if (lastSeparator is -1)
                    break;
                nl = nl.substring(0, lastSeparator);
            }
            //add the empty suffix last (most general)
            result.add(EXTENSION);
            nlSuffixes = stringcast( 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;
    }

    private static void computeMissingMessages(String bundleName, Class clazz, Map fieldMap, Field[] fieldArray, bool isAccessible) {
        // iterate over the fields in the class to make sure that there aren't any empty ones
        final int MOD_EXPECTED = Modifier.PUBLIC | Modifier.STATIC;
        final int MOD_MASK = MOD_EXPECTED | Modifier.FINAL;
        final int numFields = fieldArray.length_;
        for (int i = 0; i < numFields; i++) {
            Field field = fieldArray[i];
            if ((field.getModifiers() & MOD_MASK) !is MOD_EXPECTED)
                continue;
            //if the field has a a value assigned, there is nothing to do
            if (fieldMap.get(field.getName()) is ASSIGNED)
                continue;
            try {
                // Set a value for this empty field. We should never get an exception here because
                // we know we have a public static non-final field. If we do get an exception, silently
                // log it and continue. This means that the field will (most likely) be un-initialized and
                // will fail later in the code and if so then we will see both the NPE and this error.
                String value = "NLS missing message: " + field.getName() + " in_: " + bundleName; //$NON-NLS-1$ //$NON-NLS-2$
                if (Debug.DEBUG_MESSAGE_BUNDLES)
                    System.out_.println(value);
                log(SEVERITY_WARNING, value, null);
                if (!isAccessible)
                    field.setAccessible(true);
                field.set(null, value);
            } catch (Exception e) {
                log(SEVERITY_ERROR, "Error setting the missing message value for: " + field.getName(), e); //$NON-NLS-1$
            }
        }
    }

    /*
     * Load the given resource bundle using the specified class loader.
     */
    static void load(/+FIXFINAL+/ String bundleName, Class clazz) {
        long start = System.currentTimeMillis();
        final Field[] fieldArray = clazz.getDeclaredFields();
        ClassLoader loader = clazz.getClassLoader();

        bool isAccessible = (clazz.getModifiers() & Modifier.PUBLIC) !is 0;

        //build a map of field names to Field objects
        final int len = fieldArray.length_;
        Map fields = new HashMap(len * 2);
        for (int i = 0; i < len; i++)
            fields.put(fieldArray[i].getName(), fieldArray[i]);

        // search the variants from most specific to most general, since
        // the MessagesProperties.put method will mark assigned fields
        // to prevent them from being assigned twice
        final String[] variants = buildVariants(bundleName);
        for (int i = 0; i < variants.length_; i++) {
            // loader==null if we're launched off the Java boot classpath
            final InputStream input = loader is null ? ClassLoader.getSystemResourceAsStream(variants[i]) : loader.getResourceAsStream(variants[i]);
            if (input is null)
                continue;
            try {
                final MessagesProperties properties = new MessagesProperties(fields, bundleName, isAccessible);
                properties.load(input);
            } catch (IOException e) {
                log(SEVERITY_ERROR, "Error loading " + variants[i], e); //$NON-NLS-1$
            } finally {
                if (input !is null)
                    try {
                        input.close();
                    } catch (IOException e) {
                        // ignore
                    }
            }
        }
        computeMissingMessages(bundleName, clazz, fields, fieldArray, isAccessible);
        if (Debug.DEBUG_MESSAGE_BUNDLES)
            System.out_.println("Time to load message bundle: " + bundleName + " was " + (System.currentTimeMillis() - start) + "ms."); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
    }

    /*
     * The method adds a log entry based on the error message and exception.
     * The output is written to the System.err.
     *
     * This method is only expected to be called if there is a problem in
     * the NLS mechanism. As a result, translation facility is not available
     * here and messages coming out of this log are generally not translated.
     *
     * @param severity - severity of the message (SEVERITY_ERROR or SEVERITY_WARNING)
     * @param message - message to log
     * @param e - exception to log
     */
    static void log(int severity, String message, Exception e) {
        if (frameworkLog !is null) {
            frameworkLog.log(new FrameworkLogEntry("org.eclipse.osgi", severity, 1, message, 0, e, null)); //$NON-NLS-1$
            return;
        }
        String statusMsg;
        switch (severity) {
            case SEVERITY_ERROR :
                statusMsg = "Error: "; //$NON-NLS-1$
                break;
            case SEVERITY_WARNING :
                // intentionally fall through:
            default :
                statusMsg = "Warning: "; //$NON-NLS-1$
        }
        if (message !is null)
            statusMsg += message;
        if (e !is null)
            statusMsg += ": " + e.getMessage(); //$NON-NLS-1$
        System.err.println(statusMsg);
        if (e !is null)
            e.printStackTrace();
    }

    /*
     * Class which sub-classes java.util.Properties and uses the #put method
     * to set field values rather than storing the values in the table.
     */
    private static class MessagesProperties : Properties {

        private static final int MOD_EXPECTED = Modifier.PUBLIC | Modifier.STATIC;
        private static final int MOD_MASK = MOD_EXPECTED | Modifier.FINAL;
        private static final long serialVersionUID = 1L;

        private final String bundleName;
        private final Map fields;
        private final bool isAccessible;

        public this(Map fieldMap, String bundleName, bool isAccessible) {
            super();
            this.fields = fieldMap;
            this.bundleName = bundleName;
            this.isAccessible = isAccessible;
        }

        /* (non-Javadoc)
         * @see java.util.Hashtable#put(java.lang.Object, java.lang.Object)
         */
        public synchronized Object put(Object key, Object value) {
            Object fieldObject = fields.put(key, ASSIGNED);
            // if already assigned, there is nothing to do
            if (fieldObject is ASSIGNED)
                return null;
            if (fieldObject is null) {
                final String msg = "NLS unused message: " + key + " in_: " + bundleName;//$NON-NLS-1$ //$NON-NLS-2$
                if (Debug.DEBUG_MESSAGE_BUNDLES)
                    System.out_.println(msg);
                log(SEVERITY_WARNING, msg, null);
                return null;
            }
            final Field field = cast(Field) fieldObject;
            //can only set value of public static non-final fields
            if ((field.getModifiers() & MOD_MASK) !is MOD_EXPECTED)
                return null;
            try {
                // Check to see if we are allowed to modify the field. If we aren't (for instance
                // if the class is not public) then change the accessible attribute of the field
                // before trying to set the value.
                if (!isAccessible)
                    field.setAccessible(true);
                // Set the value into the field. We should never get an exception here because
                // we know we have a public static non-final field. If we do get an exception, silently
                // log it and continue. This means that the field will (most likely) be un-initialized and
                // will fail later in the code and if so then we will see both the NPE and this error.
                field.set(null, value);
            } catch (Exception e) {
                log(SEVERITY_ERROR, "Exception setting field value.", e); //$NON-NLS-1$
            }
            return null;
        }
    }
}