/** * Copyright 2011-2017 Asakusa Framework Team. * * 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 com.asakusafw.testdriver.model; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import com.asakusafw.testdriver.core.DataModelDefinition; import com.asakusafw.testdriver.core.DataModelReflection; import com.asakusafw.testdriver.core.PropertyName; import com.asakusafw.testdriver.core.PropertyType; /** * Simple {@link DataModelDefinition} for classes which declare public fields directly. * This supports only following types: * <ul> * <li> wrapper types of any primitive types, </li> * <li> {@link java.lang.String}, </li> * <li> {@link java.math.BigInteger}, </li> * <li> {@link java.math.BigDecimal}, </li> * <li> or {@link java.util.Calendar} </li> * </ul> * <p> * Additionaly, each field should be named in {@code camelCase}. * </p> * @param <T> type of data model * @since 0.2.0 */ public class SimpleDataModelDefinition<T> implements DataModelDefinition<T> { private static final Map<Class<?>, PropertyType> TYPES; static { Map<Class<?>, PropertyType> map = new HashMap<>(); for (PropertyType type : PropertyType.values()) { map.put(type.getRepresentation(), type); } // special rule map.put(PropertyType.DATETIME.getRepresentation(), PropertyType.DATETIME); TYPES = Collections.unmodifiableMap(map); } private final Class<T> modelClass; private final Map<PropertyName, Field> fields; /** * Creates a new instance. * @param modelClass target model class * @throws IllegalArgumentException if some parameters were {@code null} */ public SimpleDataModelDefinition(Class<T> modelClass) { if (modelClass == null) { throw new IllegalArgumentException("modelClass must not be null"); //$NON-NLS-1$ } this.modelClass = modelClass; this.fields = collectProperties(); } private Map<PropertyName, Field> collectProperties() { Map<PropertyName, Field> results = new HashMap<>(); for (Field field : modelClass.getDeclaredFields()) { PropertyName name = extract(field); if (name != null) { results.put(name, field); } } return Collections.unmodifiableMap(results); } private static final Pattern NAME = Pattern.compile("[a-z][0-9A-Za-z]*"); //$NON-NLS-1$ private PropertyName extract(Field field) { assert field != null; if (Modifier.isPublic(field.getModifiers()) == false) { return null; } if (TYPES.containsKey(field.getType()) == false) { return null; } String name = field.getName(); if (NAME.matcher(name).matches() == false) { return null; } List<String> words = new ArrayList<>(); int start = 0; for (int i = 1, n = name.length(); i < n; i++) { char c = name.charAt(i); if ('A' <= c && c <= 'Z') { words.add(name.substring(start, i)); start = i; } } words.add(name.substring(start)); return PropertyName.newInstance(words); } @Override public Class<T> getModelClass() { return modelClass; } @Override public <A extends Annotation> A getAnnotation(Class<A> annotationType) { return modelClass.getAnnotation(annotationType); } @Override public Collection<PropertyName> getProperties() { return Collections.unmodifiableCollection(fields.keySet()); } @Override public PropertyType getType(PropertyName name) { Field field = fields.get(name); return field == null ? null : getType(name, field.getType()); } /** * Returns property type kind corresponded to the property name and type. * @param name property name * @param type property type * @return property type kind, or {@code null} without suitable corresponded kind * @throws IllegalArgumentException if some parameters were {@code null} */ public static PropertyType getType(PropertyName name, Class<?> type) { if (name == null) { throw new IllegalArgumentException("name must not be null"); //$NON-NLS-1$ } if (type == null) { throw new IllegalArgumentException("type must not be null"); //$NON-NLS-1$ } PropertyType kind = TYPES.get(type); if (kind == null) { return null; } if (kind.getRepresentation() == Calendar.class) { List<String> words = name.getWords(); if (words.contains(PropertyType.DATE.name().toLowerCase())) { return PropertyType.DATE; } else if (words.contains(PropertyType.TIME.name().toLowerCase())) { return PropertyType.TIME; } else if (words.contains(PropertyType.DATETIME.name().toLowerCase())) { return PropertyType.DATETIME; } } return kind; } @Override public <A extends Annotation> A getAnnotation(PropertyName name, Class<A> annotationType) { Field field = fields.get(name); return field == null ? null : field.getAnnotation(annotationType); } @Override public Builder<T> newReflection() { return new Builder<>(this); } @Override public DataModelReflection toReflection(T object) { Builder<T> builder = newReflection(); for (Map.Entry<PropertyName, Field> entry : fields.entrySet()) { PropertyName name = entry.getKey(); Field field = entry.getValue(); try { Object value = field.get(object); builder.add(name, value); } catch (IllegalAccessException e) { throw new AssertionError(e); } } return builder.build(); } @Override public T toObject(DataModelReflection reflection) { try { T instance = modelClass.newInstance(); for (Map.Entry<PropertyName, Field> entry : fields.entrySet()) { PropertyName name = entry.getKey(); Field field = entry.getValue(); Object value = reflection.getValue(name); try { field.set(instance, value); } catch (IllegalAccessException e) { throw new AssertionError(e); } } return instance; } catch (Exception e) { throw new IllegalStateException(e); } } }