001package co.codewizards.cloudstore.ls.core.provider;
002
003import static co.codewizards.cloudstore.core.util.ReflectionUtil.*;
004import static java.util.Objects.*;
005
006import java.io.IOException;
007import java.io.ObjectOutputStream;
008import java.io.OutputStream;
009import java.lang.annotation.Annotation;
010import java.lang.reflect.Array;
011import java.lang.reflect.Field;
012import java.lang.reflect.Modifier;
013import java.lang.reflect.Type;
014import java.util.ArrayList;
015import java.util.Collection;
016import java.util.HashMap;
017import java.util.IdentityHashMap;
018import java.util.LinkedList;
019import java.util.List;
020import java.util.Map;
021
022import javax.ws.rs.Produces;
023import javax.ws.rs.WebApplicationException;
024import javax.ws.rs.core.Context;
025import javax.ws.rs.core.MediaType;
026import javax.ws.rs.core.MultivaluedMap;
027import javax.ws.rs.core.SecurityContext;
028import javax.ws.rs.ext.MessageBodyWriter;
029import javax.ws.rs.ext.Provider;
030
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033
034import co.codewizards.cloudstore.core.Uid;
035import co.codewizards.cloudstore.core.io.NoCloseOutputStream;
036import co.codewizards.cloudstore.core.ls.NoObjectRef;
037import co.codewizards.cloudstore.ls.core.invoke.ForceNonTransientClassSet;
038import co.codewizards.cloudstore.ls.core.invoke.ForceNonTransientContainer;
039import co.codewizards.cloudstore.ls.core.invoke.ObjectGraphContainer;
040import co.codewizards.cloudstore.ls.core.invoke.ObjectManager;
041import co.codewizards.cloudstore.ls.core.invoke.ObjectRef;
042import co.codewizards.cloudstore.ls.core.invoke.ObjectRefConverter;
043import co.codewizards.cloudstore.ls.core.invoke.ObjectRefConverterFactory;
044
045/**
046 * @author Marco หงุ่ยตระกูล-Schulze - marco at nightlabs dot de
047 */
048@Provider
049@Produces(MediaTypeConst.APPLICATION_JAVA_NATIVE_WITH_OBJECT_REF)
050public class JavaNativeWithObjectRefMessageBodyWriter
051implements MessageBodyWriter<Object>
052{
053        private final ObjectRefConverterFactory objectRefConverterFactory;
054
055        @Context
056        private SecurityContext securityContext;
057
058        public JavaNativeWithObjectRefMessageBodyWriter(final ObjectRefConverterFactory objectRefConverterFactory) {
059                this.objectRefConverterFactory = requireNonNull(objectRefConverterFactory, "objectRefConverterFactory");
060        }
061
062        @Override
063        public long getSize(Object t, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType)
064        {
065                return -1;
066        }
067
068        @Override
069        public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
070                // We return always true, because we declared our media-type already in the @Produces above and thus don't need to check it here.
071                // At least I hope we don't get consulted for media-types that were not declared in @Produces.
072                return true;
073        }
074
075        @Override
076        public void writeTo(
077                        Object t, Class<?> type, Type genericType,
078                        Annotation[] annotations, MediaType mediaType,
079                        MultivaluedMap<String, Object> httpHeaders,
080                        OutputStream entityStream
081                        ) throws IOException, WebApplicationException
082        {
083                final ObjectRefConverter objectRefConverter = objectRefConverterFactory.createObjectRefConverter(securityContext);
084                final ObjectGraphContainer objectGraphContainer = new ObjectGraphContainer(t);
085                final NoObjectRefAnalyser noObjectRefAnalyser = new NoObjectRefAnalyser(objectGraphContainer);
086                try (ObjectOutputStream oout = new ReplacingObjectOutputStream(new NoCloseOutputStream(entityStream), objectRefConverter, noObjectRefAnalyser, objectGraphContainer);) {
087                        oout.writeObject(objectGraphContainer);
088                        oout.flush();
089                }
090        }
091
092        private static class ReplacingObjectOutputStream extends ObjectOutputStream {
093                private final NoObjectRefAnalyser noObjectRefAnalyser;
094                private final ObjectRefConverter objectRefConverter;
095                private final ObjectGraphContainer objectGraphContainer;
096
097                public ReplacingObjectOutputStream(OutputStream out, final ObjectRefConverter objectRefConverter, final NoObjectRefAnalyser noObjectRefAnalyser, final ObjectGraphContainer objectGraphContainer) throws IOException {
098                        super(out);
099                        this.objectRefConverter = requireNonNull(objectRefConverter, "objectRefConverter");
100                        this.noObjectRefAnalyser = requireNonNull(noObjectRefAnalyser, "noObjectRefAnalyser");
101                        this.objectGraphContainer = requireNonNull(objectGraphContainer, "objectGraphContainer");
102                        enableReplaceObject(true);
103                }
104
105                @Override
106                protected Object replaceObject(final Object object) throws IOException {
107                        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.
108                                return object;
109
110                        if (noObjectRefAnalyser.isNoObjectRef(object))
111                                return object;
112
113                        final Object result = objectRefConverter.convertToObjectRefIfNeeded(object);
114                        if (result != object)
115                                noObjectRefAnalyser.analyse(result);
116
117                        if (ForceNonTransientClassSet.getInstance().isForceNonTransientClass(result.getClass())) {
118                                final List<Field> transientFields = getTransientFields(result.getClass());
119                                if (!transientFields.isEmpty()) {
120                                        final ForceNonTransientContainer forceNonTransientContainer = createForceNonTransientContainer(result, transientFields);
121                                        objectGraphContainer.putForceNonTransientContainer(forceNonTransientContainer);
122                                }
123                        }
124                        return result;
125                }
126
127                private ForceNonTransientContainer createForceNonTransientContainer(final Object object, final List<Field> transientFields) {
128                        final Map<String, Object> transientFieldName2Value = new HashMap<>();
129                        for (final Field field : transientFields) {
130                                field.setAccessible(true);
131
132                                final Object fieldValue;
133                                try {
134                                        fieldValue = field.get(object);
135                                } catch (IllegalAccessException e) {
136                                        throw new RuntimeException(e);
137                                }
138
139                                final String fieldName = field.getDeclaringClass().getName() + '.' + field.getName();
140                                transientFieldName2Value.put(fieldName, fieldValue);
141                        }
142                        return new ForceNonTransientContainer(object, transientFieldName2Value);
143                }
144
145                private List<Field> getTransientFields(Class<?> clazz) {
146                        final List<Field> allDeclaredFields = getAllDeclaredFields(clazz);
147                        final List<Field> transientFields = new ArrayList<>();
148                        for (final Field field : allDeclaredFields) {
149                                if ((Modifier.TRANSIENT & field.getModifiers()) != 0)
150                                        transientFields.add(field);
151                        }
152                        return transientFields;
153                }
154        }
155
156        private static NoObjectRef NO_OBJECT_REF_FALLBACK = new NoObjectRef() {
157                @Override
158                public Class<? extends Annotation> annotationType() {
159                        return NoObjectRef.class;
160                }
161
162                @Override
163                public boolean value() {
164                        return false;
165                }
166
167                @Override
168                public boolean inheritToObjectGraphChildren() {
169                        return true;
170                }
171        };
172
173        private static class NoObjectRefAnalyser {
174                private static final Logger logger = LoggerFactory.getLogger(JavaNativeWithObjectRefMessageBodyWriter.NoObjectRefAnalyser.class);
175
176                private final IdentityHashMap<Object, NoObjectRef> object2NoObjectRef = new IdentityHashMap<>();
177
178                public NoObjectRefAnalyser(final Object root) {
179                        analyse(root);
180                }
181
182                public void analyse(Object root) {
183                        analyse(new LinkedList<Object>(), null, root, new IdentityHashMap<Object, Void>());
184                }
185
186                private void analyse(final LinkedList<Object> parentStack, final Object parent, final Object object, final IdentityHashMap<Object, Void> processedObjects) {
187                        if (object == null)
188                                return;
189
190                        if (parentStack.size() >= 4)
191                                return;
192
193                        if (parent != null)
194                                parentStack.addLast(parent);
195
196                        try {
197                                if (processedObjects.containsKey(object))
198                                        return;
199
200                                logger.debug("analyse: object={}", object);
201
202                                processedObjects.put(object, null);
203
204                                final NoObjectRef noObjectRefVal = object2NoObjectRef.get(object);
205                                if (noObjectRefVal == null) {
206                                        final NoObjectRef noObjectRef = object.getClass().getAnnotation(NoObjectRef.class);
207                                        if (noObjectRef == null) {
208                                                final NoObjectRef parentNoObjectRef = parent == null ? NO_OBJECT_REF_FALLBACK : object2NoObjectRef.get(parent);
209                                                object2NoObjectRef.put(object, parentNoObjectRef);
210                                        }
211                                        else
212                                                object2NoObjectRef.put(object, noObjectRef);
213                                }
214
215                                if (isTypeConsideredLeaf(object))
216                                        return;
217
218                                if (object.getClass().isArray()) {
219                                        // We do not treat children of a simple array, because they are no objects and ObjectOutputStream.replaceObject(...) should thus *not* be called for them.
220                                        if (Object.class.isAssignableFrom(object.getClass().getComponentType())) {
221                                                final int length = Array.getLength(object);
222                                                for (int i = 0; i < length; ++i) {
223                                                        final Object child = Array.get(object, i);
224                                                        analyse(parentStack, object, child, processedObjects);
225                                                }
226                                        }
227                                }
228                                else if (object instanceof Collection<?>) {
229                                        final Collection<?> c = (Collection<?>) object;
230                                        for (final Object child : c)
231                                                analyse(parentStack, object, child, processedObjects);
232                                }
233                                else if (object instanceof Map<?, ?>) {
234                                        final Map<?, ?> m = (Map<?, ?>) object;
235                                        for (Map.Entry<?, ?> me : m.entrySet()) {
236                                                analyse(parentStack, object, me.getKey(), processedObjects);
237                                                analyse(parentStack, object, me.getValue(), processedObjects);
238                                        }
239                                } else {
240                                        final List<Field> allDeclaredFields = getAllDeclaredFields(object.getClass());
241                                        final List<Object> children = new ArrayList<Object>(allDeclaredFields.size());
242                                        for (final Field field : allDeclaredFields) {
243                                                if ((Modifier.STATIC & field.getModifiers()) != 0)
244                                                        continue;
245
246                                                if ((Modifier.TRANSIENT & field.getModifiers()) != 0)
247                                                        continue;
248
249                                                final Object child = getFieldValue(object, field);
250                                                children.add(child);
251
252                                                logger.debug("analyse: field={} child=", field, child);
253
254                                                NoObjectRef noObjectRef = field.getAnnotation(NoObjectRef.class);
255                                                if (noObjectRef == null)
256                                                        noObjectRef = child == null ? null : child.getClass().getAnnotation(NoObjectRef.class);
257
258                                                if (noObjectRef == null) {
259                                                        final NoObjectRef parentNoObjectRef = object2NoObjectRef.get(object);
260                                                        if (parentNoObjectRef.inheritToObjectGraphChildren())
261                                                                object2NoObjectRef.put(child, parentNoObjectRef);
262                                                }
263                                                else
264                                                        object2NoObjectRef.put(child, noObjectRef);
265                                        }
266
267                                        for (final Object child : children)
268                                                analyse(parentStack, object, child, processedObjects);
269                                }
270                        } finally {
271                                if (parent != null)
272                                        parentStack.removeLast();
273                        }
274                }
275
276                private Object getFieldValue(final Object object, final Field field) {
277                        field.setAccessible(true);
278                        try {
279                                return field.get(object);
280                        } catch (IllegalAccessException e) {
281                                throw new RuntimeException(e);
282                        }
283                }
284
285                public boolean isNoObjectRef(Object object) {
286                        final NoObjectRef result = object2NoObjectRef.get(object);
287                        if (result == null) {
288                                final NoObjectRef noObjectRef = object.getClass().getAnnotation(NoObjectRef.class);
289                                if (noObjectRef != null)
290                                        return noObjectRef.value();
291
292                                return false;
293                        }
294
295                        return result.value();
296                }
297
298                private boolean isTypeConsideredLeaf(final Object object) {
299                        final Class<?> clazz = object.getClass();
300
301                        if (ObjectManager.isObjectRefMappingEnabled(object))
302                                return true;
303
304//                      if (! (object instanceof Serializable))
305//                              return true;
306//
307////                    if (object instanceof ObjectRef) // does not work, because ObjectRef.ClassInfo.interfaceNames must not be referenced!
308////                            return true;
309//
310//                      if (object instanceof Uid)
311//                              return true;
312//
313////                    if (clazz.isArray() || Collection.class.isAssignableFrom(clazz) ||Map.class.isAssignableFrom(clazz))
314////                            return false;
315
316                        // TODO do not hard-code the following, but use an advisor-service!
317                        return !clazz.getName().startsWith("co.codewizards.") && !clazz.getName().startsWith("org.");
318                }
319        }
320}