/*
* The MIT License
*
* Copyright 2013 Tim Boudreau.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.mastfrog.giulius.annotations.processors;
import com.mastfrog.guicy.annotations.Defaults;
import com.mastfrog.guicy.annotations.Namespace;
import com.mastfrog.settings.SettingsBuilder;
import java.io.ByteArrayInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Completion;
import javax.annotation.processing.FilerException;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
import org.openide.util.lookup.ServiceProvider;
/**
* Processes the @Defaults annotation, generating properties files in the
* location specified by the annotation (the default is
* com/mastfrog/defaults.properties).
* <p/>
* Keep this in a separate package so it can be detached from this JAR
*
* @author Tim Boudreau
*/
@ServiceProvider(service = Processor.class)
@SupportedAnnotationTypes("com.mastfrog.guicy.annotations.Defaults")
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class DefaultsAnnotationProcessor extends IndexGeneratingProcessor {
private String findPath(Defaults anno, Element e) {
boolean specifiesNamespace;
Namespace namespaceAnno = anno.namespace();
if (Namespace.DEFAULT.equals(namespaceAnno.value())) {
namespaceAnno = findNamespace(e);
}
String namespace = namespaceAnno.value();
specifiesNamespace = !Namespace.DEFAULT.equals(namespace);
String path = anno.location();
String generatedDefaults = SettingsBuilder.GENERATED_PREFIX + SettingsBuilder.DEFAULT_NAMESPACE;
if (Defaults.DEFAULT_LOCATION.equals(path) && specifiesNamespace) {
path = namespace;
if (!path.startsWith(SettingsBuilder.GENERATED_PREFIX)) {
path = SettingsBuilder.GENERATED_PREFIX + path;
}
if (path.length() > 0 && path.charAt(0) != '/') {
path = Defaults.DEFAULT_PATH + path;
}
path += SettingsBuilder.DEFAULT_EXTENSION;
} else if (!generatedDefaults.equals(path) && specifiesNamespace) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, e.asType()
+ " or its package specifies the namespace \""
+ namespace + "\", but location=\"" + path + "\" "
+ "overrides it.", e);
}
return path;
}
private Properties findProperties(Map<String, Properties> propertiesForPath, String path) throws IOException {
Properties props = propertiesForPath.get(path);
if (props == null) {
props = new Properties();
try {
FileObject fo = processingEnv.getFiler().getResource(
StandardLocation.CLASS_OUTPUT,
"", path);
if (fo != null) {
try (InputStream in = fo.openInputStream()) {
props.load(in);
}
}
} catch (FileNotFoundException ex) {
//OK
}
propertiesForPath.put(path, props);
}
return props;
}
@SuppressWarnings("AnnotationAsSuperInterface")
private static final class DefaultNamespace implements Namespace {
@Override
public String value() {
return SettingsBuilder.GENERATED_PREFIX + Namespace.DEFAULT;
}
@Override
public Class<? extends Annotation> annotationType() {
return Namespace.class;
}
}
private Namespace findNamespace(Element element) {
Namespace result;
//get the package before we change the value of element
PackageElement pe = processingEnv.getElementUtils().getPackageOf(element);
do {
//search up enclosing class/method/whatever
result = element.getAnnotation(Namespace.class);
element = element.getEnclosingElement();
} while (result == null && element != null);
//element.getEnclosingElement() doens't actually make the leap from
//class -> package, so now search the package name hierarchy
//(javac doesn't see packages as having hierarchy - they might
//be in different jars)
while (pe != null && result == null && !pe.isUnnamed()) {
result = pe.getAnnotation(Namespace.class);
if (result == null) {
CharSequence name = pe.getQualifiedName();
int ix = name.toString().lastIndexOf('.');
if (ix > 0) {
name = name.subSequence(0, ix - 1);
pe = processingEnv.getElementUtils().getPackageElement(name);
result = pe == null ? null : pe.getAnnotation(Namespace.class);
} else {
break;
}
}
}
return result == null ? new DefaultNamespace() : result;
}
Set<Element> elements = new HashSet<>();
Map<String, Properties> propertiesForPath = new HashMap<>();
Map<String, List<Element>> elementForPath = new HashMap<>();
@Override
public boolean handleProcess(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
boolean result = false;
try {
Set<? extends Element> all = roundEnv.getElementsAnnotatedWith(Defaults.class);
for (Element e : all) {
Defaults anno = e.getAnnotation(Defaults.class);
if (anno == null) {
continue;
}
String path = findPath(anno, e);
Properties props = findProperties(propertiesForPath, path);
List<Element> els = elementForPath.get(path);
if (els == null) {
els = new ArrayList<>();
elementForPath.put(path, els);
}
els.add(e);
for (String pair : anno.value()) {
Properties pp = new Properties();
try (ByteArrayInputStream in = new ByteArrayInputStream(pair.getBytes("UTF-8"))) {
pp.load(in);
}
if (!pp.isEmpty()) {
elements.add(e);
}
for (String s : pp.stringPropertyNames()) {
if (props.containsKey(s)) {
String old = props.getProperty(s);
String nue = pp.getProperty(s);
if (!Objects.equals(old, nue)) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, e.asType() + " redefines " + s + " from " + old + " to " + nue, e);
}
}
}
props.putAll(pp);
addLine(path, pair, e);
}
}
} catch (IOException ex) {
Logger.getLogger(DefaultsAnnotationProcessor.class.getName()).log(Level.SEVERE, null, ex);
return false;
}
return result;
}
@Override
public Iterable<? extends Completion> getCompletions(Element element, AnnotationMirror annotation, ExecutableElement member, String userText) {
return Collections.<Completion>emptySet();
}
}