Skip to content

Commit

Permalink
Consolidate class scanning logic (#1088)
Browse files Browse the repository at this point in the history
* Ported everything from Elide 5.x

* More cleanup

* Fixed checkstyle error

* Cleanup

* Added test with no @entity on Elide model

* Merged in changes from Elide 5.x

* Added package includes back

* Class searching ignores inheritance

* Added test based on inspection feedback

* Fixed inspection comment

* Merged in elide 5 changes

* Added class scanner tests

* Inspection rework

* Turned back on OWASP scanning

* More rework

* Made changes to Include preserves inheritance property
  • Loading branch information
aklish committed Dec 3, 2019
1 parent 872c43c commit 6e05ce9
Show file tree
Hide file tree
Showing 29 changed files with 442 additions and 185 deletions.
152 changes: 118 additions & 34 deletions elide-core/src/main/java/com/yahoo/elide/core/EntityDictionary.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import com.yahoo.elide.annotation.Include;
import com.yahoo.elide.annotation.MappedInterface;
import com.yahoo.elide.annotation.SharePermission;
import com.yahoo.elide.core.exceptions.DuplicateMappingException;
import com.yahoo.elide.core.exceptions.HttpStatusException;
import com.yahoo.elide.core.exceptions.InternalServerErrorException;
import com.yahoo.elide.core.exceptions.InvalidAttributeException;
Expand Down Expand Up @@ -168,11 +167,35 @@ public static Method findMethod(Class<?> entityClass, String name, Class<?>... p
return m;
}

/**
* Returns an entity binding if the provided class has been bound in the dictionary.
* Otherwise the behavior depends on whether the unbound class is an Entity or not.
* If it is not an Entity, we return an EMPTY_BINDING. This preserves existing behavior for relationships
* which are entities but not bound. Otherwise, we throw an exception - which also preserves behavior
* for unbound non-entities.
* @param entityClass
* @return
*/
protected EntityBinding getEntityBinding(Class<?> entityClass) {
if (isMappedInterface(entityClass)) {
return EMPTY_BINDING;
}
return entityBindings.getOrDefault(lookupEntityClass(entityClass), EMPTY_BINDING);

//Common case of no inheritance. This lookup is a performance boost so we don't have to do reflection.
EntityBinding binding = entityBindings.get(entityClass);
if (binding != null) {
return binding;
}

Class<?> declaredClass = lookupBoundClass(entityClass);

if (declaredClass != null) {
return entityBindings.get(declaredClass);
}

//Will throw an exception if entityClass is not an entity.
lookupEntityClass(entityClass);
return EMPTY_BINDING;
}

public boolean isMappedInterface(Class<?> interfaceClass) {
Expand Down Expand Up @@ -808,24 +831,20 @@ public boolean isShareable(Class<?> entityClass) {
* @param cls Entity bean class
*/
public void bindEntity(Class<?> cls) {
if (entityBindings.getOrDefault(lookupEntityClass(cls), EMPTY_BINDING) != EMPTY_BINDING) {
if (isClassBound(cls)) {
//Ignore duplicate bindings.
return;
}

Annotation annotation = getFirstAnnotation(cls, Arrays.asList(Include.class, Exclude.class));
Include include = annotation instanceof Include ? (Include) annotation : null;
Exclude exclude = annotation instanceof Exclude ? (Exclude) annotation : null;
Entity entity = (Entity) getFirstAnnotation(cls, Arrays.asList(Entity.class));
Class<?> declaredClass = lookupIncludeClass(cls);

if (exclude != null) {
log.trace("Exclude {}", cls.getName());
if (declaredClass == null) {
log.trace("Missing include or excluded class {}", cls.getName());
return;
}

if (include == null) {
log.trace("Missing include {}", cls.getName());
return;
}
Include include = (Include) getFirstAnnotation(declaredClass, Arrays.asList(Include.class));
Entity entity = (Entity) getFirstAnnotation(declaredClass, Arrays.asList(Entity.class));

String name;
if (entity == null || "".equals(entity.name())) {
Expand All @@ -841,15 +860,10 @@ public void bindEntity(Class<?> cls) {
type = include.type();
}

Class<?> duplicate = bindJsonApiToEntity.put(type, cls);
if (duplicate != null && !duplicate.equals(cls)) {
log.error("Duplicate binding {} for {}, {}", type, cls, duplicate);
throw new DuplicateMappingException(type + " " + cls.getName() + ":" + duplicate.getName());
}

entityBindings.putIfAbsent(lookupEntityClass(cls), new EntityBinding(this, cls, type, name));
bindJsonApiToEntity.put(type, declaredClass);
entityBindings.put(declaredClass, new EntityBinding(this, declaredClass, type, name));
if (include.rootLevel()) {
bindEntityRoots.add(cls);
bindEntityRoots.add(declaredClass);
}
}

Expand Down Expand Up @@ -938,7 +952,7 @@ public static Annotation getFirstAnnotation(Class<?> entityClass,
Annotation annotation = null;
for (Class<?> cls = entityClass; annotation == null && cls != null; cls = cls.getSuperclass()) {
for (Class<? extends Annotation> annotationClass : annotationClassList) {
annotation = cls.getAnnotation(annotationClass);
annotation = cls.getDeclaredAnnotation(annotationClass);
if (annotation != null) {
break;
}
Expand All @@ -947,7 +961,7 @@ public static Annotation getFirstAnnotation(Class<?> entityClass,
// no class annotation, try packages
for (Package pkg = entityClass.getPackage(); annotation == null && pkg != null; pkg = getParentPackage(pkg)) {
for (Class<? extends Annotation> annotationClass : annotationClassList) {
annotation = pkg.getAnnotation(annotationClass);
annotation = pkg.getDeclaredAnnotation(annotationClass);
if (annotation != null) {
break;
}
Expand Down Expand Up @@ -979,7 +993,11 @@ public String getId(Object value) {
try {
AccessibleObject idField = null;
for (Class<?> cls = value.getClass(); idField == null && cls != null; cls = cls.getSuperclass()) {
idField = getEntityBinding(cls).getIdField();
try {
idField = getEntityBinding(cls).getIdField();
} catch (NullPointerException e) {
System.out.println("Class: " + cls.getSimpleName() + " ID Field: " + idField.toString());
}
}
if (idField instanceof Field) {
return String.valueOf(((Field) idField).get(value));
Expand Down Expand Up @@ -1029,18 +1047,86 @@ public Collection<Annotation> getIdAnnotations(Object value) {
* @return class with Entity annotation
*/
public Class<?> lookupEntityClass(Class<?> objClass) {
Class<?> declaringClass = lookupAnnotationDeclarationClass(objClass, Entity.class);
if (declaringClass != null) {
return declaringClass;
}
throw new IllegalArgumentException("Unbound Entity " + objClass);
}

/**
* Follow for this class or super-class for Include annotation.
*
* @param objClass provided class
* @return class with Include annotation or
*/
public Class<?> lookupIncludeClass(Class<?> objClass) {
Annotation first = getFirstAnnotation(objClass, Arrays.asList(Exclude.class, Include.class));
if (first instanceof Include) {
return objClass;
}
return null;
}

/**
* Search a class hierarchy to find the first instance of a declared annotation.
* @param objClass The class to start searching.
* @param annotationClass The annotation to search for.
* @return The class which declares the annotation or null.
*/
public Class<?> lookupAnnotationDeclarationClass(Class<?> objClass, Class<? extends Annotation> annotationClass) {
for (Class<?> cls = objClass; cls != null; cls = cls.getSuperclass()) {
EntityBinding binding = entityBindings.getOrDefault(cls, EMPTY_BINDING);
if (binding != EMPTY_BINDING) {
return binding.entityClass;
}
if (cls.isAnnotationPresent(Entity.class)) {
if (cls.getDeclaredAnnotation(annotationClass) != null) {
return cls;
}
}
throw new IllegalArgumentException("Unknown Entity " + objClass);
return null;
}

/**
* Return bound entity or null.
*
* @param objClass provided class
* @return Bound class.
*/
public Class<?> lookupBoundClass(Class<?> objClass) {
//Common case - we can avoid reflection by checking the map ...
EntityBinding binding = entityBindings.getOrDefault(objClass, EMPTY_BINDING);
if (binding != EMPTY_BINDING) {
return binding.entityClass;
}

Class<?> declaredClass = lookupIncludeClass(objClass);
if (declaredClass == null) {
return null;
}

binding = entityBindings.getOrDefault(declaredClass, EMPTY_BINDING);
if (binding != EMPTY_BINDING) {
return binding.entityClass;
}

try {
//Special Case for ORM proxied objects. If the class is a proxy,
//and it is unbound, try the superclass.
return lookupEntityClass(declaredClass.getSuperclass());
} catch (IllegalArgumentException e) {
return null;
}
}

/**
* Return if the class has been bound or not. Only safe to call while binding an entity (does not consider
* ORM proxy objects).
*
* @param objClass provided class
* @return true if the class is already bound.
*/
private boolean isClassBound(Class<?> objClass) {
return (entityBindings.getOrDefault(objClass, EMPTY_BINDING) != EMPTY_BINDING);
}


/**
* Retrieve the accessible object for a field from a target object.
*
Expand Down Expand Up @@ -1191,10 +1277,8 @@ public <T> List<T> walkEntityGraph(Set<Class<?>> entities, Function<Class<?>, T
for (String relationship : getElideBoundRelationships(clazz)) {
Class<?> relationshipClass = getParameterizedType(clazz, relationship);

try {
lookupEntityClass(relationshipClass);
} catch (IllegalArgumentException e) {

if (lookupBoundClass(relationshipClass) == null) {
/* The relationship hasn't been bound */
continue;
}
Expand Down Expand Up @@ -1377,7 +1461,7 @@ private boolean isValidParameterizedMap(Map<?, ?> values, Class<?> keyType, Clas
* @param entityClass the class to bind.
*/
private void bindIfUnbound(Class<?> entityClass) {
if (! entityBindings.containsKey(lookupEntityClass(entityClass))) {
if (! isClassBound(entityClass)) {
bindEntity(entityClass);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1101,7 +1101,7 @@ public void setObject(T obj) {
@Override
@JsonIgnore
public Class<T> getResourceClass() {
return (Class) dictionary.lookupEntityClass(obj.getClass());
return (Class) dictionary.lookupBoundClass(obj.getClass());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,14 @@
*/
package com.yahoo.elide.core.datastore.inmemory;

import com.yahoo.elide.annotation.Include;
import com.yahoo.elide.core.DataStore;
import com.yahoo.elide.core.DataStoreTransaction;
import com.yahoo.elide.core.EntityDictionary;

import com.yahoo.elide.core.datastore.test.DataStoreTestHarness;
import com.yahoo.elide.utils.ClassScanner;

import com.google.common.collect.Sets;
import org.reflections.Reflections;
import org.reflections.scanners.SubTypesScanner;
import org.reflections.scanners.TypeAnnotationsScanner;
import org.reflections.util.ClasspathHelper;
import org.reflections.util.ConfigurationBuilder;

import lombok.Getter;

import java.util.Collections;
Expand All @@ -28,8 +23,6 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

import javax.persistence.Entity;

/**
* Simple in-memory only database.
*/
Expand All @@ -45,24 +38,14 @@ public HashMapDataStore(Package beanPackage) {

public HashMapDataStore(Set<Package> beanPackages) {
this.beanPackages = beanPackages;
ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();

for (Package beanPackage : beanPackages) {
configurationBuilder.addUrls(ClasspathHelper.forPackage(beanPackage.getName()));
ClassScanner.getAnnotatedClasses(beanPackage, Include.class).stream().forEach(modelClass -> {
if (modelClass.getName().startsWith(beanPackage.getName())) {
dataStore.put(modelClass, Collections.synchronizedMap(new LinkedHashMap<>()));
}
});
}
configurationBuilder.setScanners(new SubTypesScanner(), new TypeAnnotationsScanner());

Reflections reflections = new Reflections(configurationBuilder);

reflections.getTypesAnnotatedWith(Entity.class).stream()
.forEach((cls) -> {
for (Package beanPackage : beanPackages) {
if (cls.getName().startsWith(beanPackage.getName())) {
dataStore.put(cls, Collections.synchronizedMap(new LinkedHashMap<>()));
break;
}
}
});
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public final boolean ok(User user) {
*/
@Override
public final boolean ok(T object, RequestScope requestScope, Optional<ChangeSpec> changeSpec) {
Class<?> entityClass = coreScope(requestScope).getDictionary().lookupEntityClass(object.getClass());
Class<?> entityClass = coreScope(requestScope).getDictionary().lookupBoundClass(object.getClass());
FilterExpression filterExpression = getFilterExpression(entityClass, requestScope);
return filterExpression.accept(new FilterExpressionCheckEvaluationVisitor(object, this, requestScope));
}
Expand Down Expand Up @@ -103,7 +103,7 @@ private static FilterExpressionPath getFilterExpressionPath(
Class<?> type,
String method,
EntityDictionary dictionary) throws NoSuchMethodException {
FilterExpressionPath path = dictionary.lookupEntityClass(type)
FilterExpressionPath path = dictionary.lookupBoundClass(type)
.getMethod(method)
.getAnnotation(FilterExpressionPath.class);
return path;
Expand Down
67 changes: 67 additions & 0 deletions elide-core/src/main/java/com/yahoo/elide/utils/ClassScanner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright 2015, Yahoo Inc.
* Licensed under the Apache License, Version 2.0
* See LICENSE file in project root for terms.
*/
package com.yahoo.elide.utils;

import org.reflections.Reflections;
import org.reflections.scanners.SubTypesScanner;
import org.reflections.scanners.TypeAnnotationsScanner;
import org.reflections.util.ClasspathHelper;
import org.reflections.util.ConfigurationBuilder;
import org.reflections.util.FilterBuilder;

import java.lang.annotation.Annotation;
import java.util.Set;

/**
* Scans a package for classes by looking at files in the classpath.
*/
public class ClassScanner {
/**
* Scans all classes accessible from the context class loader which belong to the given package and subpackages.
*
* @param toScan package to scan
* @param annotation Annotation to search
* @return The classes
*/
static public Set<Class<?>> getAnnotatedClasses(Package toScan, Class<? extends Annotation> annotation) {
return getAnnotatedClasses(toScan.getName(), annotation);
}

/**
* Scans all classes accessible from the context class loader which belong to the given package and subpackages.
*
* @param packageName package name to scan.
* @param annotation Annotation to search
* @return The classes
*/
static public Set<Class<?>> getAnnotatedClasses(String packageName, Class<? extends Annotation> annotation) {
ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();

configurationBuilder.addUrls(ClasspathHelper.forPackage(packageName));
configurationBuilder.setScanners(new SubTypesScanner(), new TypeAnnotationsScanner());
configurationBuilder.filterInputsBy(new FilterBuilder().include(FilterBuilder.prefix(packageName)));

Reflections reflections = new Reflections(configurationBuilder);

return reflections.getTypesAnnotatedWith(annotation, true);
}

/**
* Returns all classes within a package.
* @param packageName The root package to search.
* @return All the classes within a package.
*/
static public Set<Class<?>> getAllClasses(String packageName) {
ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();

configurationBuilder.addUrls(ClasspathHelper.forPackage(packageName));
configurationBuilder.setScanners(new SubTypesScanner(false));
configurationBuilder.filterInputsBy(new FilterBuilder().include(FilterBuilder.prefix(packageName)));

Reflections reflections = new Reflections(configurationBuilder);
return reflections.getSubTypesOf(Object.class);
}
}

0 comments on commit 6e05ce9

Please sign in to comment.