/*
* Copyright 2008-2011 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.nominanuda.springsoy;
import static com.nominanuda.zen.common.Str.STR;
import java.io.BufferedReader;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.StringReader;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
import com.google.template.soy.SoyFileSet;
import com.google.template.soy.data.SoyMapData;
import com.google.template.soy.jssrc.SoyJsSrcOptions;
import com.google.template.soy.msgs.SoyMsgBundle;
import com.google.template.soy.msgs.SoyMsgBundleHandler;
import com.google.template.soy.msgs.SoyMsgException;
import com.google.template.soy.tofu.SoyTofu;
import com.google.template.soy.xliffmsgplugin.XliffMsgPlugin;
import com.nominanuda.zen.common.Check;
import com.nominanuda.zen.obj.Obj;
public class SoySource {
private final static Pattern FUNC_DECL = Pattern.compile("^\\s*([\\.\\w]+)\\s*=\\s*function\\s*\\(");
private final static String NULL_LANG_KEY = "NO LANGUAGE";
protected final Logger log = LoggerFactory.getLogger(getClass());
private boolean cache = true;
private String bundleUrlPrefix;
private Resource templatesLocation;
private ConcurrentHashMap<String, Map<String, String>> jsTemplatesCache = new ConcurrentHashMap<>();
private ConcurrentHashMap<String, SoyTofu> tofuCache = new ConcurrentHashMap<>();
private Set<String> functionNames = new HashSet<>();
private boolean i18n = false;
private String i18nXlfUrl;
private Set<String> skipLangs = Collections.emptySet();
public String getJsTemplate(String name, @Nullable String lang) throws IOException {
lang = Check.ifNull(lang, NULL_LANG_KEY);
if (jsTemplatesCache.get(lang) == null || !cache) {
compile(lang);
}
Map<String, String> tpls = jsTemplatesCache.get(lang);
String tpl = tpls.get(name);
if (tpl == null) {
throw new IOException("not found template named "+name);
}
return tpl;
}
public boolean hasFunction(String name, @Nullable String lang) throws IOException {
lang = Check.ifNull(lang, NULL_LANG_KEY);
if (jsTemplatesCache.get(lang) == null || !cache) {
compile(lang);
}
return functionNames.contains(name);
}
public SoyTofu getSoyTofu(@Nullable String lang) throws IOException {
lang = Check.ifNull(lang, NULL_LANG_KEY);
if (tofuCache.get(lang) == null || !cache) {
compile(lang);
}
return tofuCache.get(lang);
}
/**
* simple helper method to quickly render a template
* @param model
* @param view
* @return
* @throws IOException
*/
public String render(SoyMapData model, String view) throws IOException {
return getSoyTofu(null).newRenderer(view).setData(model).render();
}
public String render(Map<String, ?> model, String view) throws IOException {
return getSoyTofu(null).newRenderer(view).setData(model).render();
}
public String render(Obj model, String view) throws IOException {
return render(SoyHelper.model2soy(model), view);
}
protected void cumulate(SoyFileSet.Builder builder, List<String> jsTplNames) throws IOException {
File[] templateFiles = templatesLocation.getFile().listFiles(new FilenameFilter() {
public boolean accept(File dir, String name) {
return name.endsWith(".soy");
}
});
for (File templateFile: templateFiles) {
builder.add(templateFile);
jsTplNames.add(templateFile.getName());
}
}
private void compile(String lang) throws IOException {
final List<String> jsTplNames = new LinkedList<String>();
final SoyFileSet.Builder builder = new SoyFileSet.Builder();
cumulate(builder, jsTplNames);
SoyFileSet soyFileSet = builder.build();
List<String> jsTpls = soyFileSet.compileToJsSrc(new SoyJsSrcOptions(), getBundle(lang));
for(String jsSrc : jsTpls) {
BufferedReader br = new BufferedReader(new StringReader(jsSrc));
String line;
while ((line = br.readLine()) != null) {
Matcher m = FUNC_DECL.matcher(line);
if(m.find()) {
functionNames.add(m.group(1));
}
}
}
Map<String, String> jsTplMap = new HashMap<String, String>();
int len = jsTplNames.size();
for(int i = 0; i < len; i++) {
jsTplMap.put(jsTplNames.get(i), jsTpls.get(i));
}
jsTemplatesCache.put(lang, jsTplMap);
tofuCache.put(lang, soyFileSet.compileToTofu());
}
public @Nullable SoyMsgBundle getBundle(String lang) throws SoyMsgException, IOException {
if (!i18n || lang == null || skipLangs.contains(lang) || bundleUrlPrefix == null) {
return null;
} else {
SoyMsgBundleHandler msgBundleHandler = new SoyMsgBundleHandler(new XliffMsgPlugin());
return msgBundleHandler.createFromResource(new URL(STR.joinArgs(".", Check.ifNull(bundleUrlPrefix, i18nXlfUrl), lang, "xlf")));
}
}
/* setters */
public void setTemplatesLocation(Resource templatesLocation) {
this.templatesLocation = templatesLocation;
}
public void setCache(boolean cache) {
this.cache = cache;
}
public void setBundleUrlPrefix(String bundleUrlPrefix) {
this.bundleUrlPrefix = bundleUrlPrefix;
}
/**
*
* @param i18nXlfBaseUrl e.g. classpath:/com/foo/messages which for Locale en becomes classpath:/com/foo/messages.en.xlf
*/
public void setI18nXlfBaseUrl(String i18nXlfBaseUrl) {
this.i18nXlfUrl = i18nXlfBaseUrl;
}
/**
*
* @param skipLangs list of locales to skip (i.e. not to translate) comma separated
*/
public void setSkipLangs(String skipLangs) {
this.skipLangs = new HashSet<String>(STR.splitAndTrim(skipLangs, ","));
}
/**
* @param i18n enable translation, default is false
*/
public void setI18n(boolean i18n) {
this.i18n = i18n;
}
}