// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2016 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
package com.google.appinventor.components.scripts;
import com.google.appinventor.components.annotations.DesignerComponent;
import com.google.appinventor.components.annotations.DesignerProperty;
import com.google.appinventor.components.annotations.PropertyCategory;
import com.google.appinventor.components.annotations.SimpleEvent;
import com.google.appinventor.components.annotations.SimpleFunction;
import com.google.appinventor.components.annotations.SimpleObject;
import com.google.appinventor.components.annotations.SimpleProperty;
import com.google.appinventor.components.annotations.SimpleBroadcastReceiver;
import com.google.appinventor.components.annotations.UsesAssets;
import com.google.appinventor.components.annotations.UsesLibraries;
import com.google.appinventor.components.annotations.UsesNativeLibraries;
import com.google.appinventor.components.annotations.UsesPermissions;
import com.google.appinventor.components.annotations.UsesActivities;
import com.google.appinventor.components.annotations.UsesBroadcastReceivers;
import com.google.appinventor.components.annotations.androidmanifest.ActivityElement;
import com.google.appinventor.components.annotations.androidmanifest.ReceiverElement;
import com.google.appinventor.components.annotations.androidmanifest.IntentFilterElement;
import com.google.appinventor.components.annotations.androidmanifest.MetaDataElement;
import com.google.appinventor.components.annotations.androidmanifest.ActionElement;
import com.google.appinventor.components.annotations.androidmanifest.DataElement;
import com.google.appinventor.components.annotations.androidmanifest.CategoryElement;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
/**
* Processor for generating output files based on the annotations and
* javadoc in the component source code.
* <p>
* Specifically, this reads over the source files, building up a representation
* of components and their designer properties, properties, methods, and
* events. Concrete subclasses implement the method {@link #outputResults()}
* to generate output.
* <p>
* Currently, the following annotations are used:
* <ul>
* <li> {@link DesignerComponent} and {@link SimpleObject} to identify
* components. Subclasses can distinguish between the two through
* the boolean fields
* {@link ComponentProcessor.ComponentInfo#designerComponent} and
* {@link ComponentProcessor.ComponentInfo#simpleObject}.
* <li> {@link DesignerProperty} to identify designer properties.
* <li> {@link SimpleProperty} to identify properties.
* <li> {@link SimpleFunction} to identify methods.
* <li> {@link SimpleEvent} to identify events.
* </ul>
*
* @author spertus@google.com (Ellen Spertus)
*
* [lyn, 2015/12/29] Added deprecated instance variable to ParameterizedFeature.
* This is inherited by Event, Method, and Property, which are modified
* slightly to handle it.
*
* [Will, 2016/9/20] Added methods to process annotations in the package
* com.google.appinventor.components.annotations.androidmanifest and the
* appropriate calls in {@link #processComponent(Element)}.
*/
public abstract class ComponentProcessor extends AbstractProcessor {
private static final String OUTPUT_PACKAGE = "";
// Returned by getSupportedAnnotationTypes()
private static final Set<String> SUPPORTED_ANNOTATION_TYPES = ImmutableSet.of(
"com.google.appinventor.components.annotations.DesignerComponent",
"com.google.appinventor.components.annotations.DesignerProperty",
"com.google.appinventor.components.annotations.SimpleEvent",
"com.google.appinventor.components.annotations.SimpleFunction",
"com.google.appinventor.components.annotations.SimpleObject",
"com.google.appinventor.components.annotations.SimpleProperty",
// TODO(Will): Remove the following string once the deprecated
// @SimpleBroadcastReceiver annotation is removed. It should
// should remain for the time being because otherwise we'll break
// extensions currently using @SimpleBroadcastReceiver.
"com.google.appinventor.components.annotations.SimpleBroadcastReceiver",
"com.google.appinventor.components.annotations.UsesAssets",
"com.google.appinventor.components.annotations.UsesLibraries",
"com.google.appinventor.components.annotations.UsesNativeLibraries",
"com.google.appinventor.components.annotations.UsesActivities",
"com.google.appinventor.components.annotations.UsesBroadcastReceivers",
"com.google.appinventor.components.annotations.UsesPermissions");
// Returned by getRwString()
private static final String READ_WRITE = "read-write";
private static final String READ_ONLY = "read-only";
private static final String WRITE_ONLY = "write-only";
// Must match buildserver.compiler.ARMEABI_V7A_SUFFIX
private static final String ARMEABI_V7A_SUFFIX = "-v7a";
// The next two fields are set in init().
/**
* A handle allowing access to facilities provided by the annotation
* processing tool framework
*/
private Elements elementUtils;
private Types typeUtils;
/**
* Produced through {@link ProcessingEnvironment#getMessager()} and
* used for outputing errors and warnings.
*/
// Set in process()
protected Messager messager;
/**
* Indicates which pass is being performed by the Java annotation processor
*/
private int pass = 0;
/**
* Information about every App Inventor component. Keys are fully-qualified names
* (such as "com.google.appinventor.components.runtime.components.android.Label"), and
* values are the corresponding {@link ComponentProcessor.ComponentInfo} objects.
* This is constructed by {@link #process} for use in {@link #outputResults()}.
*/
protected final SortedMap<String, ComponentInfo> components = Maps.newTreeMap();
private final List<String> componentTypes = Lists.newArrayList();
/**
* Represents a parameter consisting of a name and a type. The type is a
* String representation of the java type, such as "int", "double", or
* "java.lang.String".
*/
protected final class Parameter {
/**
* The parameter name
*/
protected final String name;
/**
* The parameter's Java type, such as "int" or "java.lang.String".
*/
protected final String type;
/**
* Constructs a Parameter.
*
* @param name the parameter name
* @param type the parameter's Java type (such as "int" or "java.lang.String")
*/
protected Parameter(String name, String type) {
this.name = name;
this.type = type;
}
/**
* Provides a Yail type for a given parameter type. This is useful because
* the parameter types used for {@link Event} are Simple types (e.g.,
* "Single"), while the parameter types used for {@link Method} are
* Java types (e.g., "int".
*
* @param parameter a parameter
* @return the string representation of the corresponding Yail type
* @throws RuntimeException if {@code parameter} does not have a
* corresponding Yail type
*/
protected String parameterToYailType(Parameter parameter) {
return javaTypeToYailType(type);
}
}
/**
* Represents a component feature that has a name and a description.
*/
protected abstract static class Feature {
protected final String name;
protected String description;
protected Feature(String name, String description, String featureType) {
this.name = name;
if (description == null || description.isEmpty()) {
this.description = featureType + " for " + name;
} else {
// Throw out the first @ or { and everything after it,
// in order to strip out @param, @author, {@link ...}, etc.
this.description = description.split("[@{]")[0].trim();
}
}
}
/**
* Represents a component feature that has a name, description, and
* parameters.
*/
protected abstract class ParameterizedFeature extends Feature {
// Inherits name, description
protected final List<Parameter> parameters;
protected final boolean userVisible;
protected final boolean deprecated; // [lyn, 2015/12/29] added
protected ParameterizedFeature(String name, String description, String feature,
boolean userVisible, boolean deprecated) {
super(name, description, feature);
this.userVisible = userVisible;
this.deprecated = deprecated;
parameters = Lists.newArrayList();
}
protected void addParameter(String name, String type) {
parameters.add(new Parameter(name, type));
}
/**
* Generates a comma-separated string corresponding to the parameter list,
* using Yail types (e.g., "number n, text t1").
*
* @return a string representation of the parameter list
* @throws RuntimeException if the parameter type cannot be mapped to any
* of the legal return values
*/
protected String toParameterString() {
StringBuilder sb = new StringBuilder();
int count = 0;
for (Parameter param : parameters) {
sb.append(param.parameterToYailType(param));
sb.append(" ");
sb.append(param.name);
if (++count != parameters.size()) {
sb.append(", ");
}
}
return new String(sb);
}
}
/**
* Represents an App Inventor event (annotated with {@link SimpleEvent}).
*/
protected final class Event extends ParameterizedFeature
implements Cloneable, Comparable<Event> {
// Inherits name, description, and parameters
protected Event(String name, String description, boolean userVisible, boolean deprecated) {
super(name, description, "Event", userVisible, deprecated);
}
@Override
public Event clone() {
Event that = new Event(name, description, userVisible, deprecated);
for (Parameter p : parameters) {
that.addParameter(p.name, p.type);
}
return that;
}
@Override
public int compareTo(Event e) {
return name.compareTo(e.name);
}
}
/**
* Represents an App Inventor component method (annotated with
* {@link SimpleFunction}).
*/
protected final class Method extends ParameterizedFeature
implements Cloneable, Comparable<Method> {
// Inherits name, description, and parameters
private String returnType;
protected Method(String name, String description, boolean userVisible, boolean deprecated) {
super(name, description, "Method", userVisible, deprecated);
// returnType defaults to null
}
protected String getReturnType() {
return returnType;
}
@Override
public Method clone() {
Method that = new Method(name, description, userVisible, deprecated);
for (Parameter p : parameters) {
that.addParameter(p.name, p.type);
}
that.returnType = returnType;
return that;
}
@Override
public int compareTo(Method f) {
return name.compareTo(f.name);
}
}
/**
* Represents an App Inventor component property (annotated with
* {@link SimpleProperty}).
*/
protected static final class Property implements Cloneable {
protected final String name;
private String description;
private PropertyCategory propertyCategory;
private boolean userVisible;
private boolean deprecated;
private String type;
private boolean readable;
private boolean writable;
private String componentInfoName;
protected Property(String name, String description,
PropertyCategory category, boolean userVisible, boolean deprecated) {
this.name = name;
this.description = description;
this.propertyCategory = category;
this.userVisible = userVisible;
this.deprecated = deprecated;
// type defaults to null
// readable and writable default to false
}
@Override
public Property clone() {
Property that = new Property(name, description, propertyCategory, userVisible, deprecated);
that.type = type;
that.readable = readable;
that.writable = writable;
that.componentInfoName = componentInfoName;
return that;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("<Property name: ");
sb.append(name);
sb.append(", type: ");
sb.append(type);
if (readable) {
sb.append(" readable");
}
if (writable) {
sb.append(" writable");
}
sb.append(">");
return sb.toString();
}
/**
* Returns the description of this property, as retrieved by
* {@link SimpleProperty#description()}.
*
* @return the description of this property
*/
protected String getDescription() {
return description;
}
/**
* Returns whether this property is visible in the Blocks Editor, as retrieved
* from {@link SimpleProperty#userVisible()}.
*
* @return whether the property is visible in the Blocks Editor
*/
protected boolean isUserVisible() {
return userVisible;
}
/**
* Returns whether this property is deprecated in the Blocks Editor.
*
* @return whether the property is visible in the Blocks Editor
*/
protected boolean isDeprecated() {
return deprecated;
}
/**
* Returns this property's Java type (e.g., "int", "double", or "java.lang.String").
*
* @return the feature's Java type
*/
protected String getType() {
return type;
}
/**
* Returns whether this property is readable (has a getter).
*
* @return whether this property is readable
*/
protected boolean isReadable() {
return readable;
}
/**
* Returns whether this property is writable (has a setter).
*
* @return whether this property is writable
*/
protected boolean isWritable() {
return writable;
}
/**
* Returns a string indicating whether this property is readable and/or
* writable.
*
* @return one of "read-write", "read-only", or "write-only"
* @throws {@link RuntimeException} if the property is neither readable nor
* writable
*/
protected String getRwString() {
if (readable) {
if (writable) {
return READ_WRITE;
} else {
return READ_ONLY;
}
} else {
if (!writable) {
throw new RuntimeException("Property " + name +
" is neither readable nor writable");
}
return WRITE_ONLY;
}
}
}
/**
* Represents an App Inventor component, including its designer properties,
* Simple properties, methods, and events.
*/
protected final class ComponentInfo extends Feature {
// Inherits name and description
/**
* Permissions required by this component.
* @see android.Manifest.permission
*/
protected final Set<String> permissions;
/**
* Libraries required by this component.
*/
protected final Set<String> libraries;
/**
* Native libraries required by this component.
*/
protected final Set<String> nativeLibraries;
/**
* Assets required by this component.
*/
protected final Set<String> assets;
/**
* Activities required by this component.
*/
protected final Set<String> activities;
/**
* Broadcast receivers required by this component.
*/
protected final Set<String> broadcastReceivers;
/**
* TODO(Will): Remove the following field once the deprecated {@link SimpleBroadcastReceiver}
* annotation is removed. It should should remain for the time being
* because otherwise we'll break extensions currently using it.
*
* Class Name and Filter Actions for a simple Broadcast Receiver
*/
protected final Set<String> classNameAndActionsBR;
/**
* Properties of this component that are visible in the Designer.
* @see DesignerProperty
*/
protected final SortedMap<String, DesignerProperty> designerProperties;
/**
* Properties of this component, whether or not they are visible in
* the Designer. The keys of this map are a superset of the keys of
* {@link #designerProperties}.
*/
protected final SortedMap<String, Property> properties;
/**
* Methods provided by this component.
*/
protected final SortedMap<String, Method> methods;
/**
* Events provided by this component.
*/
protected final SortedMap<String, Event> events;
/**
* Whether this component is abstract (such as
* {@link com.google.appinventor.components.runtime.Sprite}) or concrete.
*/
protected final boolean abstractClass;
/**
* The displayed name of this component. This is usually the same as the
* {@link Class#getSimpleName()}. The exception is for the component
* {@link com.google.appinventor.components.runtime.Form}, for which the
* name "Screen" is used.
*/
protected final String displayName;
protected final String type;
protected boolean external;
private String helpDescription; // Shorter popup description
private String helpUrl; // Custom help URL for extensions
private String category;
private String categoryString;
private boolean simpleObject;
private boolean designerComponent;
private int version;
private boolean showOnPalette;
private boolean nonVisible;
private String iconName;
protected ComponentInfo(Element element) {
super(element.getSimpleName().toString(), // Short name
elementUtils.getDocComment(element),
"Component");
type = element.asType().toString();
displayName = getDisplayNameForComponentType(name);
permissions = Sets.newHashSet();
libraries = Sets.newHashSet();
nativeLibraries = Sets.newHashSet();
assets = Sets.newHashSet();
activities = Sets.newHashSet();
broadcastReceivers = Sets.newHashSet();
classNameAndActionsBR = Sets.newHashSet();
designerProperties = Maps.newTreeMap();
properties = Maps.newTreeMap();
methods = Maps.newTreeMap();
events = Maps.newTreeMap();
abstractClass = element.getModifiers().contains(Modifier.ABSTRACT);
external = false;
for (AnnotationMirror am : element.getAnnotationMirrors()) {
DeclaredType dt = am.getAnnotationType();
String annotationName = am.getAnnotationType().toString();
if (annotationName.equals(SimpleObject.class.getName())) {
simpleObject = true;
SimpleObject simpleObjectAnnotation = element.getAnnotation(SimpleObject.class);
external = simpleObjectAnnotation.external();
}
if (annotationName.equals(DesignerComponent.class.getName())) {
designerComponent = true;
DesignerComponent designerComponentAnnotation =
element.getAnnotation(DesignerComponent.class);
// Override javadoc description with explicit description
// if provided.
String explicitDescription = designerComponentAnnotation.description();
if (!explicitDescription.isEmpty()) {
description = explicitDescription;
}
// Set helpDescription to the designerHelpDescription field if
// provided; otherwise, use description
helpDescription = designerComponentAnnotation.designerHelpDescription();
if (helpDescription.isEmpty()) {
helpDescription = description;
}
helpUrl = designerComponentAnnotation.helpUrl();
if (!helpUrl.startsWith("http:") && !helpUrl.startsWith("https:")) {
helpUrl = ""; // only accept http: or https: URLs (e.g., no javascript:)
}
category = designerComponentAnnotation.category().getName();
categoryString = designerComponentAnnotation.category().toString();
version = designerComponentAnnotation.version();
showOnPalette = designerComponentAnnotation.showOnPalette();
nonVisible = designerComponentAnnotation.nonVisible();
iconName = designerComponentAnnotation.iconName();
}
}
}
/**
* A brief description of this component to be shown when the user requests
* help in the Designer. This is obtained from the first of the following that
* was provided in the source code for the component:
* <ol>
* <li> {@link DesignerComponent#designerHelpDescription()}</li>
* <li> {@link DesignerComponent#description()}</li>
* <li> the Javadoc preceding the beginning of the class corresponding to the component</li>
* </ol>
*/
protected String getHelpDescription() {
return helpDescription;
}
/**
* Custom help URL to documentation for a component (typically an extension)
*
* @return the custom help URL, if any, for the component
*/
protected String getHelpUrl() {
return helpUrl;
}
/**
* Returns the name of this component's category within the Designer, as displayed
* (for example, "Screen Arrangement").
*
* @return the name of this component's Designer category
*/
protected String getCategory() {
return category;
}
/**
* Returns the String representation of the EnumConstant corresponding to this
* component's category within the Designer (for example, "ARRANGEMENTS").
* Usually, you should use {@link #getCategory()} instead.
*
* @return the EnumConstant representing this component's Designer category
*/
protected String getCategoryString() {
return categoryString;
}
/**
* Returns the version number of this component, as specified by
* {@link DesignerComponent#version()}.
*
* @return the version number of this component
*/
protected int getVersion() {
return version;
}
/**
* Returns whether this component is shown on the palette in the Designer, as
* specified by {@link DesignerComponent#showOnPalette()}.
*
* @return whether this component is shown on the Designer palette
*/
protected boolean getShowOnPalette() {
return showOnPalette;
}
/**
* Returns whether this component is non-visible on the device's screen, as
* specified by {@link DesignerComponent#nonVisible()}. Examples of non-visible
* components are {@link com.google.appinventor.components.runtime.LocationSensor}
* and {@link com.google.appinventor.components.runtime.Clock}.
*
* @return {@code true} if the component is non-visible, {@code false} otherwise
*/
protected boolean getNonVisible() {
return nonVisible;
}
/**
* Returns whether this component is an external component or not.
*
* @return true if the component is external. false otherwise.
*/
protected boolean getExternal() {
return external;
}
/**
* Returns the name of the icon file used on the Designer palette, as specified in
* {@link DesignerComponent#iconName()}.
*
* @return the name of the icon file
*/
protected String getIconName() {
return iconName;
}
private String getDisplayNameForComponentType(String componentTypeName) {
// Users don't know what a 'Form' is. They know it as a 'Screen'.
return "Form".equals(componentTypeName) ? "Screen" : componentTypeName;
}
}
/**
* Returns the annotations supported by this {@code ComponentProcessor}, namely those related
* to components ({@link com.google.appinventor.components.annotations}).
*
* @return the supported annotations
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
return SUPPORTED_ANNOTATION_TYPES;
}
@Override
public void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
elementUtils = processingEnv.getElementUtils();
typeUtils = processingEnv.getTypeUtils();
}
/**
* Processes the component-related annotations ({@link
* com.google.appinventor.components.annotations}),
* populating {@link #components} and initializing {@link #messager} for use within
* {@link #outputResults()}, which is called at the end of this method and must be overriden by
* concrete subclasses.
*
* @param annotations the annotation types requested to be processed
* @param roundEnv environment for information about the current and prior round
* @return {@code true}, indicating that the annotations have been claimed by this processor.
* @see AbstractProcessor#process
*/
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// This method will be called many times for the source code.
// Only do something on the first pass.
pass++;
if (pass > 1) {
return true;
}
messager = processingEnv.getMessager();
List<Element> elements = new ArrayList<>();
List<Element> excludedElements = new ArrayList<>();
for (TypeElement te : annotations) {
if (te.getSimpleName().toString().equals("DesignerComponent")) {
elements.addAll(roundEnv.getElementsAnnotatedWith(te));
} else if (te.getSimpleName().toString().equals("SimpleObject")) {
for (Element element : roundEnv.getElementsAnnotatedWith(te)) {
SimpleObject annotation = element.getAnnotation(SimpleObject.class);
if (!annotation.external()) {
elements.add(element);
} else {
excludedElements.add(element);
}
}
}
}
elements.removeAll(excludedElements);
System.out.println("Number of elements = " + elements.size());
for (Element element : elements) {
processComponent(element);
}
// Put the component class names (including abstract classes)
componentTypes.addAll(components.keySet());
for (Element element : excludedElements) {
componentTypes.add(element.asType().toString()); // allow extensions to reference one another
}
// Remove non-components before calling outputResults.
List<String> removeList = Lists.newArrayList();
for (Map.Entry<String, ComponentInfo> entry : components.entrySet()) {
ComponentInfo component = entry.getValue();
if (component.abstractClass || !component.designerComponent) {
removeList.add(entry.getKey());
}
}
components.keySet().removeAll(removeList);
try {
// This is an abstract method implemented in concrete subclasses.
outputResults();
} catch (IOException e) {
throw new RuntimeException(e);
}
// Indicate that we have successfully handled the annotations.
return true;
}
/*
* This processes an element if it represents a component, reading in its
* information and adding it to components. If this component is a
* subclass of another component, this method recursively calls itself on the
* superclass.
*/
private void processComponent(Element element) {
// If the element is not a component (e.g., Float), return early.
if (element.getAnnotation(SimpleObject.class) == null &&
element.getAnnotation(DesignerComponent.class) == null) {
return;
}
// If we already processed this component, return early.
String longComponentName = ((TypeElement) element).getQualifiedName().toString();
if (components.containsKey(longComponentName)) {
return;
}
// Create new ComponentInfo.
ComponentInfo componentInfo = new ComponentInfo(element);
// Check if this extends another component (DesignerComponent or SimpleObject).
List<? extends TypeMirror> directSupertypes = typeUtils.directSupertypes(element.asType());
if (!directSupertypes.isEmpty()) {
// Only look at the first one. Later ones would be interfaces,
// which we don't care about.
String parentName = directSupertypes.get(0).toString();
Element e = ((DeclaredType) directSupertypes.get(0)).asElement();
parentName = ((TypeElement) e).getQualifiedName().toString();
ComponentInfo parentComponent = components.get(parentName);
if (parentComponent == null) {
// Try to process the parent component now.
Element parentElement = elementUtils.getTypeElement(parentName);
if (parentElement != null) {
processComponent(parentElement);
parentComponent = components.get(parentName);
}
}
// If we still can't find the parent class, we don't care about it, since it's not a
// component (but something like java.lang.Object). Otherwise, we need to copy its
// build info, designer properties, properties, methods, and events.
if (parentComponent != null) {
// Copy its build info, designer properties, properties, methods, and events.
componentInfo.permissions.addAll(parentComponent.permissions);
componentInfo.libraries.addAll(parentComponent.libraries);
componentInfo.nativeLibraries.addAll(parentComponent.nativeLibraries);
componentInfo.assets.addAll(parentComponent.assets);
componentInfo.activities.addAll(parentComponent.activities);
componentInfo.broadcastReceivers.addAll(parentComponent.broadcastReceivers);
// TODO(Will): Remove the following call once the deprecated
// @SimpleBroadcastReceiver annotation is removed. It should
// should remain for the time being because otherwise we'll break
// extensions currently using @SimpleBroadcastReceiver.
componentInfo.classNameAndActionsBR.addAll(parentComponent.classNameAndActionsBR);
// Since we don't modify DesignerProperties, we can just call Map.putAll to copy the
// designer properties from parentComponent to componentInfo.
componentInfo.designerProperties.putAll(parentComponent.designerProperties);
// NOTE(lizlooney) We can't just call Map.putAll to copy the events/properties/methods from
// parentComponent to componentInfo because then each component will share a single
// Event/Property/Method and if one component overrides something about an
// Event/Property/Method, then it will affect all the other components that are sharing
// that Event/Property/Method.
for (Map.Entry<String, Event> entry : parentComponent.events.entrySet()) {
componentInfo.events.put(entry.getKey(), entry.getValue().clone());
}
for (Map.Entry<String, Property> entry : parentComponent.properties.entrySet()) {
componentInfo.properties.put(entry.getKey(), entry.getValue().clone());
}
for (Map.Entry<String, Method> entry : parentComponent.methods.entrySet()) {
componentInfo.methods.put(entry.getKey(), entry.getValue().clone());
}
}
}
// Gather permissions.
UsesPermissions usesPermissions = element.getAnnotation(UsesPermissions.class);
if (usesPermissions != null) {
for (String permission : usesPermissions.permissionNames().split(",")) {
componentInfo.permissions.add(permission.trim());
}
}
// Gather library names.
UsesLibraries usesLibraries = element.getAnnotation(UsesLibraries.class);
if (usesLibraries != null) {
for (String library : usesLibraries.libraries().split(",")) {
componentInfo.libraries.add(library.trim());
}
}
// Gather native library names.
UsesNativeLibraries usesNativeLibraries = element.getAnnotation(UsesNativeLibraries.class);
if (usesNativeLibraries != null) {
for (String nativeLibrary : usesNativeLibraries.libraries().split(",")) {
componentInfo.nativeLibraries.add(nativeLibrary.trim());
}
for (String v7aLibrary : usesNativeLibraries.v7aLibraries().split(",")) {
componentInfo.nativeLibraries.add(v7aLibrary.trim() + ARMEABI_V7A_SUFFIX);
}
}
// Gather required files.
UsesAssets usesAssets = element.getAnnotation(UsesAssets.class);
if (usesAssets != null) {
for (String file : usesAssets.fileNames().split(",")) {
componentInfo.assets.add(file.trim());
}
}
// Gather the required activities and build their element strings.
UsesActivities usesActivities = element.getAnnotation(UsesActivities.class);
if (usesActivities != null) {
try {
for (ActivityElement ae : usesActivities.activities()) {
componentInfo.activities.add(activityElementToString(ae));
}
} catch (IllegalAccessException e) {
messager.printMessage(Diagnostic.Kind.ERROR, "IllegalAccessException when gathering " +
"activity attributes and subelements for component " + componentInfo.name);
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
messager.printMessage(Diagnostic.Kind.ERROR, "InvocationTargetException when gathering " +
"activity attributes and subelements for component " + componentInfo.name);
throw new RuntimeException(e);
}
}
// Gather the required broadcast receivers and build their element strings.
UsesBroadcastReceivers usesBroadcastReceivers = element.getAnnotation(UsesBroadcastReceivers.class);
if (usesBroadcastReceivers != null) {
try {
for (ReceiverElement re : usesBroadcastReceivers.receivers()) {
componentInfo.broadcastReceivers.add(receiverElementToString(re));
}
} catch (IllegalAccessException e) {
messager.printMessage(Diagnostic.Kind.ERROR, "IllegalAccessException when gathering " +
"broadcast receiver attributes and subelements for component " + componentInfo.name);
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
messager.printMessage(Diagnostic.Kind.ERROR, "InvocationTargetException when gathering " +
"broadcast receiver attributes and subelements for component " + componentInfo.name);
throw new RuntimeException(e);
}
}
// TODO(Will): Remove the following legacy code once the deprecated
// @SimpleBroadcastReceiver annotation is removed. It should
// should remain for the time being because otherwise we'll break
// extensions currently using @SimpleBroadcastReceiver.
//
// Gather required actions for legacy Broadcast Receivers. The annotation
// has a Class Name and zero or more Filter Actions. In the
// resulting String, Class name will go first, and each Action
// will be added, separated by a comma.
SimpleBroadcastReceiver simpleBroadcastReceiver = element.getAnnotation(SimpleBroadcastReceiver.class);
if (simpleBroadcastReceiver != null) {
for (String className : simpleBroadcastReceiver.className().split(",")){
StringBuffer nameAndActions = new StringBuffer();
nameAndActions.append(className.trim());
for (String action : simpleBroadcastReceiver.actions().split(",")) {
nameAndActions.append("," + action.trim());
}
componentInfo.classNameAndActionsBR.add(nameAndActions.toString());
break; // We only need one class name; If more than one is passed, ignore all but first.
}
}
// Build up event information.
processEvents(componentInfo, element);
// Build up property information.
processProperties(componentInfo, element);
// Build up method information.
processMethods(componentInfo, element);
// Add it to our components map.
components.put(longComponentName, componentInfo);
}
private boolean isPublicMethod(Element element) {
return element.getModifiers().contains(Modifier.PUBLIC)
&& element.getKind() == ElementKind.METHOD;
}
private Property executableElementToProperty(Element element, String componentInfoName) {
String propertyName = element.getSimpleName().toString();
SimpleProperty simpleProperty = element.getAnnotation(SimpleProperty.class);
if (!(element.asType() instanceof ExecutableType)) {
throw new RuntimeException("element.asType() is not an ExecutableType for " +
propertyName);
}
Property property = new Property(propertyName,
simpleProperty.description(),
simpleProperty.category(),
simpleProperty.userVisible(),
elementUtils.isDeprecated(element));
// Get parameters to tell if this is a getter or setter.
ExecutableType executableType = (ExecutableType) element.asType();
List<? extends TypeMirror> parameters = executableType.getParameterTypes();
// Check if it is a setter or getter, and set the property's readable, writable,
// and type fields appropriately.
TypeMirror typeMirror;
if (parameters.size() == 0) {
// It is a getter.
property.readable = true;
typeMirror = executableType.getReturnType();
if (typeMirror.getKind().equals(TypeKind.VOID)) {
throw new RuntimeException("Property method is void and has no parameters: "
+ propertyName);
}
} else {
// It is a setter.
property.writable = true;
if (parameters.size() != 1) {
throw new RuntimeException("Too many parameters for setter for " +
propertyName);
}
typeMirror = parameters.get(0);
}
// Use typeMirror to set the property's type.
if (!typeMirror.getKind().equals(TypeKind.VOID)) {
property.type = typeMirror.toString();
}
property.componentInfoName = componentInfoName;
return property;
}
// Transform an @ActivityElement into an XML element String for use later
// in creating AndroidManifest.xml.
private static String activityElementToString(ActivityElement element)
throws IllegalAccessException, InvocationTargetException {
// First, we build the <activity> element's opening tag including any
// receiver element attributes.
StringBuilder elementString = new StringBuilder(" <activity ");
elementString.append(elementAttributesToString(element));
elementString.append(">\\n");
// Now, we collect any <activity> subelements.
elementString.append(subelementsToString(element.metaDataElements()));
elementString.append(subelementsToString(element.intentFilters()));
// Finally, we close the <activity> element and create its String.
return elementString.append(" </activity>\\n").toString();
}
// Transform a @ReceiverElement into an XML element String for use later
// in creating AndroidManifest.xml.
private static String receiverElementToString(ReceiverElement element)
throws IllegalAccessException, InvocationTargetException {
// First, we build the <receiver> element's opening tag including any
// receiver element attributes.
StringBuilder elementString = new StringBuilder(" <receiver ");
elementString.append(elementAttributesToString(element));
elementString.append(">\\n");
// Now, we collect any <receiver> subelements.
elementString.append(subelementsToString(element.metaDataElements()));
elementString.append(subelementsToString(element.intentFilters()));
// Finally, we close the <receiver> element and create its String.
return elementString.append(" </receiver>\\n").toString();
}
// Transform a @MetaDataElement into an XML element String for use later
// in creating AndroidManifest.xml.
private static String metaDataElementToString(MetaDataElement element)
throws IllegalAccessException, InvocationTargetException {
// First, we build the <meta-data> element's opening tag including any
// receiver element attributes.
StringBuilder elementString = new StringBuilder(" <meta-data ");
elementString.append(elementAttributesToString(element));
// Finally, we close the <meta-data> element and create its String.
return elementString.append("/>\\n").toString();
}
// Transform an @IntentFilterElement into an XML element String for use later
// in creating AndroidManifest.xml.
private static String intentFilterElementToString(IntentFilterElement element)
throws IllegalAccessException, InvocationTargetException {
// First, we build the <intent-filter> element's opening tag including any
// receiver element attributes.
StringBuilder elementString = new StringBuilder(" <intent-filter ");
elementString.append(elementAttributesToString(element));
elementString.append(">\\n");
// Now, we collect any <intent-filter> subelements.
elementString.append(subelementsToString(element.actionElements()));
elementString.append(subelementsToString(element.categoryElements()));
elementString.append(subelementsToString(element.dataElements()));
// Finally, we close the <intent-filter> element and create its String.
return elementString.append(" </intent-filter>\\n").toString();
}
// Transform an @ActionElement into an XML element String for use later
// in creating AndroidManifest.xml.
private static String actionElementToString(ActionElement element)
throws IllegalAccessException, InvocationTargetException {
// First, we build the <action> element's opening tag including any
// receiver element attributes.
StringBuilder elementString = new StringBuilder(" <action ");
elementString.append(elementAttributesToString(element));
// Finally, we close the <action> element and create its String.
return elementString.append("/>\\n").toString();
}
// Transform an @CategoryElement into an XML element String for use later
// in creating AndroidManifest.xml.
private static String categoryElementToString(CategoryElement element)
throws IllegalAccessException, InvocationTargetException {
// First, we build the <category> element's opening tag including any
// receiver element attributes.
StringBuilder elementString = new StringBuilder(" <category ");
elementString.append(elementAttributesToString(element));
// Finally, we close the <category> element and create its String.
return elementString.append("/>\\n").toString();
}
// Transform an @DataElement into an XML element String for use later
// in creating AndroidManifest.xml.
private static String dataElementToString(DataElement element)
throws IllegalAccessException, InvocationTargetException {
// First, we build the <data> element's opening tag including any
// receiver element attributes.
StringBuilder elementString = new StringBuilder(" <data ");
elementString.append(elementAttributesToString(element));
// Finally, we close the <data> element and create its String.
return elementString.append("/>\\n").toString();
}
// Build the attribute String for a given XML element modeled by an
// annotation.
//
// Note that we use the fully qualified names for certain classes in the
// "java.lang.reflect" package to avoid namespace collisions.
private static String elementAttributesToString(Annotation element)
throws IllegalAccessException, InvocationTargetException {
StringBuilder attributeString = new StringBuilder("");
Class<? extends Annotation> clazz = element.annotationType();
java.lang.reflect.Method[] methods = clazz.getDeclaredMethods();
String attributeSeparator = "";
for (java.lang.reflect.Method method : methods) {
int modCode = method.getModifiers();
if (java.lang.reflect.Modifier.isPublic(modCode)
&& !java.lang.reflect.Modifier.isStatic(modCode)) {
if (method.getReturnType().getSimpleName().equals("String")) {
// It is an XML element attribute.
String attributeValue = (String) method.invoke(clazz.cast(element));
if (!attributeValue.equals("")) {
attributeString.append(attributeSeparator);
attributeString.append("android:");
attributeString.append(method.getName());
attributeString.append("=\\\"");
attributeString.append(attributeValue);
attributeString.append("\\\"");
attributeSeparator = " ";
}
}
}
}
return attributeString.toString();
}
// Build the subelement String for a given array of XML elements modeled by
// corresponding annotations.
private static String subelementsToString(Annotation[] subelements)
throws IllegalAccessException, InvocationTargetException {
StringBuilder subelementString = new StringBuilder("");
for (Annotation subelement : subelements) {
if (subelement instanceof MetaDataElement) {
subelementString.append(metaDataElementToString((MetaDataElement) subelement));
} else if (subelement instanceof IntentFilterElement) {
subelementString.append(intentFilterElementToString((IntentFilterElement) subelement));
} else if (subelement instanceof ActionElement) {
subelementString.append(actionElementToString((ActionElement) subelement));
} else if (subelement instanceof CategoryElement) {
subelementString.append(categoryElementToString((CategoryElement) subelement));
} else if (subelement instanceof DataElement) {
subelementString.append(dataElementToString((DataElement) subelement));
}
}
return subelementString.toString();
}
private void processProperties(ComponentInfo componentInfo,
Element componentElement) {
// We no longer support properties that use the variant type.
for (Element element : componentElement.getEnclosedElements()) {
if (!isPublicMethod(element)) {
continue;
}
// Get the name of the prospective property.
String propertyName = element.getSimpleName().toString();
// Designer property information
DesignerProperty designerProperty = element.getAnnotation(DesignerProperty.class);
if (designerProperty != null) {
componentInfo.designerProperties.put(propertyName, designerProperty);
}
// If property is overridden without again using SimpleProperty, remove
// it. For example, this is done for Ball.Width(), which overrides the
// inherited property Width() because Ball uses Radius() instead.
if (element.getAnnotation(SimpleProperty.class) == null) {
if (componentInfo.properties.containsKey(propertyName)) {
// Look at the prior property's componentInfoName.
Property priorProperty = componentInfo.properties.get(propertyName);
if (priorProperty.componentInfoName.equals(componentInfo.name)) {
// The prior property's componentInfoName is the same as this componentInfo's name.
// This is just a read-only or write-only property. We don't need to do anything
// special here.
} else {
// The prior property's componentInfoName is the different than this componentInfo's
// name. This is an overridden property without the SimpleProperty annotation and we
// need to remove it.
componentInfo.properties.remove(propertyName);
}
}
} else {
// Create a new Property element, then compare and combine it with any
// prior Property element with the same property name, verifying that
// they are consistent.
Property newProperty = executableElementToProperty(element, componentInfo.name);
if (componentInfo.properties.containsKey(propertyName)) {
Property priorProperty = componentInfo.properties.get(propertyName);
if (!priorProperty.type.equals(newProperty.type)) {
// The 'real' type of a property is determined by its getter, if
// it has one. In theory there can be multiple setters which
// take different types and those types can differ from the
// getter.
if (newProperty.readable) {
priorProperty.type = newProperty.type;
} else if (priorProperty.writable) {
// TODO(user): handle lang_def and document generation for multiple setters.
throw new RuntimeException("Inconsistent types " + priorProperty.type +
" and " + newProperty.type + " for property " +
propertyName + " in component " + componentInfo.name);
}
}
// Merge newProperty into priorProperty, which is already in the properties map.
if (priorProperty.description.isEmpty() && !newProperty.description.isEmpty()) {
priorProperty.description = newProperty.description;
}
if (priorProperty.propertyCategory == PropertyCategory.UNSET) {
priorProperty.propertyCategory = newProperty.propertyCategory;
} else if (newProperty.propertyCategory != priorProperty.propertyCategory &&
newProperty.propertyCategory != PropertyCategory.UNSET) {
throw new RuntimeException(
"Property " + propertyName + " has inconsistent categories " +
priorProperty.propertyCategory + " and " +
newProperty.propertyCategory + " in component " +
componentInfo.name);
}
priorProperty.readable = priorProperty.readable || newProperty.readable;
priorProperty.writable = priorProperty.writable || newProperty.writable;
priorProperty.userVisible = priorProperty.userVisible && newProperty.userVisible;
priorProperty.deprecated = priorProperty.deprecated && newProperty.deprecated;
priorProperty.componentInfoName = componentInfo.name;
} else {
// Add the new property to the properties map.
componentInfo.properties.put(propertyName, newProperty);
}
}
}
}
// Note: The top halves of the bodies of processEvent() and processMethods()
// are very similar. I tried refactoring in several ways but it just made
// things more complex.
private void processEvents(ComponentInfo componentInfo,
Element componentElement) {
for (Element element : componentElement.getEnclosedElements()) {
if (!isPublicMethod(element)) {
continue;
}
// Get the name of the prospective event.
String eventName = element.getSimpleName().toString();
SimpleEvent simpleEventAnnotation = element.getAnnotation(SimpleEvent.class);
// Remove overriden events unless SimpleEvent is again specified.
// See comment in processProperties for an example.
if (simpleEventAnnotation == null) {
if (componentInfo.events.containsKey(eventName)) {
componentInfo.events.remove(eventName);
}
} else {
String eventDescription = simpleEventAnnotation.description();
if (eventDescription.isEmpty()) {
eventDescription = elementUtils.getDocComment(element);
if (eventDescription == null) {
messager.printMessage(Diagnostic.Kind.WARNING,
"In component " + componentInfo.name +
", event " + eventName +
" is missing a description.");
eventDescription = "";
}
}
boolean userVisible = simpleEventAnnotation.userVisible();
boolean deprecated = elementUtils.isDeprecated(element);
Event event = new Event(eventName, eventDescription, userVisible, deprecated);
componentInfo.events.put(event.name, event);
// Verify that this element has an ExecutableType.
if (!(element instanceof ExecutableElement)) {
throw new RuntimeException("In component " + componentInfo.name +
", the representation of SimpleEvent " + eventName +
" does not implement ExecutableElement.");
}
ExecutableElement e = (ExecutableElement) element;
// Extract the parameters.
for (VariableElement ve : e.getParameters()) {
event.addParameter(ve.getSimpleName().toString(),
ve.asType().toString());
}
}
}
}
private void processMethods(ComponentInfo componentInfo,
Element componentElement) {
for (Element element : componentElement.getEnclosedElements()) {
if (!isPublicMethod(element)) {
continue;
}
// Get the name of the prospective method.
String methodName = element.getSimpleName().toString();
SimpleFunction simpleFunctionAnnotation = element.getAnnotation(SimpleFunction.class);
// Remove overriden methods unless SimpleFunction is again specified.
// See comment in processProperties for an example.
if (simpleFunctionAnnotation == null) {
if (componentInfo.methods.containsKey(methodName)) {
componentInfo.methods.remove(methodName);
}
} else {
String methodDescription = simpleFunctionAnnotation.description();
if (methodDescription.isEmpty()) {
methodDescription = elementUtils.getDocComment(element);
if (methodDescription == null) {
messager.printMessage(Diagnostic.Kind.WARNING,
"In component " + componentInfo.name +
", method " + methodName +
" is missing a description.");
methodDescription = "";
}
}
boolean userVisible = simpleFunctionAnnotation.userVisible();
boolean deprecated = elementUtils.isDeprecated(element);
Method method = new Method(methodName, methodDescription, userVisible, deprecated);
componentInfo.methods.put(method.name, method);
// Verify that this element has an ExecutableType.
if (!(element instanceof ExecutableElement)) {
throw new RuntimeException("In component " + componentInfo.name +
", the representation of SimpleFunction " + methodName +
" does not implement ExecutableElement.");
}
ExecutableElement e = (ExecutableElement) element;
// Extract the parameters.
for (VariableElement ve : e.getParameters()) {
method.addParameter(ve.getSimpleName().toString(),
ve.asType().toString());
}
// Extract the return type.
if (e.getReturnType().getKind() != TypeKind.VOID) {
method.returnType = e.getReturnType().toString();
}
}
}
}
/**
* <p>Outputs the required component information in the desired format. It is called by
* {@link #process} after the fields {@link #components} and {@link #messager}
* have been populated.</p>
*
* <p>Implementations of this methods should call {@link #getOutputWriter(String)} to obtain a
* {@link Writer} for their output. Diagnostic messages should be written
* using {@link #messager}.</p>
*/
protected abstract void outputResults() throws IOException;
/**
* Returns the appropriate Yail type (e.g., "number" or "text") for a
* given Java type (e.g., "float" or "java.lang.String"). All component
* names are converted to "component".
*
* @param type a type name, as returned by {@link TypeMirror#toString()}
* @return one of "boolean", "text", "number", "list", or "component".
* @throws RuntimeException if the parameter cannot be mapped to any of the
* legal return values
*/
protected final String javaTypeToYailType(String type) {
// boolean -> boolean
if (type.equals("boolean")) {
return type;
}
// String -> text
if (type.equals("java.lang.String")) {
return "text";
}
// {float, double, int, short, long} -> number
if (type.equals("float") || type.equals("double") || type.equals("int") ||
type.equals("short") || type.equals("long") || type.equals("byte") ||
type.equals("short")) {
return "number";
}
// YailList -> list
if (type.equals("com.google.appinventor.components.runtime.util.YailList")) {
return "list";
}
// List<?> -> list
if (type.startsWith("java.util.List")) {
return "list";
}
// Calendar -> InstantInTime
if (type.equals("java.util.Calendar")) {
return "InstantInTime";
}
if (type.equals("java.lang.Object")) {
return "any";
}
// Check if it's a component.
if (componentTypes.contains(type)) {
return "component";
}
throw new RuntimeException("Cannot convert Java type '" + type +
"' to Yail type");
}
/**
* Creates and returns a {@link FileObject} for output.
*
* @param fileName the name of the output file
* @return the {@code FileObject}
* @throws IOException if the file cannot be created
*/
protected FileObject createOutputFileObject(String fileName) throws IOException {
return processingEnv.getFiler().
createResource(StandardLocation.SOURCE_OUTPUT, OUTPUT_PACKAGE, fileName);
}
/**
* Returns a {@link Writer} to which output should be written. As with any
* {@code Writer}, the methods {@link Writer#flush()} and {@link Writer#close()}
* should be called when output is complete.
*
* @param fileName the name of the output file
* @return the {@code Writer}
* @throws IOException if the {@code Writer} or underlying {@link FileObject}
* cannot be created
*/
protected Writer getOutputWriter(String fileName) throws IOException {
return createOutputFileObject(fileName).openWriter();
}
}