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