/*
* This file is part of Catfish.
* Copyright (C) 2010 Namazu Studios LLC
*
* Catfish is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* Catfish is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with Catfish. If not, please visit:
*
* http://www.gnu.org/licenses/
*
*/
package com.namazustudios.catfish.gae;
import static com.namazustudios.catfish.gae.GaeCatfish.entity;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Key;
import com.google.inject.Inject;
import com.namazustudios.catfish.CatfishException;
import com.namazustudios.catfish.Configuration;
import com.namazustudios.catfish.Marshaller;
import com.namazustudios.catfish.annotation.Child;
import com.namazustudios.catfish.annotation.Flat;
import com.namazustudios.catfish.annotation.Property;
import com.namazustudios.catfish.converter.Converter;
import com.namazustudios.catfish.validator.ValidationFailure;
import com.namazustudios.catfish.validator.Validator;
public class GaeMarshaller implements Marshaller {
@Inject
private GaeCatfish catfish;
@Inject
private Configuration configuration;
private final Map<Object, Entity> identities = new IdentityHashMap<Object, Entity>();
public IdentityHashMap<Object, Entity> marshall(Object object) {
Key key = catfish.getKey(object);
Key parent = catfish.getKey(object);
if (key != null) {
parent = parent.getParent();
}
key = catfish.generateKey(object, parent);
Entity entity = new Entity(key);
try {
identities.clear();
doMarshall(entity, object);
return new IdentityHashMap<Object, Entity>(identities);
} finally {
identities.clear();
}
}
@SuppressWarnings("unchecked")
private Object convert(Field field, Object object) throws IllegalAccessException {
Converter<Object, Object> converter;
Property property = field.getAnnotation(Property.class);
try {
if (!Utility.isConverterSpecified(property)) {
return field.get(object);
}
converter = (Converter<Object, Object>)
configuration.getConverter(property);
return converter.store(field.get(object));
} catch (ClassCastException ex) {
throw new CatfishException("Type mismatch for converter.", ex);
}
}
private void doMarshall(Entity entity, Object object) {
final Key key = entity.getKey();
Class<?> cls = object.getClass();
// Checks if the object was already marshalled. If the object was
// already marshalled, it just sets the new entitiy'sproperties if there
// is a different entity.
if (identities.containsKey(object)) {
Entity other = identities.get(object);
// Ensures that the object's key matches the cached identity, if
// they dont' match then there is a set of conflicting keys and
// that's no good.
if (!entity.getKey().equals(catfish.getKey(object))) {
throw new CatfishException("Conflicting keys. Found " + "entity with key " + entity.getKey()
+ " and object " + "with key " + catfish.getKey(object));
}
// Assigns the properies from the old entity to the new entity.
if (other != entity) {
entity.setPropertiesFrom(other);
}
// Since this object was already marshalled, simply return to
// prevent infinite recursion.
return;
}
do {
for (Field field : cls.getDeclaredFields()) {
boolean accessable = field.isAccessible();
if (Modifier.isTransient(field.getModifiers())) {
continue;
}
try {
field.setAccessible(true);
if (field.isAnnotationPresent(Property.class)) {
marshallProperty(entity, object, field);
} else if (field.isAnnotationPresent(Child.class)) {
marshallChild(entity, object, field);
} else if (field.isAnnotationPresent(Flat.class)) {
Object child = field.get(object);
if (child != null) {
doMarshall(entity, child);
}
}
} catch (IllegalArgumentException ex) {
throw new CatfishException("Encountered error " + "while marshalling " + object + " with key "
+ key + ".", ex);
} catch (IllegalAccessException ex) {
throw new CatfishException("Encountered error " + "while marshalling " + object + " with key "
+ key + ".", ex);
} finally {
field.setAccessible(accessable);
}
}
} while ((cls = cls.getSuperclass()) != null);
if (!entity.getKey().equals(entity.getKey())) {
throw new CatfishException("Conflicting keys. Found " + "entity with key " + entity.getKey()
+ " and object " + "with key " + catfish.getKey(object));
}
if (object.getClass().isAnnotationPresent(entity())) {
identities.put(object, entity);
}
}
private void marshallChild(Entity entity, Object object, Field field) throws IllegalAccessException {
Object child;
String propertyName;
propertyName = field.getAnnotation(Child.class).name().trim();
if (propertyName.isEmpty()) {
propertyName = field.getName();
}
child = field.get(object);
if (child == null) {
entity.removeProperty(propertyName);
} else if (child instanceof Collection<?>) {
List<Object> tmp = new ArrayList<Object>((Collection<?>) child);
ListIterator<Object> itr = tmp.listIterator();
while (itr.hasNext()) {
Object element = itr.next();
Key key = catfish.generateKey(element, catfish.getKey(object));
Entity childEntity = new Entity(key);
itr.set(key);
doMarshall(childEntity, element);
}
entity.setProperty(propertyName, tmp);
} else {
Key key = catfish.generateKey(child, catfish.getKey(object));
Entity childEntity = new Entity(key);
entity.setProperty(propertyName, key);
doMarshall(childEntity, child);
}
}
@SuppressWarnings("unchecked")
private void marshallProperty(Entity entity, Object object, Field field) throws ValidationFailure,
IllegalAccessException {
String propertyName;
Object propertyValue;
propertyName = field.getAnnotation(Property.class).name();
propertyName = propertyName.trim();
if (propertyName.isEmpty()) {
propertyName = field.getName();
}
if (Collection.class.isAssignableFrom(field.getType())) {
ListIterator<Object> itr;
List<Object> tmp = new ArrayList<Object>((Collection<?>) field.get(object));
Property property = field.getAnnotation(Property.class);
if (Utility.isValidatorSpecified(property)) {
itr = tmp.listIterator();
Validator<Object> validator = (Validator<Object>)
configuration.getValidator(property);
try {
while (itr.hasNext()) {
Object element = itr.next();
element = validator.validate(element);
itr.set(element);
}
} catch (ClassCastException ex) {
throw new CatfishException("Type mismatch while " +
"validating field " + field, ex);
}
}
if (Utility.isConverterSpecified(property)) {
Converter<Object, Object> converter;
itr = tmp.listIterator();
converter = (Converter<Object, Object>)
configuration.getConverter(property);
try {
while (itr.hasNext()) {
Object element = itr.next();
element = converter.store(element);
itr.set(element);
}
} catch (ClassCastException ex) {
throw new CatfishException("Type mismatch while " +
"converting field " + field, ex);
}
}
entity.setProperty(propertyName, tmp);
} else {
propertyValue = validate(field, object);
propertyValue = convert(field, object);
entity.setProperty(propertyName, propertyValue);
}
}
private Object validate(Field field, Object object) throws ValidationFailure, IllegalAccessException {
return validate(field, object, field.get(object));
}
@SuppressWarnings("unchecked")
private Object validate(Field field, Object object, Object value) throws ValidationFailure, IllegalAccessException {
Validator<Object> validator;
Property property = field.getAnnotation(Property.class);
try {
if (!Utility.isValidatorSpecified(property)) {
return value;
}
validator = (Validator<Object>) configuration.getValidator(property);
return validator.validate(value);
} catch (ClassCastException ex) {
throw new CatfishException("Type mismatch for validator ", ex);
}
}
}
|