/*
* Copyright 2013 Blazebit.
*
* 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.blazebit.message.apt;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.ElementKind;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic.Kind;
import javax.tools.FileObject;
import javax.tools.JavaFileObject;
import javax.tools.StandardLocation;
import org.apache.deltaspike.core.api.message.MessageBundle;
import com.blazebit.i18n.LocaleUtils;
import freemarker.cache.ClassTemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.ObjectWrapper;
import freemarker.template.Template;
/**
*
* @author Christian
*/
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public abstract class MessageBundleProcessor extends AbstractInterfaceProcessor<MessageBundleElementInfo, MessageBundleInfo2> {
@Override
protected Set<Class<? extends Annotation>> getAnnotationsToProcess() {
Set<Class<? extends Annotation>> classes = new HashSet<Class<? extends Annotation>>();
classes.add(MessageBundle.class);
return classes;
}
@Override
protected MessageBundleInfo2 processElement(InterfaceInfo<MessageBundleElementInfo> interfaceInfo, Class<? extends Annotation> annotationClass) {
if (interfaceInfo.getElement().getKind() != ElementKind.INTERFACE) {
processingEnv.getMessager().printMessage(Kind.ERROR, "The annotation '" + annotationClass.getName() + "' can only be applied on interfaces!", interfaceInfo.getElement());
}
TypeMirror serializableType = processingEnv.getElementUtils().getTypeElement(Serializable.class.getName()).asType();
TypeMirror interfaceType = interfaceInfo.getElement().asType();
if (!processingEnv.getTypeUtils().isSubtype(interfaceType, serializableType)) {
processingEnv.getMessager().printMessage(Kind.ERROR, "The message bundle interface must extend java.io.Serializable!", interfaceInfo.getElement());
}
String qualifiedEnumClassName = getQualifiedEnumClassName(interfaceInfo);
String simpleEnumClassName = getSimpleEnumClassName(interfaceInfo);
String propertiesBasePath = getPropertiesBasePath(interfaceInfo);
String propertiesBaseName = getPropertiesBaseName(interfaceInfo);
String templateLocation = getTemplateLocation(interfaceInfo);
Collection<Locale> locales = getLocales(interfaceInfo);
MessageBundleInfo2 messageBundleInfo = new MessageBundleInfo2(interfaceInfo, qualifiedEnumClassName, simpleEnumClassName, propertiesBasePath, propertiesBaseName, templateLocation, locales);
validatePropertiesFiles(messageBundleInfo);
return messageBundleInfo;
}
protected File getResourceFile(String propertiesBasePath, String propertiesFileName) {
try {
FileObject fileObject = processingEnv.getFiler().getResource(StandardLocation.CLASS_OUTPUT, propertiesBasePath, propertiesFileName);
return new File(fileObject.toUri());
} catch(FileNotFoundException ex) {
throw new IllegalArgumentException("Could not find the properties file '" + propertiesFileName + "' at the location '" + propertiesBasePath + "'!", ex);
} catch (IOException ex) {
throw new IllegalArgumentException("Could not load the properties file '" + propertiesFileName + "' from the location '" + propertiesBasePath + "'!", ex);
}
}
protected String getPropertiesFileName(String propertiesBaseName, Locale locale) {
StringBuilder sb = new StringBuilder(propertiesBaseName);
if (locale.getLanguage() != null && !locale.getLanguage().isEmpty()) {
sb.append('_').append(locale.getLanguage());
}
if (locale.getCountry() != null && !locale.getCountry().isEmpty()) {
sb.append('_').append(locale.getCountry());
}
sb.append(".properties");
return sb.toString();
}
protected String getQualifiedEnumClassName(InterfaceInfo<MessageBundleElementInfo> interfaceInfo) {
return interfaceInfo.getQualifiedName() + "Enum";
}
protected String getSimpleEnumClassName(InterfaceInfo<MessageBundleElementInfo> interfaceInfo) {
return interfaceInfo.getSimpleName() + "Enum";
}
protected String getTemplateLocation(InterfaceInfo<MessageBundleElementInfo> interfaceInfo) {
MessageBundleConfig config = interfaceInfo.getElement().getAnnotation(MessageBundleConfig.class);
return config.templateLocation();
}
protected String getPropertiesBasePath(InterfaceInfo<MessageBundleElementInfo> interfaceInfo) {
MessageBundleConfig config = interfaceInfo.getElement().getAnnotation(MessageBundleConfig.class);
String basePath = config.base();
if (basePath.isEmpty()) {
basePath = interfaceInfo.getQualifiedName().replaceAll("\\.", "/");
}
int slashIndex = basePath.lastIndexOf('/');
if (slashIndex == -1) {
return "";
}
return basePath.substring(0, slashIndex);
}
protected String getPropertiesBaseName(InterfaceInfo<MessageBundleElementInfo> interfaceInfo) {
MessageBundleConfig config = interfaceInfo.getElement().getAnnotation(MessageBundleConfig.class);
String basePath = config.base();
if (basePath.isEmpty()) {
basePath = interfaceInfo.getQualifiedName().replaceAll("\\.", "/");
}
int slashIndex = basePath.lastIndexOf('/');
if (slashIndex == -1) {
return basePath;
}
return basePath.substring(slashIndex + 1);
}
protected Collection<Locale> getLocales(InterfaceInfo<MessageBundleElementInfo> interfaceInfo) {
MessageBundleConfig config = interfaceInfo.getElement().getAnnotation(MessageBundleConfig.class);
Collection<Locale> locales = new ArrayList<Locale>(config.locales().length);
for (String localeString : config.locales()) {
locales.add(LocaleUtils.getLocale(localeString));
}
return locales;
}
@Override
protected MessageBundleElementInfo processMethod(InterfaceMethodInfo methodInfo) {
String enumKey = getEnumKey(methodInfo);
MessageBundleElementInfo messageBundleElementInfo = new MessageBundleElementInfo(methodInfo, enumKey);
if (validateMethod(messageBundleElementInfo)) {
return messageBundleElementInfo;
}
return null;
}
protected boolean validateMethod(MessageBundleElementInfo messageBundleElementInfo) {
String returnTypeName = messageBundleElementInfo.getElement().getReturnType().toString();
String expectedReturnTypeName = String.class.getName();
if (!messageBundleElementInfo.getName().startsWith("get") || !returnTypeName.equals(expectedReturnTypeName)) {
String msg = "Only getter methods with the return type '" + expectedReturnTypeName + "' are allowed!";
processingEnv.getMessager().printMessage(Kind.ERROR, msg, messageBundleElementInfo.getElement());
return false;
}
return true;
}
protected abstract String getEnumKey(InterfaceMethodInfo methodInfo);
@Override
protected void processInterfaceInfo(MessageBundleInfo2 messageBundleInfo) {
File enumClassJavaFile = getJavaSourceFile(messageBundleInfo.getPackageName(), messageBundleInfo.getSimpleEnumClassName());
if (enumClassJavaFile.lastModified() == messageBundleInfo.getLastModified()) {
// Skip unchanged files
return;
}
Writer writer = null;
try {
JavaFileObject jfo = processingEnv.getFiler().createSourceFile(messageBundleInfo.getQualifiedEnumClassName());
writer = jfo.openWriter();
generateEnumClass(messageBundleInfo, writer);
} catch (Exception ex) {
String msg = "Error while generating enum class!";
printMessage(Kind.ERROR, msg, messageBundleInfo.getElement(), ex);
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
// Ignore
}
}
}
enumClassJavaFile.setLastModified(messageBundleInfo.getLastModified());
}
protected void generateEnumClass(MessageBundleInfo2 info, Writer writer) throws Exception {
Map<String, Object> parameters = getTemplateParameters(info);
Template template = getTemplate(info);
template.process(parameters, writer);
}
protected Template getTemplate(MessageBundleInfo2 info) throws IOException {
final Configuration configuration = new Configuration();
configuration.setTemplateLoader(new ClassTemplateLoader(MessageBundleProcessor.class, "/"));
configuration.setAutoFlush(true);
configuration.setObjectWrapper(ObjectWrapper.BEANS_WRAPPER);
return configuration.getTemplate(info.getTemplateLocation());
}
protected Map<String, Object> getTemplateParameters(MessageBundleInfo2 info) {
Map<String, Object> parameters = new HashMap<String, Object>();
parameters.put("packageName", info.getPackageName());
parameters.put("baseName", new StringBuilder(info.getPropertiesBasePath()).append('/').append(info.getPropertiesBaseName()));
parameters.put("enumName", info.getSimpleEnumClassName());
List<String> locales = new ArrayList<String>(info.getLocales().size());
for (Locale locale : info.getLocales()) {
locales.add(locale.toString());
}
Collections.sort(locales);
parameters.put("locales", locales);
List<String> keys = new ArrayList<String>(info.getInterfaceMethodInfos().size());
for (MessageBundleElementInfo elementInfo : info.getInterfaceMethodInfos()) {
keys.add(elementInfo.getEnumKey());
}
Collections.sort(keys);
parameters.put("keys", keys);
return parameters;
}
protected void validatePropertiesFiles(MessageBundleInfo2 messageBundleInfo) {
for (Locale locale : messageBundleInfo.getLocales()) {
String propertiesFileName = getPropertiesFileName(messageBundleInfo.getPropertiesBaseName(), locale);
try {
File propertiesFile = getResourceFile(messageBundleInfo.getPropertiesBasePath(), propertiesFileName);
Properties properties = new Properties();
properties.load(new InputStreamReader(new FileInputStream(propertiesFile), "UTF-8"));
for (MessageBundleElementInfo elementInfo : messageBundleInfo.getInterfaceMethodInfos()) {
String enumKey = elementInfo.getEnumKey();
String propertiesValue = properties.remove(enumKey).toString();
if (propertiesValue == null) {
String msg = "The entry for the enum key '" + enumKey + "' is missing in the properties file '" + propertiesFileName + "'!";
processingEnv.getMessager().printMessage(Kind.ERROR, msg, elementInfo.getElement());
} else {
validatePropertiesFileEntry(elementInfo, locale, propertiesValue);
}
}
for (Object propertiesKey : properties.keySet()) {
String msg = "The entry '" + propertiesKey + "' in the properties file '" + propertiesFileName + "' has no corresponding enum key!";
processingEnv.getMessager().printMessage(Kind.ERROR, msg, messageBundleInfo.getElement());
}
} catch (Exception ex) {
printMessage(Kind.ERROR, "Error while loading properties files.", messageBundleInfo.getElement(), ex);
}
}
}
protected void validatePropertiesFileEntry(MessageBundleElementInfo elementInfo, Locale locale, String propertiesValue) {
int methodParameterCount = elementInfo.getQualifiedParameterTypeNames().size();
int propertiesValueParameterCount = getParameterCount(propertiesValue);
if (methodParameterCount != propertiesValueParameterCount) {
String msg = "The method accepts " + methodParameterCount + " parameters, but the properties entry '"
+ elementInfo.getEnumKey() + "' for the locale '" + locale.toString() + "' requires " + propertiesValueParameterCount + " parameters!";
processingEnv.getMessager().printMessage(Kind.ERROR, msg, elementInfo.getElement());
}
}
private static final Pattern PROPERTY_VALUE_PARAMETER_PATTERN = Pattern.compile("\\{(.*?)\\}");
private static int getParameterCount(final String value) {
int count = 0;
if (value != null) {
final Matcher matcher = PROPERTY_VALUE_PARAMETER_PATTERN.matcher(value);
while (matcher.find()) {
count++;
}
}
return count;
}
}