/*
* Copyright (C) 2013 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.databinding.rebind;
import static java.util.stream.Collectors.toCollection;
import static org.jboss.errai.codegen.util.Stmt.invokeStatic;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import java.util.Set;
import org.jboss.errai.codegen.Statement;
import org.jboss.errai.codegen.Variable;
import org.jboss.errai.codegen.exception.GenerationException;
import org.jboss.errai.codegen.meta.HasAnnotations;
import org.jboss.errai.codegen.meta.MetaClass;
import org.jboss.errai.codegen.meta.MetaClassFactory;
import org.jboss.errai.codegen.meta.MetaConstructor;
import org.jboss.errai.codegen.meta.MetaField;
import org.jboss.errai.codegen.meta.MetaMethod;
import org.jboss.errai.codegen.meta.MetaParameter;
import org.jboss.errai.codegen.util.PrivateAccessUtil;
import org.jboss.errai.common.metadata.RebindUtils;
import org.jboss.errai.config.rebind.EnvUtil;
import org.jboss.errai.config.util.ClassScanner;
import org.jboss.errai.databinding.client.api.Bindable;
import org.jboss.errai.databinding.client.api.DataBinder;
import org.jboss.errai.ioc.rebind.ioc.graph.api.DependencyGraphBuilder.Dependency;
import org.jboss.errai.ioc.rebind.ioc.graph.api.DependencyGraphBuilder.FieldDependency;
import org.jboss.errai.ioc.rebind.ioc.graph.api.DependencyGraphBuilder.ParamDependency;
import org.jboss.errai.ioc.rebind.ioc.graph.api.Injectable;
import org.jboss.errai.ioc.rebind.ioc.injector.api.Decorable;
import org.jboss.errai.ioc.rebind.ioc.injector.api.Decorable.DecorableType;
import org.jboss.errai.ioc.rebind.ioc.injector.api.FactoryController;
import org.jboss.errai.reflections.util.SimplePackageFilter;
import org.jboss.errai.ui.shared.api.annotations.AutoBound;
import org.jboss.errai.ui.shared.api.annotations.Model;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gwt.core.ext.GeneratorContext;
/**
* Utility to retrieve a data binder reference. The reference is either to an
* injected {@link AutoBound} data binder or to a generated data binder for an
* injected {@link Model}.
*
* @author Christian Sadilek <csadilek@redhat.com>
* @author Mike Brock
*/
public class DataBindingUtil {
private static final Logger log = LoggerFactory.getLogger(DataBindingUtil.class);
public static final String BINDER_VAR_NAME = "DataModelBinder";
public static final String MODEL_VAR_NAME = "DataModel";
public static final String BINDER_MODEL_TYPE_VALUE = "DataBinderModelType";
public static final Annotation[] MODEL_QUALIFICATION = new Annotation[] {
new Model() {
@Override
public Class<? extends Annotation> annotationType() {
return Model.class;
}
}
};
private DataBindingUtil() {}
/**
* Represents a reference to an injected or generated data binder.
*/
public static class DataBinderRef {
private final MetaClass dataModelType;
private final Statement valueAccessor;
public DataBinderRef(final MetaClass dataModelType, final Statement valueAccessor) {
this.dataModelType = dataModelType;
this.valueAccessor = valueAccessor;
}
public MetaClass getDataModelType() {
return dataModelType;
}
public Statement getValueAccessor() {
return valueAccessor;
}
}
/**
* Tries to find a data binder reference for either an injected {@link Model}
* or an injected {@link AutoBound} data binder.
*
* @param inst
* the injectable instance
*
* @return the data binder reference or null if not found.
*/
public static DataBinderRef lookupDataBinderRef(final Decorable decorable, final FactoryController controller) {
DataBinderRef ref = lookupBinderForModel(decorable, controller);
if (ref == null) {
ref = lookupAutoBoundBinder(decorable, controller);
}
return ref;
}
/**
* Tries to find a data binder reference for an injected {@link Model}.
*
* @param inst
* the injectable instance
*
* @return the data binder reference or null if not found.
*/
private static DataBinderRef lookupBinderForModel(final Decorable decorable, final FactoryController controller) {
Statement dataBinderRef;
MetaClass dataModelType;
final MetaClass enclosingType = decorable.getEnclosingInjectable().getInjectedType();
final Collection<HasAnnotations> allAnnotated = getMembersAndParamsAnnotatedWith(enclosingType, Model.class);
if (!allAnnotated.isEmpty()) {
if (allAnnotated.size() > 1) {
throw new GenerationException("Multiple @Models injected in " + enclosingType);
}
else if (allAnnotated.size() == 1) {
final HasAnnotations annotated = allAnnotated.iterator().next();
if (annotated instanceof MetaParameter) {
final MetaParameter mp = (MetaParameter) annotated;
dataModelType = mp.getType();
assertTypeIsBindable(dataModelType);
controller.addInitializationStatements(
Collections.<Statement>singletonList(
controller.setReferenceStmt(MODEL_VAR_NAME, DecorableType.PARAM.getAccessStatement(mp, decorable.getFactoryMetaClass()))));
dataBinderRef = controller.getInstancePropertyStmt(
controller.getReferenceStmt(MODEL_VAR_NAME, dataModelType), BINDER_VAR_NAME,
DataBinder.class);
}
else {
final MetaField field = (MetaField) allAnnotated.iterator().next();
dataModelType = field.getType();
assertTypeIsBindable(dataModelType);
if (!field.isPublic()) {
controller.exposedFieldStmt(field);
}
dataBinderRef = controller.getInstancePropertyStmt(
DecorableType.FIELD.getAccessStatement(field, decorable.getFactoryMetaClass()), BINDER_VAR_NAME,
DataBinder.class);
}
return new DataBinderRef(dataModelType, dataBinderRef);
}
}
else {
final List<MetaField> modelFields = decorable.getDecorableDeclaringType().getFieldsAnnotatedWith(Model.class);
if (!modelFields.isEmpty()) {
throw new GenerationException("Found one or more fields annotated with @Model but missing @Inject "
+ modelFields.toString());
}
final List<MetaParameter> modelParameters = decorable.getDecorableDeclaringType().getParametersAnnotatedWith(Model.class);
if (!modelParameters.isEmpty()) {
throw new GenerationException(
"Found one or more constructor or method parameters annotated with @Model but missing @Inject "
+ modelParameters.toString());
}
}
return null;
}
private static Collection<HasAnnotations> getMembersAndParamsAnnotatedWith(final MetaClass enclosingType, final Class<? extends Annotation> annoType) {
final Collection<HasAnnotations> annotated = new ArrayList<>();
final Target target = annoType.getAnnotation(Target.class);
final Collection<ElementType> allowedTypes = (target == null) ? null : Arrays.asList(target.value());
if (allowedTypes == null || allowedTypes.contains(ElementType.FIELD)) {
annotated.addAll(enclosingType.getFieldsAnnotatedWith(annoType));
}
if (allowedTypes == null || allowedTypes.contains(ElementType.METHOD)) {
annotated.addAll(enclosingType.getMethodsAnnotatedWith(annoType));
}
if (allowedTypes == null || allowedTypes.contains(ElementType.CONSTRUCTOR)) {
for (final MetaConstructor ctor : enclosingType.getConstructors()) {
if (ctor.isAnnotationPresent(annoType)) {
annotated.add(ctor);
}
}
}
if (allowedTypes == null || allowedTypes.contains(ElementType.PARAMETER)) {
for (final MetaMethod method : enclosingType.getMethods()) {
for (final MetaParameter param : method.getParameters()) {
if (param.isAnnotationPresent(annoType)) {
annotated.add(param);
}
}
}
for (final MetaConstructor ctor : enclosingType.getConstructors()) {
for (final MetaParameter param : ctor.getParameters()) {
if (param.isAnnotationPresent(annoType)) {
annotated.add(param);
}
}
}
}
return annotated;
}
/**
* Tries to find a reference for an injected {@link AutoBound} data binder.
*
* @return the data binder reference or null if not found.
*/
private static DataBinderRef lookupAutoBoundBinder(final Decorable decorable, final FactoryController controller) {
Statement dataBinderRef = null;
MetaClass dataModelType = null;
final MetaClass enclosingType = decorable.getEnclosingInjectable().getInjectedType();
final Collection<HasAnnotations> allAnnotated = getMembersAndParamsAnnotatedWith(enclosingType, AutoBound.class);
if (allAnnotated.size() > 1) {
throw new GenerationException("Multiple @AutoBound data binders injected in " + enclosingType);
}
else if (allAnnotated.size() == 1) {
final HasAnnotations annotated = allAnnotated.iterator().next();
if (annotated instanceof MetaParameter) {
final MetaParameter mp = (MetaParameter) annotated;
assertTypeIsDataBinder(mp.getType());
dataModelType = (MetaClass) mp.getType().getParameterizedType().getTypeParameters()[0];
dataBinderRef = getAccessStatementForAutoBoundDataBinder(decorable, controller);
}
else {
final MetaField field = (MetaField) allAnnotated.iterator().next();
assertTypeIsDataBinder(field.getType());
dataModelType = (MetaClass) field.getType().getParameterizedType().getTypeParameters()[0];
dataBinderRef = DecorableType.FIELD.getAccessStatement(field, decorable.getFactoryMetaClass());
if (!field.isPublic()) {
controller.addExposedField(field);
}
}
}
else {
for (final MetaField field : enclosingType.getFields()) {
if (field.isAnnotationPresent(AutoBound.class)) {
assertTypeIsDataBinder(field.getType());
dataModelType = (MetaClass) field.getType().getParameterizedType().getTypeParameters()[0];
dataBinderRef = invokeStatic(decorable.getInjectionContext().getProcessingContext().getBootstrapClass(),
PrivateAccessUtil.getPrivateFieldAccessorName(field),
Variable.get("instance"));
controller.exposedFieldStmt(field);
break;
}
}
}
return (dataBinderRef != null) ? new DataBinderRef(dataModelType, dataBinderRef) : null;
}
private static Statement getAccessStatementForAutoBoundDataBinder(final Decorable decorable, final FactoryController controller) {
final Injectable enclosingInjectable = decorable.getEnclosingInjectable();
for (final Dependency dep : enclosingInjectable.getDependencies()) {
switch (dep.getDependencyType()) {
case Constructor:
case SetterParameter:
if (!(dep instanceof ParamDependency)) {
throw new RuntimeException("Found " + dep.getDependencyType() + " dependency that was not of type " + ParamDependency.class.getName());
}
final ParamDependency paramDep = (ParamDependency) dep;
if (paramDep.getParameter().isAnnotationPresent(AutoBound.class)) {
return DecorableType.PARAM.getAccessStatement(paramDep.getParameter(), decorable.getFactoryMetaClass());
} else {
break;
}
case Field:
if (!(dep instanceof FieldDependency)) {
throw new RuntimeException("Found " + dep.getDependencyType() + " dependency that was not of type " + FieldDependency.class.getName());
}
final FieldDependency fieldDep = (FieldDependency) dep;
if (fieldDep.getField().isAnnotationPresent(AutoBound.class)) {
if (!fieldDep.getField().isPublic()) {
controller.exposedFieldStmt(fieldDep.getField());
}
return DecorableType.FIELD.getAccessStatement(fieldDep.getField(), decorable.getFactoryMetaClass());
}
default:
break;
}
}
return null;
}
/**
* Ensures the provided type is a {@link DataBinder} and throws a
* {@link GenerationException} in case it's not.
*
* @param type
* the type to check
*/
private static void assertTypeIsDataBinder(final MetaClass type) {
final MetaClass databinderMetaClass = MetaClassFactory.get(DataBinder.class);
if (!databinderMetaClass.isAssignableFrom(type)) {
throw new GenerationException("Type of @AutoBound element must be " + DataBinder.class.getName() + " but is: "
+ type.getFullyQualifiedName());
}
}
/**
* Ensured the provided type is bindable and throws a
* {@link GenerationException} in case it's not.
*
* @param type
* the type to check
*/
private static void assertTypeIsBindable(final MetaClass type) {
if (!type.isAnnotationPresent(Bindable.class) && !getConfiguredBindableTypes().contains(type)) {
throw new GenerationException(type.getName() + " must be a @Bindable type when used as @Model");
}
}
/**
* Checks if the provided type is bindable.
*
* @param type
* the type to check
*
* @return true if the provide type is bindable, otherwise false.
*/
public static boolean isBindableType(final MetaClass type) {
return (type.isAnnotationPresent(Bindable.class) || getConfiguredBindableTypes().contains(type));
}
/**
* Returns all bindable types on the classpath.
*
* @param context
* the current generator context
*
* @return a set of meta classes representing the all bindable types (both
* annotated and configured in ErraiApp.properties).
*/
public static Set<MetaClass> getAllBindableTypes(final GeneratorContext context) {
final Collection<MetaClass> annotatedBindableTypes = ClassScanner.getTypesAnnotatedWith(Bindable.class,
RebindUtils.findTranslatablePackages(context), context);
final Set<MetaClass> bindableTypes = new HashSet<>(annotatedBindableTypes);
bindableTypes.addAll(DataBindingUtil.getConfiguredBindableTypes());
return bindableTypes;
}
private static Set<MetaClass> configuredBindableTypes = null;
/**
* Reads bindable types from all ErraiApp.properties files on the classpath.
*
* @return a set of meta classes representing the configured bindable types.
*/
public static Set<MetaClass> getConfiguredBindableTypes() {
if (configuredBindableTypes != null) {
configuredBindableTypes = refreshConfiguredBindableTypes();
} else {
configuredBindableTypes = findConfiguredBindableTypes();
}
return configuredBindableTypes;
}
private static Set<MetaClass> refreshConfiguredBindableTypes() {
final Set<MetaClass> refreshedTypes = new HashSet<>(configuredBindableTypes.size());
for (final MetaClass clazz : configuredBindableTypes) {
refreshedTypes.add(MetaClassFactory.get(clazz.getFullyQualifiedName()));
}
return refreshedTypes;
}
private static Set<MetaClass> findConfiguredBindableTypes() {
final Set<MetaClass> bindableTypes = new HashSet<>();
final Collection<URL> erraiAppProperties = EnvUtil.getErraiAppProperties();
for (final URL url : erraiAppProperties) {
InputStream inputStream = null;
try {
log.debug("Checking " + url.getFile() + " for bindable types...");
inputStream = url.openStream();
final ResourceBundle props = new PropertyResourceBundle(inputStream);
for (final String key : props.keySet()) {
if (key.equals(EnvUtil.CONFIG_ERRAI_BINDABLE_TYPES)) {
final Set<String> patterns = new LinkedHashSet<>();
for (final String s : props.getString(key).split(" ")) {
final String singleValue = s.trim();
if (singleValue.endsWith("*")) {
patterns.add(singleValue);
}
else {
try {
bindableTypes.add(MetaClassFactory.get(s.trim()));
} catch (final Exception e) {
throw new RuntimeException("Could not find class defined in ErraiApp.properties as bindable type: " + s);
}
}
}
if (!patterns.isEmpty()) {
final SimplePackageFilter filter = new SimplePackageFilter(patterns);
MetaClassFactory
.getAllCachedClasses()
.stream()
.filter(mc -> filter.apply(mc.getFullyQualifiedName()))
.collect(toCollection(() -> bindableTypes));
}
break;
}
}
} catch (final IOException e) {
throw new RuntimeException("Error reading ErraiApp.properties", e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (final IOException e) {
log.warn("Failed to close input stream", e);
}
}
}
}
return bindableTypes;
}
}