/*
* Copyright (C) 2011 Red Hat, Inc. and/or its affiliates.
*
* 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 org.jboss.errai.codegen;
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.Map;
import java.util.Set;
import java.util.TreeSet;
import org.jboss.errai.codegen.control.branch.Label;
import org.jboss.errai.codegen.control.branch.LabelReference;
import org.jboss.errai.codegen.exception.OutOfScopeException;
import org.jboss.errai.codegen.literal.LiteralValue;
import org.jboss.errai.codegen.meta.MetaClass;
import org.jboss.errai.codegen.meta.MetaClassFactory;
import org.jboss.errai.codegen.meta.MetaField;
import org.jboss.errai.codegen.meta.MetaMethod;
import org.jboss.errai.codegen.util.GenUtil;
import org.jboss.errai.common.client.api.Assert;
/**
* This class represents a context in which {@link Statement}s are generated.
* <p/>
* Its main purpose is to support the concept of scopes so that {@link Statement}s can be validated prior to
* compilation.
*
* @author Christian Sadilek <csadilek@redhat.com>
* @author Mike Brock
*/
public class Context {
private final Thread BLESSED_THREAD = Thread.currentThread();
private void checkThread() {
if (Thread.currentThread() != BLESSED_THREAD) {
throw new RuntimeException("WRONG THREAD!! Expected " + BLESSED_THREAD + "; got " + Thread.currentThread());
}
}
private Context parent = null;
private Map<String, Variable> variables;
private Map<String, Label> labels;
private boolean autoImportActive = false;
private Map<String, String> imports;
private final Set<MetaClass> classContexts;
private final Set<MetaClass> literalizableClasses;
private List<InterningCallback> interningCallbackList;
private Map<Object, Statement> internedValues;
private Map<String, Map<Object, Object>> renderingCache;
private boolean permissiveMode = GenUtil.isPermissiveMode();
private final Set<String> missingSymbols = new HashSet<String>();
private Context() {
classContexts = new HashSet<MetaClass>();
renderingCache = new HashMap<String, Map<Object, Object>>();
literalizableClasses = new HashSet<MetaClass>();
internedValues = new HashMap<Object, Statement>();
interningCallbackList = new ArrayList<InterningCallback>();
}
private Context(final Context parent) {
this();
this.parent = parent;
this.autoImportActive = parent.autoImportActive;
this.imports = parent.imports;
this.renderingCache = parent.renderingCache;
interningCallbackList = parent.interningCallbackList;
internedValues = parent.internedValues;
}
/**
* Creates a new and empty context.
*
* @return empty context
*/
public static Context create() {
return new Context();
}
/**
* Create a new sub context for the given parent context.
*
* @param parent
* the parent context to use.
*
* @return Created sub context
*/
public static Context create(final Context parent) {
return new Context(parent);
}
/**
* Add a variable to the current scope.
*
* @param name
* the name of the variable, must not be null.
* @param type
* the type of the variable, must not be null.
*
* @return the current context with the variable added.
*/
public Context addVariable(final String name, final Class<?> type) {
checkThread();
return addVariable(Variable.create(Assert.notNull(name), Assert.notNull(type)));
}
/**
* Add a variable to the current scope and initialize it.
*
* @param name
* the name of the variable, must not be null.
* @param type
* the type of the variable, must not be null.
* @param initialization
* the {@link Statement} or literal value to initialize the {@link Variable}, can be null.
*
* @return the current context with the variable added.
*/
public Context addVariable(final String name, final Class<?> type, final Object initialization) {
checkThread();
final Variable v = Variable.create(Assert.notNull(name), Assert.notNull(type), initialization);
return addVariable(v);
}
/**
* Add a {@link Variable} to the current scope.
*
* @param variable
* the variable instance to add, must not be null.
*
* @return the current context with the variable added.
*/
public Context addVariable(final Variable variable) {
checkThread();
if (variables == null)
variables = new HashMap<String, Variable>();
variables.put(variable.getName(), variable);
return this;
}
/**
* Add a {@link Label} to the current scope.
*
* @param label
* the label instance to add, must not be null.
*
* @return the current context with the label added.
*/
public Context addLabel(final Label label) {
checkThread();
if (labels == null)
labels = new HashMap<String, Label>();
labels.put(label.getName(), label);
return this;
}
// Ensures that imports are visible/shared between all parent/child contexts.
private void initImports() {
checkThread();
if (imports == null) {
Context c = this;
Map<String, String> importsMap = null;
while (c.parent != null) {
c = c.parent;
if (c.imports != null)
importsMap = c.imports;
}
if (importsMap == null) {
imports = importsMap = new HashMap<String, String>();
c = this;
while (c.parent != null) {
(c = c.parent).imports = importsMap;
}
}
else {
imports = importsMap;
}
}
}
/**
* Imports the given class.
*
* @param clazz
* the class to import, must not be null. If it is an array type (of any number of dimensions), its non-array
* component type will be imported.
*
* @return the current context with the import added.
*/
public Context addImport(MetaClass clazz) {
checkThread();
initImports();
while (clazz.isArray()) {
clazz = clazz.getComponentType();
}
if (!imports.containsKey(clazz.getName())) {
final String imp = getImportForClass(clazz);
if (imp != null) {
imports.put(clazz.getName(), imp);
}
}
return this;
}
/**
* Checks whether the given class has been imported.
*
* @param clazz
* the class to check, must not be null.
*
* @return true if import exists, otherwise false.
*/
public boolean hasImport(MetaClass clazz) {
checkThread();
if (clazz.isArray()) {
clazz = clazz.getComponentType();
}
return imports != null && imports.containsKey(clazz.getName()) &&
imports.get(clazz.getName()).equals(getImportForClass(clazz));
}
private String getImportForClass(final MetaClass clazz) {
checkThread();
String imp = null;
final String fqcn = clazz.getCanonicalName();
final int idx = fqcn.lastIndexOf('.');
if (idx != -1) {
imp = fqcn.substring(0, idx);
}
return imp;
}
/**
* Returns all imports except the optional ones (java.lang.*).
*
* @return required imports
*/
public Set<String> getRequiredImports() {
checkThread();
if (imports == null)
return Collections.emptySet();
final Set<String> importedClasses = new TreeSet<String>();
imports.keySet().stream()
.filter(clsName -> !imports.get(clsName).equals("java.lang"))
.forEach(clsName -> importedClasses.add(imports.get(clsName) + "." + clsName));
return importedClasses;
}
/**
* Enables automatic import of classes used during code generation.
*
* @return the current context whit auto import enabled.
*/
public Context autoImport() {
checkThread();
this.autoImportActive = true;
return this;
}
/**
* Returns a reference to the {@link Variable} with the given name.
*
* @param name
* the name of the variable.
*
* @return the {@link VariableReference} found, can not be null.
*
* @throws OutOfScopeException
* if variable with the given name can not be found.
*/
public VariableReference getVariable(final String name) {
checkThread();
return getVariable(name, false);
}
/**
* Returns a reference to the class member {@link Variable} with the given name.
*
* @param name
* the name of the class member variable.
*
* @return the {@link VariableReference} found, can not be null.
*
* @throws OutOfScopeException
* if member variable with the given name can not be found.
*/
public VariableReference getClassMember(final String name) {
checkThread();
return getVariable(name, true);
}
private VariableReference getVariable(final String name, final boolean mustBeClassMember) {
checkThread();
Variable found = null;
Context ctx = this;
do {
if (ctx.variables != null) {
final Variable var = ctx.variables.get(name);
found = (mustBeClassMember && var != null && !variables.containsKey(var.getName())) ? null : var;
}
}
while (found == null && (ctx = ctx.parent) != null);
if (found == null) {
missingSymbols.add(name);
if (GenUtil.isPermissiveMode()) {
return Variable.create(name, Object.class).getReference();
}
else {
throw new OutOfScopeException((mustBeClassMember) ? "this." + name : name + " not found.\nScope:\n" + this);
}
}
return found.getReference();
}
/**
* Returns the a reference to the {@link Label} with the given name.
*
* @param name
* the name of the label.
*
* @return the {@link LabelReference} found, can not be null.
*
* @throws OutOfScopeException
* if label with the given name can not be found.
*/
public LabelReference getLabel(final String name) {
checkThread();
Label found = null;
Context ctx = this;
do {
if (ctx.labels != null) {
found = ctx.labels.get(name);
}
}
while (found == null && (ctx = ctx.parent) != null);
if (found == null) {
throw new OutOfScopeException("Label not found: " + name);
}
return found.getReference();
}
/**
* Checks is the given {@link Variable} is in scope.
*
* @param variable
* the variable to check.
*
* @return true if in scope, otherwise false.
*/
public boolean isScoped(final Variable variable) {
checkThread();
Context ctx = this;
do {
if (ctx.variables != null && ctx.variables.containsValue(variable))
return true;
}
while ((ctx = ctx.parent) != null);
return false;
}
/**
* Checks is the given {@link MetaMethod} is in scope (part of the attached class contexts).
*
* @param method
* the method to check.
*
* @return true if in scope, otherwise false.
*/
public boolean isInScope(final MetaMethod method) {
checkThread();
Context c = this;
do {
for (final MetaClass clazz : c.classContexts) {
for (final MetaMethod m : clazz.getDeclaredMethods()) {
if (m.equals(method))
return true;
}
}
}
while ((c = c.parent) != null);
return false;
}
/**
* Checks is the given {@link MetaField} is in scope (part of the attached class contexts).
*
* @param field
* the field to check.
*
* @return true if in scope, otherwise false.
*/
public boolean isInScope(final MetaField field) {
checkThread();
Context c = this;
do {
for (final MetaClass clazz : c.classContexts) {
for (final MetaField m : clazz.getDeclaredFields()) {
if (m.equals(field))
return true;
}
}
}
while ((c = c.parent) != null);
return false;
}
/**
* Checks if the the given variable name is ambiguous in this scope.
*
* @param varName
* the variable name to check.
*
* @return true if ambiguous, otherwise false.
*/
public boolean isAmbiguous(final String varName) {
checkThread();
Context ctx = this;
int matches = 0;
do {
if (ctx.variables != null && ctx.variables.containsKey(varName))
matches++;
}
while ((ctx = ctx.parent) != null);
return matches > 1;
}
/**
* Returns all variables in this scope (does not include variables of parent scopes).
*
* @return collection of {@link Variable}, empty if no variables are in scope.
*/
public Collection<Variable> getDeclaredVariables() {
checkThread();
if (variables == null)
return Collections.<Variable>emptyList();
return variables.values();
}
public void addLiteralizableClasses(final Collection<Class<?>> clazzes) {
checkThread();
for (final Class<?> cls : clazzes) {
addLiteralizableClass(cls);
}
}
public void addLiteralizableMetaClasses(final Collection<MetaClass> clazzes) {
checkThread();
for (final MetaClass cls : clazzes) {
addLiteralizableClass(cls);
}
}
/**
* Mark a class "literalizable". Meaning that all classes that are assignable to this type, are candidates for
* reification to code snapshots for this context and all subcontexts. See {@link SnapshotMaker} for further details.
*
* @param clazz
* the class, interface or superclass to be considered literalizable.
*/
public void addLiteralizableClass(final Class<?> clazz) {
checkThread();
addLiteralizableClass(MetaClassFactory.get(clazz));
}
/**
* Mark a class "literalizable". Meaning that all classes that are assignable to this type, are candidates for
* reification to code snapshots for this context and all subcontexts. See {@link SnapshotMaker} for further details.
*
* @param clazz
* the class, interface or superclass to be considered literalizable.
*/
public void addLiteralizableClass(final MetaClass clazz) {
checkThread();
literalizableClasses.add(clazz.getErased());
}
/**
* Returns true if the specified class is literalizable.
*
* @param clazz
* the class, interface or superclass to be tested if literalizable
*
* @return true if the specified class is literalizable
*
* @see #addLiteralizableClass(Class)
*/
public boolean isLiteralizableClass(final Class<?> clazz) {
checkThread();
return isLiteralizableClass(MetaClassFactory.get(clazz));
}
/**
* Returns true if the specified class is literalizable.
*
* @param clazz
* the class, interface or superclass to be tested if literalizable
*
* @return true if the specified class is literalizable
*
* @see #addLiteralizableClass(MetaClass)
*/
public boolean isLiteralizableClass(final MetaClass clazz) {
checkThread();
return getLiteralizableTargetType(clazz) != null;
}
/**
* Returns the literalizable target type for any matching subtype. Meaning, that if say, the type
* <tt com.bar.FooImpl</tt> is a subtype of the interface <tt>com.bar.Foo</tt>, which is itself marked literalizable,
* this method will return a reference to the <tt>java.lang.Class</tt> instance for <tt>com.bar.Foo</tt>
*
* @param clazz
* the class, interface or superclass to obtain a literalizable target type for.
* @param clazz
* . If there are no matches, returns <tt>null</tt>.
*
* @return the literalizable target type that matches
*/
public Class<?> getLiteralizableTargetType(final Class<?> clazz) {
checkThread();
return getLiteralizableTargetType(MetaClassFactory.get(clazz));
}
/**
* Returns the literalizable target type for any matching subtype. Meaning, that if say, the type
* <tt>com.bar.FooImpl</tt> is a subtype of the interface <tt>com.bar.Foo</tt>, which is itself marked literalizable,
* this method will return a reference to the <tt>java.lang.Class</tt> instance for <tt>com.bar.Foo</tt>
*
* @param clazz
* the class, interface or superclass to obtain a literalizable target type for.
* @param clazz
* . If there are no matches, returns <tt>null</tt>.
*
* @return the literalizable target type that matches
*/
public Class<?> getLiteralizableTargetType(final MetaClass clazz) {
checkThread();
Context ctx = this;
do {
MetaClass cls = clazz;
do {
if (ctx.literalizableClasses.contains(cls))
return cls.asClass();
for (final MetaClass iface : cls.getInterfaces()) {
if (ctx.literalizableClasses.contains(iface))
return iface.asClass();
}
}
while ((cls = cls.getSuperClass()) != null);
}
while ((ctx = ctx.parent) != null);
return null;
}
/**
* Returns all variables in this scope (does not include variables of parent scopes).
*
* @return map of variable name to {@link Variable}, empty if no variables are in scope.
*/
public Map<String, Variable> getVariables() {
checkThread();
if (variables == null)
return Collections.<String, Variable>emptyMap();
return Collections.<String, Variable>unmodifiableMap(variables);
}
/**
* Attaches a class to the current scope.
*
* @param clazz
* class to attach.
*/
public void attachClass(final MetaClass clazz) {
checkThread();
this.classContexts.add(clazz);
}
/**
* Checks if automatic import is active.
*
* @return true if auto import active, otherwise false.
*/
public boolean isAutoImportActive() {
checkThread();
return autoImportActive;
}
/**
* Check is permissive mode is active for this context.
*
* @return
*/
public boolean isPermissiveMode() {
checkThread();
return permissiveMode;
}
/**
* Sets permissive mode active for this context.
*
* @param permissiveMode
*/
public void setPermissiveMode(final boolean permissiveMode) {
checkThread();
this.permissiveMode = permissiveMode;
}
// TODO factor this out. should not be part of Context.
@SuppressWarnings("unchecked")
public <K, V> Map<K, V> getRenderingCache(final RenderCacheStore<K, V> store) {
checkThread();
Map<K, V> cacheStore = (Map<K, V>) renderingCache.get(store.getName());
if (cacheStore == null) {
renderingCache.put(store.getName(), (Map<Object, Object>) (cacheStore = new HashMap<K, V>()));
}
return cacheStore;
}
/**
* Adds an {@link InterningCallback} to the context. Multiple callbacks can be registered. But, in the event that
* multiple callbacks fire and intern the same values, the <em>last</em> callback fired will be the effective
* interning behavior.
*
* @param interningCallback
*/
public void addInterningCallback(final InterningCallback interningCallback) {
checkThread();
interningCallbackList.add(interningCallback);
}
public Statement intern(final LiteralValue<?> literalValue) {
checkThread();
if (interningCallbackList.isEmpty()) return null;
Statement internedReference;
if ((internedReference = internedValues.get(literalValue.getValue())) != null) {
return internedReference;
}
for (final InterningCallback interningCallback : interningCallbackList) {
internedReference = interningCallback.intern(literalValue);
if (internedReference != null) {
internedValues.put(literalValue.getValue(), internedReference);
}
}
return internedReference;
}
@Override
public String toString() {
checkThread();
String indent = "";
final StringBuilder context = new StringBuilder();
Context ctx = this;
do {
if (ctx.variables != null && !ctx.variables.isEmpty()) {
context.append("Variables:\n");
for (final String varName : ctx.variables.keySet()) {
context.append(indent).append(ctx.variables.get(varName)).append("\n");
}
}
if (ctx.labels != null && !ctx.labels.isEmpty()) {
context.append("Labels:\n");
for (final String labelName : ctx.labels.keySet()) {
context.append(indent).append(ctx.labels.get(labelName)).append("\n");
}
}
if (ctx.classContexts != null && !ctx.classContexts.isEmpty()) {
context.append("Classes:\n");
for (final MetaClass clazz : ctx.classContexts) {
context.append(indent).append(clazz.getFullyQualifiedName()).append("\n");
}
}
if (ctx.imports != null && !ctx.imports.isEmpty()) {
context.append("Imports:\n");
for (final String className : ctx.imports.keySet()) {
context.append(indent).append(ctx.imports.get(className)).append(".").append(className).append("\n");
;
}
}
indent += " ";
}
while ((ctx = ctx.parent) != null);
return context.toString();
}
public Set<String> getMissingSymbols() {
checkThread();
return Collections.unmodifiableSet(missingSymbols);
}
}