View Javadoc
1   /**
2    * Copyright 2005-2016 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.service.impl;
17  
18  import com.thoughtworks.xstream.XStream;
19  import com.thoughtworks.xstream.converters.ConverterLookup;
20  import com.thoughtworks.xstream.converters.MarshallingContext;
21  import com.thoughtworks.xstream.converters.UnmarshallingContext;
22  import com.thoughtworks.xstream.converters.collections.CollectionConverter;
23  import com.thoughtworks.xstream.converters.reflection.ObjectAccessException;
24  import com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider;
25  import com.thoughtworks.xstream.converters.reflection.ReflectionConverter;
26  import com.thoughtworks.xstream.converters.reflection.ReflectionProvider;
27  import com.thoughtworks.xstream.core.ReferenceByXPathMarshallingStrategy;
28  import com.thoughtworks.xstream.core.TreeMarshaller;
29  import com.thoughtworks.xstream.io.HierarchicalStreamReader;
30  import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
31  import com.thoughtworks.xstream.io.path.PathTracker;
32  import com.thoughtworks.xstream.mapper.Mapper;
33  import org.apache.commons.lang.StringUtils;
34  import org.apache.commons.lang.reflect.FieldUtils;
35  import org.kuali.rice.krad.document.Document;
36  import org.kuali.rice.krad.service.DocumentSerializerService;
37  import org.kuali.rice.krad.service.LegacyDataAdapter;
38  import org.kuali.rice.krad.service.SerializerService;
39  import org.kuali.rice.krad.service.XmlObjectSerializerService;
40  import org.kuali.rice.krad.service.util.DateTimeConverter;
41  import org.kuali.rice.krad.util.documentserializer.AlwaysTruePropertySerializibilityEvaluator;
42  import org.kuali.rice.krad.util.documentserializer.PropertySerializabilityEvaluator;
43  import org.kuali.rice.krad.util.documentserializer.PropertyType;
44  import org.kuali.rice.krad.util.documentserializer.SerializationState;
45  import org.springframework.beans.factory.annotation.Required;
46  import org.springframework.util.AutoPopulatingList;
47  
48  import java.lang.reflect.Field;
49  import java.util.ArrayList;
50  import java.util.HashMap;
51  import java.util.Iterator;
52  import java.util.List;
53  import java.util.Map;
54  
55  /**
56   * Default implementation of the {@link DocumentSerializerService}.  If no <workflowProperties> have been defined in the
57   * data dictionary for a document type (i.e. {@link Document#getDocumentPropertySerizabilityEvaluator()} returns an instance of
58   * {@link AlwaysTruePropertySerializibilityEvaluator}), then this service will revert to using the {@link XmlObjectSerializerService}
59   * bean, which was the old way of serializing a document for routing.  If workflowProperties are defined, then this implementation
60   * will selectively serialize items.
61   *
62   * @author Kuali Rice Team (rice.collab@kuali.org)
63   */
64  public abstract class SerializerServiceBase implements SerializerService  {
65  
66  	protected LegacyDataAdapter legacyDataAdapter;
67      protected XmlObjectSerializerService xmlObjectSerializerService;
68  
69      protected XStream xstream;
70  
71      // ThreadLocals to track state during serialization
72  
73      protected ThreadLocal<PropertySerializabilityEvaluator> evaluators;
74      protected ThreadLocal<Map<String, SerializationState>> pathToSerializationState;
75      protected ThreadLocal<PathTracker> currentPathTracker;
76  
77      public SerializerServiceBase() {
78          evaluators = new ThreadLocal<>();
79          currentPathTracker = new ThreadLocal<>();
80          pathToSerializationState = new ThreadLocal<>();
81  
82          xstream = new XStream(new ProxyAndStateAwareJavaReflectionProvider());
83          xstream.setMarshallingStrategy(new PathTrackerSmugglingMarshallingStrategy(currentPathTracker));
84          xstream.registerConverter(new ProxyConverter(xstream.getMapper(), xstream.getReflectionProvider() ));
85          try {
86          	Class<?> objListProxyClass = Class.forName("org.apache.ojb.broker.core.proxy.ListProxyDefaultImpl");
87              xstream.addDefaultImplementation(ArrayList.class, objListProxyClass);
88              xstream.addDefaultImplementation(AutoPopulatingList.class, objListProxyClass);
89          } catch ( Exception ex ) {
90          	// Do nothing - this will blow if the OJB class does not exist, which it won't in some installs
91          }
92          xstream.registerConverter(new AutoPopulatingListConverter(xstream.getMapper()));
93          xstream.registerConverter(new DateTimeConverter());
94      }
95  
96      /**
97       * Execute the specified {@link Serializer} with the appropriate setup and tear down, and return the serialized XML
98       * when done.
99       */
100     protected <T> String doSerialization(PropertySerializabilityEvaluator evaluator, T object, Serializer<T> serializer) {
101         try {
102             evaluators.set(evaluator);
103             pathToSerializationState.set(new HashMap<String, SerializationState>());
104             currentPathTracker.set(null);
105             return serializer.serialize(object);
106         } finally {
107             evaluators.set(null);
108             pathToSerializationState.set(null);
109             currentPathTracker.set(null);
110         }
111     }
112 
113     public String serializeBusinessObjectToXml(Object businessObject) {
114         final PropertySerializabilityEvaluator propertySerizabilityEvaluator =
115                 getPropertySerizabilityEvaluator(businessObject);
116         return doSerialization(propertySerizabilityEvaluator, businessObject, new Serializer<Object>() {
117             @Override
118             public String serialize(Object object) {
119                 String xml;
120                 if (propertySerizabilityEvaluator instanceof AlwaysTruePropertySerializibilityEvaluator) {
121                     xml = getXmlObjectSerializerService().toXml(object);
122                 } else {
123                     xml = xstream.toXML(object);
124                 }
125                 return xml;
126             }
127         });
128     }
129 
130     /**
131      * Method called by the ProxyAndStateAwareJavaReflectionProvider during serialization to determine if a field
132      * should be omitted from the serialized form.
133      *
134      * <p>This is a short circuit check that will avoid more expensive calls in to the PropertySerializabilityEvaluator
135      * if it returns true.</p>
136      *
137      * @param field the field
138      * @return true if the field should be omitted
139      */
140     protected boolean ignoreField(Field field) {
141         return false;
142     }
143 
144     /**
145      * Get the appropriate {@link PropertySerializabilityEvaluator} for the given dataObject.
146      *
147      * @param dataObject the data object
148      * @return the evaluator
149      */
150     protected abstract PropertySerializabilityEvaluator getPropertySerizabilityEvaluator(Object dataObject);
151 
152     protected PathTracker getCurrentPathTracker() {
153         PathTracker pathTracker = currentPathTracker.get();
154         if (pathTracker == null) {
155             throw new IllegalStateException("No XStream PathTracker is bound to the current thread");
156         }
157         return pathTracker;
158     }
159 
160     /**
161      * Parse the given explicit XPath expression to find the path to the parent XML element.
162      *
163      * @param pathString
164      * @return the parent path, or empty string if the given path represents the root path of the xml document
165      * @throws IllegalArgumentException if the given path is not a valid path (i.e. doesn't contain a "/")
166      */
167     private String parseParentPath(String pathString) {
168         int indexOfLastSlash = pathString.lastIndexOf("/");
169         if (indexOfLastSlash == -1) {
170             throw new IllegalArgumentException("Expected a path");
171         }
172         return pathString.substring(0, indexOfLastSlash);
173     }
174 
175     /**
176      * Returns the SerializationState for the given path string
177      */
178     private SerializationState determineSerializationState(String pathString) {
179         if (pathToSerializationState.get().isEmpty()) {
180             pathToSerializationState.get().put(pathString, new SerializationState());
181         }
182         return searchSerializationState(pathString, pathString);
183     }
184 
185     /**
186      * Attempts to find the SerializationState for the given path string, searching the parent paths if none found
187      */
188     private SerializationState searchSerializationState(String pathString, String originalPath) {
189         if (StringUtils.isBlank(pathString)) {
190             throw new IllegalStateException("Failed to find existing SerializationState for path: " + originalPath);
191         }
192         SerializationState state = pathToSerializationState.get().get(pathString);
193         return state != null ? state : searchSerializationState(parseParentPath(pathString), originalPath);
194     }
195 
196     /**
197      * Records the given serialization state if it is not already registered.
198      */
199     private void registerSerializationStateForField(SerializationState state, String fieldName, PropertyType propertyType, String parentPath) {
200         String path = parentPath + "/" + fieldName;
201         if (pathToSerializationState.get().get(path) == null) {
202             SerializationState newState = new SerializationState(state);
203             newState.addSerializedProperty(fieldName, propertyType);
204             pathToSerializationState.get().put(path, newState);
205         }
206     }
207 
208     /**
209      * A simple functional interface that defines a method which executes serialization to an XML string
210      */
211     protected interface Serializer<T> {
212         String serialize(T object);
213     }
214 
215     public class ProxyConverter extends ReflectionConverter {
216         public ProxyConverter(Mapper mapper, ReflectionProvider reflectionProvider) {
217             super(mapper, reflectionProvider);
218         }
219         @Override
220 		public boolean canConvert(Class clazz) {
221             return clazz.getName().contains("CGLIB") || clazz.getName().equals("org.apache.ojb.broker.core.proxy.ListProxyDefaultImpl");
222         }
223 
224         @Override
225 		public void marshal(Object obj, HierarchicalStreamWriter writer, MarshallingContext context) {
226             if (obj.getClass().getName().equals("org.apache.ojb.broker.core.proxy.ListProxyDefaultImpl")) {
227                 List copiedList = new ArrayList();
228                 List proxiedList = (List) obj;
229                 for (Iterator iter = proxiedList.iterator(); iter.hasNext();) {
230                     copiedList.add(iter.next());
231                 }
232                 context.convertAnother( copiedList );
233             }
234             else {
235                 super.marshal(legacyDataAdapter.resolveProxy(obj), writer, context);
236             }
237         }
238 
239         @Override
240 		public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
241             return null;
242         }
243     }
244 
245     public class ProxyAndStateAwareJavaReflectionProvider extends PureJavaReflectionProvider {
246         @Override
247         public void visitSerializableFields(Object object, Visitor visitor) {
248             PathTracker pathTracker = getCurrentPathTracker();
249             PropertySerializabilityEvaluator evaluator = evaluators.get();
250             String currentPath = pathTracker.getPath().toString();
251             SerializationState state = determineSerializationState(currentPath);
252 
253 
254             for (Iterator iterator = fieldDictionary.serializableFieldsFor(object.getClass()); iterator.hasNext();) {
255                 Field field = (Field) iterator.next();
256                 if (!fieldModifiersSupported(field)) {
257                     continue;
258                 }
259 
260                 if (ignoreField(field)) {
261                     continue;
262                 }
263 
264                 validateFieldAccess(field);
265 
266                 initializeField(object, field);
267 
268                 Object value = null;
269                 try {
270                     value = field.get(object);
271                 } catch (IllegalArgumentException e) {
272                     throw new ObjectAccessException("Could not get field " + field.getClass() + "." + field.getName(), e);
273                 } catch (IllegalAccessException e) {
274                     throw new ObjectAccessException("Could not get field " + field.getClass() + "." + field.getName(), e);
275                 }
276 
277                 if (evaluator.isPropertySerializable(state, object, field.getName(), value)) {
278                     if (value != null && legacyDataAdapter.isProxied(value)) {
279                         // resolve proxies after we determine that it's serializable
280                         value = legacyDataAdapter.resolveProxy(value);
281                     }
282                     PropertyType propertyType = evaluator.determinePropertyType(value);
283                     registerSerializationStateForField(state, field.getName(), propertyType, currentPath);
284                     visitor.visit(field.getName(), field.getType(), field.getDeclaringClass(), value);
285                 }
286             }
287         }
288 
289         protected void initializeField(Object object, Field field) {
290         }
291     }
292 
293     public class AutoPopulatingListConverter extends CollectionConverter {
294 
295     	public AutoPopulatingListConverter(Mapper mapper){
296     		super(mapper);
297     	}
298 
299         @Override
300     	public boolean canConvert(Class clazz) {
301     		return clazz.equals(AutoPopulatingList.class);
302         }
303 
304     }
305 
306     private static class PathTrackerSmugglingMarshallingStrategy extends ReferenceByXPathMarshallingStrategy {
307 
308         private final ThreadLocal<PathTracker> pathTrackerThreadLocal;
309 
310         public PathTrackerSmugglingMarshallingStrategy(ThreadLocal<PathTracker> pathTrackerThreadLocal) {
311             super(ReferenceByXPathMarshallingStrategy.RELATIVE);
312             this.pathTrackerThreadLocal = pathTrackerThreadLocal;
313         }
314 
315         @Override
316         protected TreeMarshaller createMarshallingContext(HierarchicalStreamWriter writer, ConverterLookup converterLookup, Mapper mapper) {
317             TreeMarshaller treeMarshaller = super.createMarshallingContext(writer, converterLookup, mapper);
318             smugglePathTracker(treeMarshaller);
319             return treeMarshaller;
320         }
321 
322         /**
323          * Shhh...don't tell anybody, but we are going to smuggle the PathTracker out of here so we can
324          * reference it during marshalling in our custom reflection provider.
325          *
326          * This is really an XStream internal API so has the potential to break us horribly if they change the
327          * implementation in the future. We are betting on our unit tests catching that if it happens.
328          */
329         private void smugglePathTracker(TreeMarshaller treeMarshaller) {
330             try {
331                 PathTracker pathTracker = (PathTracker)FieldUtils.readField(treeMarshaller, "pathTracker", true);
332                 if (pathTracker == null) {
333                     throw new IllegalStateException("The pathTracker on xstream marshaller is null");
334                 }
335                 this.pathTrackerThreadLocal.set(pathTracker);
336             } catch (IllegalAccessException e) {
337                 throw new IllegalStateException(e);
338             }
339         }
340 
341     }
342 
343     protected XmlObjectSerializerService getXmlObjectSerializerService() {
344         return this.xmlObjectSerializerService;
345     }
346 
347     @Required
348     public void setXmlObjectSerializerService(XmlObjectSerializerService xmlObjectSerializerService) {
349         this.xmlObjectSerializerService = xmlObjectSerializerService;
350     }
351 
352     @Required
353 	public void setLegacyDataAdapter(LegacyDataAdapter legacyDataAdapter) {
354 		this.legacyDataAdapter = legacyDataAdapter;
355 	}
356 }
357