package pl.matisoft.soy.bundle; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.collect.Lists; import com.google.template.soy.msgs.SoyMsgBundle; import com.google.template.soy.msgs.SoyMsgBundleHandler; import com.google.template.soy.msgs.restricted.SoyMsg; import com.google.template.soy.msgs.restricted.SoyMsgBundleImpl; import com.google.template.soy.xliffmsgplugin.XliffMsgPlugin; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import pl.matisoft.soy.config.SoyViewConfigDefaults; import java.io.IOException; import java.net.URL; import java.util.*; import java.util.concurrent.ConcurrentHashMap; /** * Created with IntelliJ IDEA. * User: mati * Date: 20/06/2013 * Time: 00:01 * * An implementation of SoyMsgBundleResolver that returns SoyMsgBundle based on a configurable url, * which can be retrieved from classpath using Thread.getContextClassLoader(). * * The implementation will fallback to English translation if a language specific translation cannot be found * only if fallbackToEnglish configuration option is set. * * Assuming defaults and locale set to pl_PL, an implementation will look for a following file in a classpath: * messages_pl_PL.xlf */ public class DefaultSoyMsgBundleResolver implements SoyMsgBundleResolver { private static final Logger logger = LoggerFactory.getLogger(DefaultSoyMsgBundleResolver.class); /** a cache of soy msg bundles */ /** friendly */ Map<Locale, SoyMsgBundle> msgBundles = new ConcurrentHashMap<Locale, SoyMsgBundle>(); /** a path to a bundle */ private String messagesPath = SoyViewConfigDefaults.DEF_MESSAGES_PATH; /** will cache msgBundles if a hotReloadMode is off, if debug is on, * will compile msg bundles each time it is invoked */ private boolean hotReloadMode = SoyViewConfigDefaults.DEFAULT_HOT_RELOAD_MODE; /** in case translation is missing for a passed in locale, * whether the implementation should fallback to English returning * an english translation if available */ private boolean fallbackToEnglish = true; /** * Based on a provided locale return a SoyMsgBundle file. * * If a passed in locale object is "Optional.absent()", * the implementation will return Optional.absent() as well * @param locale - maybe locale * @return maybe soy msg bundle */ public Optional<SoyMsgBundle> resolve(final Optional<Locale> locale) throws IOException { if (!locale.isPresent()) { return Optional.absent(); } synchronized (msgBundles) { SoyMsgBundle soyMsgBundle = null; if (isHotReloadModeOff()) { soyMsgBundle = msgBundles.get(locale.get()); } if (soyMsgBundle == null) { soyMsgBundle = createSoyMsgBundle(locale.get()); if (soyMsgBundle == null) { soyMsgBundle = createSoyMsgBundle(new Locale(locale.get().getLanguage())); } if (soyMsgBundle == null && fallbackToEnglish) { soyMsgBundle = createSoyMsgBundle(Locale.ENGLISH); } if (soyMsgBundle == null) { return Optional.absent(); } if (isHotReloadModeOff()) { msgBundles.put(locale.get(), soyMsgBundle); } } return Optional.fromNullable(soyMsgBundle); } } /** * An implementation that using a ContextClassLoader iterates over all urls it finds * based on a messagePath and locale, e.g. messages_de_DE.xlf and returns a merged * SoyMsgBundle of SoyMsgBundle matching a a pattern it finds in a class path. * * @param locale - locale * @return SoyMsgBundle - bundle * @throws java.io.IOException - io error */ protected SoyMsgBundle createSoyMsgBundle(final Locale locale) throws IOException { Preconditions.checkNotNull(messagesPath, "messagesPath cannot be null!"); final String path = messagesPath + "_" + locale.toString() + ".xlf"; final Enumeration<URL> e = Thread.currentThread().getContextClassLoader().getResources(path); final List<SoyMsgBundle> msgBundles = Lists.newArrayList(); final SoyMsgBundleHandler msgBundleHandler = new SoyMsgBundleHandler(new XliffMsgPlugin()); while (e.hasMoreElements()) { final URL msgFile = e.nextElement(); final SoyMsgBundle soyMsgBundle = msgBundleHandler.createFromResource(msgFile); msgBundles.add(soyMsgBundle); } return mergeMsgBundles(locale, msgBundles).orNull(); } /** * Merge msg bundles together, creating new MsgBundle with merges msg bundles passed in as a method argument */ private Optional<? extends SoyMsgBundle> mergeMsgBundles(final Locale locale, final List<SoyMsgBundle> soyMsgBundles) { if (soyMsgBundles.isEmpty()) { return Optional.absent(); } final List<SoyMsg> msgs = Lists.newArrayList(); for (final SoyMsgBundle smb : soyMsgBundles) { for (final Iterator<SoyMsg> it = smb.iterator(); it.hasNext();) { msgs.add(it.next()); } } return Optional.of(new SoyMsgBundleImpl(locale.toString(), msgs)); } public void setMessagesPath(final String messagesPath) { this.messagesPath = messagesPath; } public void setHotReloadMode(final boolean hotReloadMode) { this.hotReloadMode = hotReloadMode; } public void setFallbackToEnglish(boolean fallbackToEnglish) { this.fallbackToEnglish = fallbackToEnglish; } public String getMessagesPath() { return messagesPath; } public boolean isHotReloadMode() { return hotReloadMode; } private boolean isHotReloadModeOff() { return !hotReloadMode; } public boolean isFallbackToEnglish() { return fallbackToEnglish; } }