001package org.kuali.rice.krad.service.impl;
002
003import com.thoughtworks.xstream.XStream;
004import com.thoughtworks.xstream.converters.MarshallingContext;
005import com.thoughtworks.xstream.converters.UnmarshallingContext;
006import com.thoughtworks.xstream.converters.collections.CollectionConverter;
007import com.thoughtworks.xstream.converters.reflection.ObjectAccessException;
008import com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider;
009import com.thoughtworks.xstream.converters.reflection.ReflectionConverter;
010import com.thoughtworks.xstream.converters.reflection.ReflectionProvider;
011import com.thoughtworks.xstream.io.HierarchicalStreamReader;
012import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
013import com.thoughtworks.xstream.mapper.Mapper;
014import org.apache.commons.logging.Log;
015import org.apache.commons.logging.LogFactory;
016import org.apache.ojb.broker.core.proxy.ListProxyDefaultImpl;
017import org.apache.ojb.broker.core.proxy.SetProxyDefaultImpl;
018import org.kuali.rice.krad.service.KRADServiceLocator;
019import org.kuali.rice.krad.service.PersistenceService;
020import org.kuali.rice.krad.service.XmlObjectSerializerService;
021
022import java.lang.reflect.Field;
023import java.util.ArrayList;
024import java.util.HashSet;
025import java.util.Iterator;
026import java.util.List;
027import java.util.Set;
028
029/**
030 * Created by sheiksalahudeenm on 16/4/15.
031 */
032public class XmlObjectSerializerServiceImpl implements XmlObjectSerializerService {
033    private static final Log LOG = LogFactory.getLog(XmlObjectSerializerServiceImpl.class);
034
035
036    private PersistenceService persistenceService;
037
038    private XStream xstream;
039
040    public XmlObjectSerializerServiceImpl() {
041        xstream = new XStream(new ProxyAwareJavaReflectionProvider());
042        xstream.registerConverter(new ProxyConverter(xstream.getMapper(), xstream.getReflectionProvider()));
043        xstream.addDefaultImplementation(ArrayList.class, ListProxyDefaultImpl.class);
044
045        // register converters so that ListProxyDefaultImpl and SetProxyDefaultImpl are
046        // serialized as ArrayLists and HashSets
047        xstream.registerConverter(new ListProxyDefaultImplConverter(xstream.getMapper()));
048        xstream.registerConverter(new SetProxyDefaultImplConverter(xstream.getMapper()));
049    }
050
051    /**
052     * @see org.kuali.rice.krad.service.XmlObjectSerializer#toXml(java.lang.Object)
053     * @see org.kuali.rice.krad.service.XmlObjectSerializerService#toXml(java.lang.Object)
054     */
055    public String toXml(Object object) {
056        if (LOG.isDebugEnabled()) {
057            LOG.debug("toXml(" + object + ") : \n" + xstream.toXML(object));
058        }
059        return xstream.toXML(object);
060    }
061
062    /**
063     * @see org.kuali.rice.krad.service.XmlObjectSerializer#fromXml(java.lang.String)
064     * @see org.kuali.rice.krad.service.XmlObjectSerializerService#fromXml(java.lang.String)
065     */
066    public Object fromXml(String xml) {
067        if (LOG.isDebugEnabled()) {
068            LOG.debug("fromXml() : \n" + xml);
069        }
070        if (xml != null) {
071            xml = xml.replaceAll("--EnhancerByCGLIB--[0-9a-f]{0,8}", "");
072        }
073        return xstream.fromXML(xml);
074    }
075
076    /**
077     * This custom converter only handles proxies for BusinessObjects.  List-type proxies are handled by configuring XStream to treat
078     * ListProxyDefaultImpl as ArrayLists (see constructor for this service).
079     */
080    public class ProxyConverter extends ReflectionConverter {
081        public ProxyConverter(Mapper mapper, ReflectionProvider reflectionProvider) {
082            super(mapper, reflectionProvider);
083        }
084
085        @Override
086        // since the ReflectionConverter supertype defines canConvert without using a parameterized Class type, we must declare
087        // the overridden version the same way
088        @SuppressWarnings("unchecked")
089        public boolean canConvert(Class clazz) {
090            return clazz.getName().contains("CGLIB");
091        }
092
093        @Override
094        public void marshal(Object obj, HierarchicalStreamWriter writer, MarshallingContext context) {
095            super.marshal(getPersistenceService().resolveProxy(obj), writer, context);
096        }
097
098        // we shouldn't need an unmarshal method because all proxy metadata is taken out of the XML, so we'll reserialize as a base BO.
099    }
100
101    public class ProxyAwareJavaReflectionProvider extends PureJavaReflectionProvider {
102
103        public ProxyAwareJavaReflectionProvider() {
104            super();
105        }
106
107        /**
108         * @see com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider#visitSerializableFields(java.lang.Object, com.thoughtworks.xstream.converters.reflection.ReflectionProvider.Visitor)
109         */
110        @Override
111        public void visitSerializableFields(Object object, Visitor visitor) {
112            for (Iterator iterator = fieldDictionary.fieldsFor(object.getClass()); iterator.hasNext(); ) {
113                Field field = (Field) iterator.next();
114                if (!fieldModifiersSupported(field)) {
115                    continue;
116                }
117                validateFieldAccess(field);
118                Object value = null;
119                try {
120                    value = field.get(object);
121                    if (value != null && getPersistenceService().isProxied(value)) {
122                        value = getPersistenceService().resolveProxy(value);
123                    }
124                } catch (IllegalArgumentException e) {
125                    throw new ObjectAccessException("Could not get field " + field.getClass() + "." + field.getName(), e);
126                } catch (IllegalAccessException e) {
127                    throw new ObjectAccessException("Could not get field " + field.getClass() + "." + field.getName(), e);
128                }
129                visitor.visit(field.getName(), field.getType(), field.getDeclaringClass(), value);
130            }
131        }
132
133
134    }
135
136    public PersistenceService getPersistenceService() {
137        if (persistenceService == null) {
138            persistenceService = KRADServiceLocator.getPersistenceService();
139        }
140        return persistenceService;
141    }
142
143
144    /**
145     * Custom {@link com.thoughtworks.xstream.converters.Converter} that moves elements from a
146     * {@link ListProxyDefaultImpl} into an {@link ArrayList} and marshals that instead.
147     */
148    private static class ListProxyDefaultImplConverter extends CollectionConverter {
149
150        ListProxyDefaultImplConverter(Mapper mapper) {
151            super(mapper);
152        }
153
154        @Override
155        @SuppressWarnings("unchecked")
156        public boolean canConvert(Class type) {
157            return ListProxyDefaultImpl.class.equals(type);
158        }
159
160        /**
161         * moves elements from a {@link ListProxyDefaultImpl} into an {@link ArrayList} and marshals that instead.
162         */
163        @Override
164        @SuppressWarnings("unchecked")
165        public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
166
167            // move data to an ArrayList
168            ArrayList altered = new ArrayList((List) source);
169            // and marshal that
170            super.marshal(altered, writer, context);
171        }
172
173        /**
174         * NOTE: using this class to unmarshal a ListProxyDefaultImpl inside Rice is unexpected, since those should
175         * have been converted to ArrayLists during marshalling. However, in the interest of attempting to
176         * provide a full Converter implementation here, we'll attempt to unmarshal it to an ArrayList by throwing
177         * away all the funny fields that a ListProxyDefaultImpl contains (Helpfully, their names all start with "_")'
178         */
179        @Override
180        @SuppressWarnings("unchecked")
181        public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
182
183            ArrayList result = null;
184
185            while (reader.hasMoreChildren()) {
186                try {
187                    reader.moveDown();
188
189                    if (reader.getNodeName().startsWith("_")) {
190                        // do nothing
191                    } else {
192                        if (result == null) {
193                            result = new ArrayList();
194                        } // lazy init
195                        addCurrentElementToCollection(reader, context, result, result);
196                    }
197                } finally {
198                    reader.moveUp();
199                }
200            }
201
202            return result;
203        }
204    }
205
206
207    /**
208     * Custom {@link com.thoughtworks.xstream.converters.Converter} that moves elements from a
209     * {@link SetProxyDefaultImpl} into a {@link HashSet} and marshals that instead.
210     */
211    private static class SetProxyDefaultImplConverter extends CollectionConverter {
212
213        SetProxyDefaultImplConverter(Mapper mapper) {
214            super(mapper);
215        }
216
217        @Override
218        @SuppressWarnings("unchecked")
219        public boolean canConvert(Class type) {
220            return SetProxyDefaultImpl.class.equals(type);
221        }
222
223        /**
224         * moves elements from a {@link SetProxyDefaultImpl} into a {@link HashSet} and marshals that instead.
225         */
226        @Override
227        @SuppressWarnings("unchecked")
228        public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
229
230            // move data to a HashSet
231            HashSet altered = new HashSet((Set) source);
232            // and marshal that
233            super.marshal(altered, writer, context);
234        }
235
236        /**
237         * NOTE: using this class to unmarshal a SetProxyDefaultImpl inside Rice is unexpected, since those should
238         * have been converted to HashSets during marshalling. However, in the interest of attempting to
239         * provide a full Converter implementation here, we'll attempt to unmarshal it to an HashSet by throwing
240         * away all the funny fields that a SetProxyDefaultImpl contains (Helpfully, their names all start with "_")'
241         */
242        @Override
243        @SuppressWarnings("unchecked")
244        public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
245
246            HashSet result = null;
247
248            while (reader.hasMoreChildren()) {
249                try {
250                    reader.moveDown();
251
252                    if (reader.getNodeName().startsWith("_")) {
253                        // do nothing
254                    } else {
255                        if (result == null) {
256                            result = new HashSet();
257                        } // lazy init
258                        addCurrentElementToCollection(reader, context, result, result);
259                    }
260                } finally {
261                    reader.moveUp();
262                }
263            }
264
265            return result;
266        }
267    }
268}