package co.codewizards.cloudstore.ls.core.provider;
import static co.codewizards.cloudstore.core.util.AssertUtil.*;
import static co.codewizards.cloudstore.core.util.ReflectionUtil.*;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.Provider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import co.codewizards.cloudstore.core.Uid;
import co.codewizards.cloudstore.core.io.NoCloseOutputStream;
import co.codewizards.cloudstore.core.ls.NoObjectRef;
import co.codewizards.cloudstore.ls.core.invoke.ForceNonTransientClassSet;
import co.codewizards.cloudstore.ls.core.invoke.ForceNonTransientContainer;
import co.codewizards.cloudstore.ls.core.invoke.ObjectGraphContainer;
import co.codewizards.cloudstore.ls.core.invoke.ObjectManager;
import co.codewizards.cloudstore.ls.core.invoke.ObjectRef;
import co.codewizards.cloudstore.ls.core.invoke.ObjectRefConverter;
import co.codewizards.cloudstore.ls.core.invoke.ObjectRefConverterFactory;
/**
* @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de
*/
@Provider
@Produces(MediaTypeConst.APPLICATION_JAVA_NATIVE_WITH_OBJECT_REF)
public class JavaNativeWithObjectRefMessageBodyWriter
implements MessageBodyWriter<Object>
{
private final ObjectRefConverterFactory objectRefConverterFactory;
@Context
private SecurityContext securityContext;
public JavaNativeWithObjectRefMessageBodyWriter(final ObjectRefConverterFactory objectRefConverterFactory) {
this.objectRefConverterFactory = assertNotNull(objectRefConverterFactory, "objectRefConverterFactory");
}
@Override
public long getSize(Object t, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType)
{
return -1;
}
@Override
public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
// We return always true, because we declared our media-type already in the @Produces above and thus don't need to check it here.
// At least I hope we don't get consulted for media-types that were not declared in @Produces.
return true;
}
@Override
public void writeTo(
Object t, Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders,
OutputStream entityStream
) throws IOException, WebApplicationException
{
final ObjectRefConverter objectRefConverter = objectRefConverterFactory.createObjectRefConverter(securityContext);
final ObjectGraphContainer objectGraphContainer = new ObjectGraphContainer(t);
final NoObjectRefAnalyser noObjectRefAnalyser = new NoObjectRefAnalyser(objectGraphContainer);
try (ObjectOutputStream oout = new ReplacingObjectOutputStream(new NoCloseOutputStream(entityStream), objectRefConverter, noObjectRefAnalyser, objectGraphContainer);) {
oout.writeObject(objectGraphContainer);
oout.flush();
}
}
private static class ReplacingObjectOutputStream extends ObjectOutputStream {
private final NoObjectRefAnalyser noObjectRefAnalyser;
private final ObjectRefConverter objectRefConverter;
private final ObjectGraphContainer objectGraphContainer;
public ReplacingObjectOutputStream(OutputStream out, final ObjectRefConverter objectRefConverter, final NoObjectRefAnalyser noObjectRefAnalyser, final ObjectGraphContainer objectGraphContainer) throws IOException {
super(out);
this.objectRefConverter = assertNotNull(objectRefConverter, "objectRefConverter");
this.noObjectRefAnalyser = assertNotNull(noObjectRefAnalyser, "noObjectRefAnalyser");
this.objectGraphContainer = assertNotNull(objectGraphContainer, "objectGraphContainer");
enableReplaceObject(true);
}
@Override
protected Object replaceObject(final Object object) throws IOException {
if (object == null || object instanceof ObjectRef || object instanceof Uid) // we replace the objects by ObjectRefs, hence we must ignore the replaced objects here - they are unknown to our analyser.
return object;
if (noObjectRefAnalyser.isNoObjectRef(object))
return object;
final Object result = objectRefConverter.convertToObjectRefIfNeeded(object);
if (result != object)
noObjectRefAnalyser.analyse(result);
if (ForceNonTransientClassSet.getInstance().isForceNonTransientClass(result.getClass())) {
final List<Field> transientFields = getTransientFields(result.getClass());
if (!transientFields.isEmpty()) {
final ForceNonTransientContainer forceNonTransientContainer = createForceNonTransientContainer(result, transientFields);
objectGraphContainer.putForceNonTransientContainer(forceNonTransientContainer);
}
}
return result;
}
private ForceNonTransientContainer createForceNonTransientContainer(final Object object, final List<Field> transientFields) {
final Map<String, Object> transientFieldName2Value = new HashMap<>();
for (final Field field : transientFields) {
field.setAccessible(true);
final Object fieldValue;
try {
fieldValue = field.get(object);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
final String fieldName = field.getDeclaringClass().getName() + '.' + field.getName();
transientFieldName2Value.put(fieldName, fieldValue);
}
return new ForceNonTransientContainer(object, transientFieldName2Value);
}
private List<Field> getTransientFields(Class<?> clazz) {
final List<Field> allDeclaredFields = getAllDeclaredFields(clazz);
final List<Field> transientFields = new ArrayList<>();
for (final Field field : allDeclaredFields) {
if ((Modifier.TRANSIENT & field.getModifiers()) != 0)
transientFields.add(field);
}
return transientFields;
}
}
private static NoObjectRef NO_OBJECT_REF_FALLBACK = new NoObjectRef() {
@Override
public Class<? extends Annotation> annotationType() {
return NoObjectRef.class;
}
@Override
public boolean value() {
return false;
}
@Override
public boolean inheritToObjectGraphChildren() {
return true;
}
};
private static class NoObjectRefAnalyser {
private static final Logger logger = LoggerFactory.getLogger(JavaNativeWithObjectRefMessageBodyWriter.NoObjectRefAnalyser.class);
private final IdentityHashMap<Object, NoObjectRef> object2NoObjectRef = new IdentityHashMap<>();
public NoObjectRefAnalyser(final Object root) {
analyse(root);
}
public void analyse(Object root) {
analyse(new LinkedList<Object>(), null, root, new IdentityHashMap<Object, Void>());
}
private void analyse(final LinkedList<Object> parentStack, final Object parent, final Object object, final IdentityHashMap<Object, Void> processedObjects) {
if (object == null)
return;
if (parentStack.size() >= 4)
return;
if (parent != null)
parentStack.addLast(parent);
try {
if (processedObjects.containsKey(object))
return;
logger.debug("analyse: object={}", object);
processedObjects.put(object, null);
final NoObjectRef noObjectRefVal = object2NoObjectRef.get(object);
if (noObjectRefVal == null) {
final NoObjectRef noObjectRef = object.getClass().getAnnotation(NoObjectRef.class);
if (noObjectRef == null) {
final NoObjectRef parentNoObjectRef = parent == null ? NO_OBJECT_REF_FALLBACK : object2NoObjectRef.get(parent);
object2NoObjectRef.put(object, parentNoObjectRef);
}
else
object2NoObjectRef.put(object, noObjectRef);
}
if (isTypeConsideredLeaf(object))
return;
if (object.getClass().isArray()) {
// We do not treat children of a simple array, because they are no objects and ObjectOutputStream.replaceObject(...) should thus *not* be called for them.
if (Object.class.isAssignableFrom(object.getClass().getComponentType())) {
final int length = Array.getLength(object);
for (int i = 0; i < length; ++i) {
final Object child = Array.get(object, i);
analyse(parentStack, object, child, processedObjects);
}
}
}
else if (object instanceof Collection<?>) {
final Collection<?> c = (Collection<?>) object;
for (final Object child : c)
analyse(parentStack, object, child, processedObjects);
}
else if (object instanceof Map<?, ?>) {
final Map<?, ?> m = (Map<?, ?>) object;
for (Map.Entry<?, ?> me : m.entrySet()) {
analyse(parentStack, object, me.getKey(), processedObjects);
analyse(parentStack, object, me.getValue(), processedObjects);
}
} else {
final List<Field> allDeclaredFields = getAllDeclaredFields(object.getClass());
final List<Object> children = new ArrayList<Object>(allDeclaredFields.size());
for (final Field field : allDeclaredFields) {
if ((Modifier.STATIC & field.getModifiers()) != 0)
continue;
if ((Modifier.TRANSIENT & field.getModifiers()) != 0)
continue;
final Object child = getFieldValue(object, field);
children.add(child);
logger.debug("analyse: field={} child=", field, child);
NoObjectRef noObjectRef = field.getAnnotation(NoObjectRef.class);
if (noObjectRef == null)
noObjectRef = child == null ? null : child.getClass().getAnnotation(NoObjectRef.class);
if (noObjectRef == null) {
final NoObjectRef parentNoObjectRef = object2NoObjectRef.get(object);
if (parentNoObjectRef.inheritToObjectGraphChildren())
object2NoObjectRef.put(child, parentNoObjectRef);
}
else
object2NoObjectRef.put(child, noObjectRef);
}
for (final Object child : children)
analyse(parentStack, object, child, processedObjects);
}
} finally {
if (parent != null)
parentStack.removeLast();
}
}
private Object getFieldValue(final Object object, final Field field) {
field.setAccessible(true);
try {
return field.get(object);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public boolean isNoObjectRef(Object object) {
final NoObjectRef result = object2NoObjectRef.get(object);
if (result == null) {
final NoObjectRef noObjectRef = object.getClass().getAnnotation(NoObjectRef.class);
if (noObjectRef != null)
return noObjectRef.value();
return false;
}
return result.value();
}
private boolean isTypeConsideredLeaf(final Object object) {
final Class<?> clazz = object.getClass();
if (ObjectManager.isObjectRefMappingEnabled(object))
return true;
// if (! (object instanceof Serializable))
// return true;
//
//// if (object instanceof ObjectRef) // does not work, because ObjectRef.ClassInfo.interfaceNames must not be referenced!
//// return true;
//
// if (object instanceof Uid)
// return true;
//
//// if (clazz.isArray() || Collection.class.isAssignableFrom(clazz) ||Map.class.isAssignableFrom(clazz))
//// return false;
// TODO do not hard-code the following, but use an advisor-service!
return !clazz.getName().startsWith("co.codewizards.") && !clazz.getName().startsWith("org.");
}
}
}