Package com.google.sitebricks

Source Code of com.google.sitebricks.Localizer$MessageDescriptor

package com.google.sitebricks;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import javax.servlet.http.HttpServletRequest;

import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.inject.Binder;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.name.Named;
import com.google.sitebricks.compiler.ExpressionCompileException;
import com.google.sitebricks.compiler.MvelEvaluatorCompiler;
import com.google.sitebricks.compiler.Parsing;
import com.google.sitebricks.compiler.Token;
import com.google.sitebricks.i18n.Message;
import com.google.sitebricks.rendering.Strings;

/**
* A Utility that binds a localizable interface to its instance parameters.
*/
public class Localizer {
  private final Binder binder;
  private final Set<Localization> localizations;

  // These are the processed, individual message sets by locale.
  private final Map<String, Map<String, MessageDescriptor>> localizedValues = Maps.newHashMap();

  // A map to track if we have bound the proxy for a given i18n interface yet.
  private Set<Class<?>> i18nedSoFar = Sets.newHashSet();

  /**
   * A value object that represents the localization of an i18n  interface to a locale
   * and corresponding set of messages.
   */
  public static class Localization {
    // TODO(dhanji): Convert class reference to weak?
    private final Class<?> clazz;
    private final Locale locale;
    private final Map<String, String> messageBundle;

    public Localization(Class<?> clazz, Locale locale, Map<String, String> messageBundle) {
      this.clazz = clazz;
      this.locale = locale;
      this.messageBundle = messageBundle;
    }
   
    public Class<?> getClazz() {
        return this.clazz;
    }
    public Locale getLocale() {
        return this.locale;
    }
    public Map<String, String> getMessageBundle() {
        return this.messageBundle;
    }

  }

  static final Localization DEFAULT = new Localization(null, null, null);

  private Localizer(Binder binder, Set<Localization> localizations) {
    this.binder = binder;
    this.localizations = localizations;
  }

  public static void localizeAll(Binder binder, Set<Localization> localizations) {
    new Localizer(binder, localizations).localize();
  }

  private void localize() {
    for (Localization localization : localizations) {
      // First scan and ensure that all methods on the interface contain i18n params.
      bindMessages(localization);
    }

    // We're done with this so we don't need the set anymore.
    i18nedSoFar = null;
  }

  private void bindMessages(Localization localization) {
    Class<?> iface = localization.clazz;
    Map<String, MessageDescriptor> messages = Maps.newHashMap();

    for (Method method : iface.getMethods()) {
      Message message = method.getAnnotation(Message.class);

      check(null != message,
          "Found an i18n interface method missing @Message annotation: ", iface, method);

      if (null != message) {
        check(!Strings.empty(message.message()),
            "Empty @Message annotation is not allowed ", iface, method);
      }

      String template = localization.messageBundle.get(method.getName());
      check(null != template,
          "Provided resource bundle does not contain a localization for message: ", iface, method);
      check(String.class.equals(method.getReturnType()),
          "All i18n interface methods MUST return String: ", iface, method);

      int argumentCount = method.getParameterTypes().length;
      Map<String, Type> arguments = Maps.newLinkedHashMap();

      for (int i = 0; i < argumentCount; i++) {
        Annotation[] annotations = method.getParameterAnnotations()[i];

        check(annotations.length == 1,
            "Only @Named annotations are allowed on i18n method arguments: ", iface, method);
        if (annotations.length == 0) {
          continue;
        }

        check(Named.class.isInstance(annotations[0]),
            "Named annotation is missing from i18n interface method argument: ", iface, method);

        // Bind each argument to a template parameter a la Dynamic Finders.
        arguments.put(((Named) annotations[0]).value(), method.getParameterTypes()[i]);
      }

      // No point in throwing an NPE ourselves, but we want to keep processing errors so continue
      if (null == template || null == message) {
        continue;
      }

      // Compile arg names against message template to ensure it works.
      List<Token> tokens = null;
      try {
        MvelEvaluatorCompiler compiler = new MvelEvaluatorCompiler(arguments);

        // Compile both the default message as well as the provided localized one.
        Parsing.tokenize(message.message(), compiler);
        tokens = Parsing.tokenize(template, compiler);
      } catch (ExpressionCompileException e) {
        check(false, "Compile error in i18n message template: \n  " + e.getError().getError() +
            " in expression " + e.getError().getExpression() +"\n\n  ...in: ", iface, method);
      }

      // OK now actually go through and build a map between method names and values.
      messages.put(method.getName(), new MessageDescriptor(tokens, arguments));
    }

    bindMessageProvider(iface, localization, messages);
  }

