/*
* Copyright 2013 MovingBlocks
*
* 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.terasology.reflection.metadata;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import gnu.trove.map.TIntObjectMap;
import gnu.trove.map.hash.TIntObjectHashMap;
import org.reflections.ReflectionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.terasology.engine.SimpleUri;
import org.terasology.reflection.copy.CopyStrategy;
import org.terasology.reflection.copy.CopyStrategyLibrary;
import org.terasology.reflection.reflect.InaccessibleFieldException;
import org.terasology.reflection.reflect.ObjectConstructor;
import org.terasology.reflection.reflect.ReflectFactory;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.security.Permission;
import java.util.Collection;
import java.util.Locale;
import java.util.Map;
/**
* Class Metadata provides information on a class and its fields, and the ability to create, copy or manipulate an instance of the class.
* <br><br>
* Subclasses can be created to hold additional information for specific types of objects. These may override createField()
* to change how fields are processed and possibly switch to a subtype of FieldMetadata that holds additional information.
* <br><br>
* Consumed classes are required to have a default constructor (this may be private)
*
*/
public abstract class ClassMetadata<T, FIELD extends FieldMetadata<T, ?>> {
private static final Logger logger = LoggerFactory.getLogger(ClassMetadata.class);
private static final Permission CREATE_CLASS_METADATA = new RuntimePermission("createClassMetadata");
private final SimpleUri uri;
private final Class<T> clazz;
private final ObjectConstructor<T> constructor;
private Map<String, FIELD> fields = Maps.newHashMap();
private TIntObjectMap<FIELD> fieldsById = new TIntObjectHashMap<>();
/**
* Creates a class metatdata
*
* @param uri The uri that identifies this type
* @param type The type to create the metadata for
* @param factory A reflection library to provide class construction and field get/set functionality
* @param copyStrategyLibrary A copy strategy library
* @throws NoSuchMethodException If the class has no default constructor
*/
public ClassMetadata(SimpleUri uri, Class<T> type, ReflectFactory factory, CopyStrategyLibrary copyStrategyLibrary, Predicate<Field> includedFieldPredicate)
throws NoSuchMethodException {
if (System.getSecurityManager() != null) {
System.getSecurityManager().checkPermission(CREATE_CLASS_METADATA);
}
this.uri = uri;
this.clazz = type;
if (!type.isInterface() && !Modifier.isAbstract(type.getModifiers())) {
this.constructor = factory.createConstructor(type);
} else {
this.constructor = null;
}
addFields(copyStrategyLibrary, factory, includedFieldPredicate);
}
public final SimpleUri getUri() {
return uri;
}
/**
* Scans the class this metadata describes, adding all fields to the class' metadata.
*
* @param copyStrategyLibrary The library of copy strategies
* @param factory The reflection provider
*/
private void addFields(CopyStrategyLibrary copyStrategyLibrary, ReflectFactory factory, Predicate<Field> includedFieldsPredicate) {
for (Field field : ReflectionUtils.getAllFields(clazz, includedFieldsPredicate)) {
if (Modifier.isTransient(field.getModifiers()) || Modifier.isStatic(field.getModifiers())) {
continue;
}
CopyStrategy<?> copyStrategy = copyStrategyLibrary.getStrategy(field.getGenericType());
try {
FIELD metadata = createField(field, copyStrategy, factory);
if (metadata != null) {
fields.put(metadata.getName().toLowerCase(Locale.ENGLISH), metadata);
}
} catch (InaccessibleFieldException e) {
logger.error("Could not create metadata for field '{}' of type '{}', may be private.'", field, clazz);
}
}
}
/**
* Creates the FieldMetadata describing a field
*
* @param field The field to create metadata for
* @param copyStrategy The copy strategy library
* @param factory The reflection provider
* @return A FieldMetadata describing the field, or null to ignore this field
*/
protected abstract <V> FIELD createField(Field field, CopyStrategy<V> copyStrategy, ReflectFactory factory) throws InaccessibleFieldException;
/**
* @return The class described by this metadata
*/
public Class<T> getType() {
return clazz;
}
/**
* @param id The previously set id of the field
* @return The field identified by the given id, or null if there is no such field
*/
public FIELD getField(int id) {
return fieldsById.get(id);
}
/**
* @param fieldName The name of the field
* @return The field identified by the given name, or null if there is no such field
*/
public FIELD getField(String fieldName) {
return fields.get(fieldName.toLowerCase(Locale.ENGLISH));
}
/**
* @return The fields that this class has.
*/
public Collection<FIELD> getFields() {
return ImmutableList.copyOf(fields.values());
}
public boolean isConstructable() {
return constructor != null;
}
/**
* @return A new instance of this class.
*/
public T newInstance() {
Preconditions.checkState(isConstructable(), "Cannot construct '" + this + "' - no accessible default constructor");
return constructor.construct();
}
/**
* @param object The instance of this class to copy
* @return A copy of the given object
*/
public T copy(T object) {
T result = constructor.construct();
if (result != null) {
for (FIELD field : fields.values()) {
field.setValue(result, field.getCopyOfValue(object));
}
}
return result;
}
/**
* This method is for use in situations where metadata is being used generically and the actual type of the value cannot be
*
* @param object The instance of this class to copy
* @return A copy of the given object, or null if object is not of the type described by this metadata.
*/
public T copyRaw(Object object) {
if (getType().isInstance(object)) {
return copy(getType().cast(object));
}
return null;
}
/**
* @return The number of fields this class has
*/
public int getFieldCount() {
return fields.size();
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj instanceof ClassMetadata) {
ClassMetadata<?, ?> other = (ClassMetadata<?, ?>) obj;
return Objects.equal(other.clazz, clazz);
}
return false;
}
@Override
public int hashCode() {
return clazz.hashCode();
}
@Override
public String toString() {
if (uri.isValid()) {
return uri.toString();
}
return getType().toString();
}
/**
* Used by FieldMetadata to update the id lookup table
*
* @param field The field to update the id for
* @param id The new id of the field
*/
@SuppressWarnings("unchecked")
void setFieldId(FieldMetadata<T, ?> field, byte id) {
if (fields.containsValue(field)) {
fieldsById.put(id, (FIELD) field);
}
}
}