package ameba.mvc.template.internal;
import ameba.exception.AmebaException;
import ameba.mvc.template.TemplateException;
import ameba.util.IOUtils;
import com.google.common.base.Charsets;
import com.google.common.collect.Collections2;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.glassfish.hk2.api.ServiceLocator;
import org.glassfish.jersey.internal.util.PropertiesHelper;
import org.glassfish.jersey.internal.util.ReflectionHelper;
import org.glassfish.jersey.internal.util.collection.DataStructures;
import org.glassfish.jersey.internal.util.collection.Value;
import org.glassfish.jersey.server.mvc.MvcFeature;
import org.glassfish.jersey.server.mvc.Viewable;
import org.glassfish.jersey.server.mvc.internal.LocalizationMessages;
import org.glassfish.jersey.server.mvc.spi.TemplateProcessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.inject.Singleton;
import javax.ws.rs.container.ResourceInfo;
import javax.ws.rs.core.*;
import java.io.*;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.*;
import java.util.concurrent.ConcurrentMap;
/**
* <p>Abstract AbstractTemplateProcessor class.</p>
*
* @author icode
*
*/
@Singleton
public abstract class AbstractTemplateProcessor<T> implements TemplateProcessor<T> {
/**
* Constant <code>TEMPLATE_CONF_PREFIX="template."</code>
*/
public static final String TEMPLATE_CONF_PREFIX = "template.";
private static Logger logger = LoggerFactory.getLogger(AbstractTemplateProcessor.class);
private final ConcurrentMap<String, T> cache;
private final String suffix;
private final Configuration config;
private final String[] basePath;
private final Charset encoding;
private final Set<String> supportedExtensions;
@Context
private ResourceInfo resourceInfo;
/**
* <p>Constructor for AbstractTemplateProcessor.</p>
*
* @param config a {@link javax.ws.rs.core.Configuration} object.
* @param propertySuffix a {@link java.lang.String} object.
* @param supportedExtensions a {@link java.lang.String} object.
*/
public AbstractTemplateProcessor(Configuration config, String propertySuffix, String... supportedExtensions) {
this.config = config;
this.suffix = '.' + propertySuffix;
Map<String, Object> properties = config.getProperties();
String basePath = TemplateHelper.getBasePath(properties, propertySuffix);
Collection<String> basePaths = TemplateHelper.getBasePaths(basePath);
this.basePath = basePaths.toArray(new String[basePaths.size()]);
Boolean cacheEnabled = PropertiesHelper.getValue(properties,
MvcFeature.CACHE_TEMPLATES + this.suffix, Boolean.class, null);
if (cacheEnabled == null) {
cacheEnabled = PropertiesHelper.getValue(properties, MvcFeature.CACHE_TEMPLATES, false, null);
}
this.cache = cacheEnabled ? DataStructures.createConcurrentMap() : null;
this.encoding = TemplateHelper.getTemplateOutputEncoding(config, this.suffix);
this.supportedExtensions = Sets.newHashSet(Collections2.transform(
Arrays.asList(supportedExtensions), input -> {
input = input.toLowerCase();
return input.startsWith(".") ? input : "." + input;
}));
}
/**
* <p>Getter for the field <code>basePath</code>.</p>
*
* @return an array of {@link java.lang.String} objects.
*/
protected String[] getBasePath() {
return this.basePath;
}
private Collection<String> getTemplatePaths(String name) {
Set<String> paths = Sets.newLinkedHashSet();
for (String path : basePath) {
paths.addAll(getTemplatePaths(name, path));
}
return paths;
}
private Collection<String> getTemplatePaths(String name, String basePath) {
String lowerName = name.toLowerCase();
final String templatePath = basePath.endsWith("/") ? basePath + name.substring(1) : basePath + name;
// Check whether the given name ends with supported suffix.
for (final String extension : supportedExtensions) {
if (lowerName.endsWith(extension)) {
return Collections.singleton(templatePath);
}
}
return Collections2.transform(this.supportedExtensions, input -> templatePath + input);
}
/**
* <p>getTemplateObjectFactory.</p>
*
* @param serviceLocator a {@link org.glassfish.hk2.api.ServiceLocator} object.
* @param type a {@link java.lang.Class} object.
* @param defaultValue a {@link org.glassfish.jersey.internal.util.collection.Value} object.
* @param <F> a F object.
* @return a F object.
*/
@SuppressWarnings("unchecked")
protected <F> F getTemplateObjectFactory(ServiceLocator serviceLocator, Class<F> type, Value<F> defaultValue) {
Object objectFactoryProperty = this.config.getProperty(MvcFeature.TEMPLATE_OBJECT_FACTORY + this.suffix);
if (objectFactoryProperty != null) {
if (type.isAssignableFrom(objectFactoryProperty.getClass())) {
return type.cast(objectFactoryProperty);
}
Class<F> factoryClass = null;
if (objectFactoryProperty instanceof String) {
factoryClass = ReflectionHelper.<F>classForNamePA((String) objectFactoryProperty).run();
} else if (objectFactoryProperty instanceof Class) {
factoryClass = (Class<F>) objectFactoryProperty;
}
if (factoryClass != null) {
if (type.isAssignableFrom(factoryClass)) {
return serviceLocator.create(factoryClass);
}
logger.warn(LocalizationMessages.WRONG_TEMPLATE_OBJECT_FACTORY(factoryClass, type));
}
}
return defaultValue.get();
}
/**
* <p>setContentType.</p>
*
* @param mediaType a {@link javax.ws.rs.core.MediaType} object.
* @param httpHeaders a {@link javax.ws.rs.core.MultivaluedMap} object.
* @return a {@link java.nio.charset.Charset} object.
* @since 0.1.6e
*/
protected Charset setContentType(MediaType mediaType, MultivaluedMap<String, Object> httpHeaders) {
String charset = mediaType.getParameters().get("charset");
Charset encoding;
MediaType finalMediaType;
if (charset == null) {
encoding = this.getEncoding();
HashMap<String, String> typeList = Maps.newHashMap(mediaType.getParameters());
typeList.put("charset", encoding.name());
finalMediaType = new MediaType(mediaType.getType(), mediaType.getSubtype(), typeList);
} else {
try {
encoding = Charset.forName(charset);
} catch (Exception e) {
encoding = Charsets.UTF_8;
}
finalMediaType = mediaType;
}
List<Object> typeList = Lists.newArrayListWithCapacity(1);
typeList.add(finalMediaType.toString());
httpHeaders.put("Content-Type", typeList);
return encoding;
}
/**
* <p>Getter for the field <code>encoding</code>.</p>
*
* @return a {@link java.nio.charset.Charset} object.
* @since 0.1.6e
*/
protected Charset getEncoding() {
return this.encoding;
}
/**
* <p>resolveJarFile.</p>
*
* @return a {@link java.lang.String} object.
*/
protected String resolveJarFile() {
Class resourceClass = resourceInfo.getResourceClass();
if (resourceClass == null) return null;
String classFile = resourceClass.getName().replace(".", "/") + ".class";
URL classUrl = IOUtils.getResource(classFile);
String urlProtocol = classUrl.getProtocol();
if (urlProtocol.equals("jar")) {
String path = classUrl.getPath();
return path.substring(0, path.length() - classFile.length() - 1);
}
return null;
}
/**
* <p>getNearTemplateURL.</p>
*
* @param jarFile a {@link java.lang.String} object.
* @param template a {@link java.lang.String} object.
* @return a {@link java.net.URL} object.
*/
protected URL getNearTemplateURL(String jarFile, String template) {
if (jarFile != null) {
Enumeration<URL> urls = IOUtils.getResources(template);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
if (url.getPath().startsWith(jarFile)) {
return url;
}
}
}
return null;
}
private T resolve(String name) {
Collection<String> tpls = this.getTemplatePaths(name);
Iterator iterator = tpls.iterator();
String jarFile = resolveJarFile();
String template;
InputStreamReader reader = null;
URL url = null;
do {
if (!iterator.hasNext()) {
break;
}
template = (String) iterator.next();
url = getNearTemplateURL(jarFile, template);
reader = resolveReader(url);
} while (reader == null);
if (reader == null) {
iterator = tpls.iterator();
do {
if (!iterator.hasNext()) {
return null;
}
template = (String) iterator.next();
url = IOUtils.getResource(template);
reader = resolveReader(url);
} while (reader == null);
}
try {
return this.resolve(url, reader);
} catch (Exception e) {
RuntimeException r;
try {
r = createException(e, null);
} catch (Exception ex) {
if (ex instanceof AmebaException) {
r = (RuntimeException) ex;
} else {
r = new TemplateException("create resolve Exception error", ex, -1);
}
}
throw r;
} finally {
IOUtils.closeQuietly(reader);
}
}
/**
* <p>resolveReader.</p>
*
* @param url a {@link java.net.URL} object.
* @return a {@link java.io.InputStreamReader} object.
*/
protected InputStreamReader resolveReader(URL url) {
if (url != null) {
try {
InputStream in = url.openStream();
if (in != null)
return new InputStreamReader(in, this.encoding);
} catch (IOException e) {
logger.warn("resolve template error", e);
}
}
return null;
}
/** {@inheritDoc} */
@Override
public T resolve(String name, MediaType mediaType) {
if (this.cache != null) {
if (!this.cache.containsKey(name)) {
T t = this.resolve(name);
if (t != null)
this.cache.putIfAbsent(name, t);
}
return this.cache.get(name);
} else {
return this.resolve(name);
}
}
/**
* <p>createException.</p>
*
* @param e a {@link java.lang.Exception} object.
* @param template a T object.
* @return a {@link ameba.mvc.template.TemplateException} object.
*/
protected abstract TemplateException createException(Exception e, T template);
/**
* <p>resolve.</p>
*
* @param templateURL a {@link java.lang.String} object.
* @param reader a {@link java.io.Reader} object.
* @return a T object.
* @throws java.lang.Exception if any.
*/
protected abstract T resolve(URL templateURL, Reader reader) throws Exception;
/** {@inheritDoc} */
@Override
public void writeTo(T templateReference, Viewable viewable, MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders, OutputStream out) throws IOException {
MediaType m = (MediaType) httpHeaders.getFirst(HttpHeaders.CONTENT_TYPE);
if (m == null) m = mediaType;
setContentType(m, httpHeaders);
try {
writeTemplate(templateReference, viewable, mediaType, httpHeaders, out);
} catch (Exception e) {
RuntimeException r;
try {
r = createException(e, templateReference);
} catch (Exception ex) {
if (ex instanceof AmebaException) {
r = (RuntimeException) ex;
} else {
r = new TemplateException("create writeTo Exception error", e, -1);
}
}
throw r;
}
}
/**
* <p>writeTemplate.</p>
*
* @param templateReference a T object.
* @param viewable a {@link org.glassfish.jersey.server.mvc.Viewable} object.
* @param mediaType a {@link javax.ws.rs.core.MediaType} object.
* @param httpHeaders a {@link javax.ws.rs.core.MultivaluedMap} object.
* @param out a {@link java.io.OutputStream} object.
* @throws java.lang.Exception if any.
*/
public abstract void writeTemplate(T templateReference, Viewable viewable, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream out) throws Exception;
}