  @SuppressWarnings("unchecked") // We have a guarantee that Proxy will only return subtypes.
  private void bindMessageProvider(final Class<?> iface,
                                   Localization localization,
                                   Map<String, MessageDescriptor> messages) {

    // Add to the value map.
    localizedValues.put(createLocaleInterfaceKey(iface, localization.locale), messages);

    // Only need to bind the proxy once, for all locales.
    if (!i18nedSoFar.contains(iface)) {
      i18nedSoFar.add(iface);

      binder.bind((Class)iface).toProvider(new Provider() {

        // Wonderful Guice hack to get around not using assisted inject.
        @Inject
        private final Provider<HttpServletRequest> requestProvider = null;

        // This is our delegate field that proxies the interface.
        private final Object instance = Proxy.newProxyInstance(
            Thread.currentThread().getContextClassLoader(),
            new Class[] { iface }, new InvocationHandler() {

              /**
               * Returns the localized message bundle value, keyed by the method name invoked.
               */
              public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                Locale locale = requestProvider.get().getLocale();
                Map<String, MessageDescriptor> messages = getMessagesWithFallback(locale);

                // Use default if we don't support the given locale.
                if (null == messages) {
                  messages = getMessagesWithFallback(Locale.getDefault());
                }

                MessageDescriptor descriptor = messages.get(method.getName());
                if (descriptor == null) {
                  throw new IllegalStateException("Could not find message '"
                      + method.getName() + "' in " + messages);
                }
        return descriptor.render(args);
              }

      private Map<String, MessageDescriptor> getMessagesWithFallback(Locale locale) {
        String localeInterfaceKey = createLocaleInterfaceKey(iface, locale);
        Map<String, MessageDescriptor> result = localizedValues.get(localeInterfaceKey);
        if (result == null) {
          result = localizedValues.get(new Locale(locale.getLanguage()));
        }
        return result;
      }
          });

        // return our proxy here.
        public Object get() {
          return instance;
        }

      });
    }

  }

  private String createLocaleInterfaceKey(final Class<?> iface, Locale locale) {
    return locale.toString() + ":" + iface.getName();
  }

  private static class MessageDescriptor {
    private final List<Token> tokens;
    private final Map<String, Type> argumentTypes;

    private MessageDescriptor(List<Token> tokens, Map<String, Type> argumentTypes) {
      this.tokens = tokens;
      this.argumentTypes = argumentTypes;
    }

    public String render(Object[] args) {
      Map<String, Object> arguments = Maps.newHashMap();

      int i = 0;
      for (String name : argumentTypes.keySet()) {
        arguments.put(name, args[i]);
        i++;
      }

      return Parsing.render(tokens, arguments);
    }

  }

  private void check(boolean condition, String error, Class<?> key, Method method) {
    if (!condition) {
      binder.addError(error + "\n  at " + key.getName() + "." + method.getName() + "()\n");
    }
  }

  /**
   * Returns a localization value object describing the defaults specified in the @Message
   * annotations of the methods on the given i18n interface. The locale used is the system
   * default.
   */
  public static Localization defaultLocalizationFor(Class<?> iface) {
    Map<String, String> defaultMessages = Maps.newHashMap();

    for (Method method : iface.getMethods()) {
      Message msg = method.getAnnotation(Message.class);
      if (null != msg) {
        defaultMessages.put(method.getName(), msg.message());
      }
    }

    return new Localization(iface, Locale.getDefault(), defaultMessages);
  }

}
TOP

Related Classes of com.google.sitebricks.Localizer$MessageDescriptor

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.