1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37 package com.sun.jersey.wadl.resourcedoc;
38
39 import com.atlassian.plugins.rest.common.expand.EntityCrawler;
40 import com.atlassian.plugins.rest.common.expand.parameter.DefaultExpandParameter;
41 import com.atlassian.plugins.rest.common.expand.resolver.ChainingEntityExpanderResolver;
42 import com.atlassian.plugins.rest.common.expand.resolver.CollectionEntityExpanderResolver;
43 import com.atlassian.plugins.rest.common.expand.resolver.EntityExpanderResolver;
44 import com.atlassian.plugins.rest.common.expand.resolver.ExpandConstraintEntityExpanderResolver;
45 import com.atlassian.plugins.rest.common.expand.resolver.IdentityEntityExpanderResolver;
46 import com.atlassian.plugins.rest.common.expand.resolver.ListWrapperEntityExpanderResolver;
47 import com.atlassian.plugins.rest.common.json.DefaultJaxbJsonMarshaller;
48 import com.google.common.collect.Lists;
49 import com.sun.javadoc.AnnotationDesc;
50 import com.sun.javadoc.AnnotationDesc.ElementValuePair;
51 import com.sun.javadoc.ClassDoc;
52 import com.sun.javadoc.DocErrorReporter;
53 import com.sun.javadoc.MemberDoc;
54 import com.sun.javadoc.MethodDoc;
55 import com.sun.javadoc.ParamTag;
56 import com.sun.javadoc.Parameter;
57 import com.sun.javadoc.RootDoc;
58 import com.sun.javadoc.SeeTag;
59 import com.sun.javadoc.Tag;
60 import com.sun.jersey.server.wadl.generators.resourcedoc.model.AnnotationDocType;
61 import com.sun.jersey.server.wadl.generators.resourcedoc.model.ClassDocType;
62 import com.sun.jersey.server.wadl.generators.resourcedoc.model.MethodDocType;
63 import com.sun.jersey.server.wadl.generators.resourcedoc.model.NamedValueType;
64 import com.sun.jersey.server.wadl.generators.resourcedoc.model.ParamDocType;
65 import com.sun.jersey.server.wadl.generators.resourcedoc.model.RepresentationDocType;
66 import com.sun.jersey.server.wadl.generators.resourcedoc.model.RequestDocType;
67 import com.sun.jersey.server.wadl.generators.resourcedoc.model.ResourceDocType;
68 import com.sun.jersey.server.wadl.generators.resourcedoc.model.ResponseDocType;
69 import com.sun.jersey.server.wadl.generators.resourcedoc.model.WadlParamType;
70 import org.apache.commons.lang.StringUtils;
71 import org.apache.xml.serialize.OutputFormat;
72 import org.apache.xml.serialize.XMLSerializer;
73
74 import java.io.BufferedOutputStream;
75 import java.io.File;
76 import java.io.FileOutputStream;
77 import java.io.IOException;
78 import java.io.OutputStream;
79 import java.lang.reflect.Array;
80 import java.lang.reflect.Field;
81 import java.net.MalformedURLException;
82 import java.net.URL;
83 import java.net.URLClassLoader;
84 import java.util.ArrayList;
85 import java.util.Arrays;
86 import java.util.Collection;
87 import java.util.HashMap;
88 import java.util.Iterator;
89 import java.util.List;
90 import java.util.Map;
91 import java.util.Map.Entry;
92 import java.util.logging.Level;
93 import java.util.logging.Logger;
94 import java.util.regex.Matcher;
95 import java.util.regex.Pattern;
96 import javax.xml.bind.JAXBContext;
97 import javax.xml.bind.JAXBException;
98 import javax.xml.bind.Marshaller;
99 import javax.xml.namespace.QName;
100
101 import static java.util.Collections.emptyList;
102
103
104
105
106
107
108
109
110
111
112 public class ResourceDocletJSON
113 {
114
115 private static final Pattern PATTERN_RESPONSE_REPRESENATION = Pattern.compile( "@response\\.representation\\.([\\d]+)\\..*" );
116 private static final String OPTION_OUTPUT = "-output";
117 private static final String OPTION_CLASSPATH = "-classpath";
118 private static final String OPTION_DOC_PROCESSORS = "-processors";
119
120 private static final Logger LOG = Logger.getLogger( ResourceDocletJSON.class
121 .getName() );
122
123 private static final String OPTION_EXPAND = "-expand";
124
125
126
127
128
129
130
131 public static boolean start( RootDoc root ) {
132 final String output = getOptionArg( root.options(), OPTION_OUTPUT );
133
134 final String classpath = getOptionArg( root.options(), OPTION_CLASSPATH );
135
136 final String[] classpathElements = classpath.split( ":" );
137
138 final String expandString = getOptionArg( root.options(), OPTION_EXPAND );
139 Collection<String> expand = emptyList();
140 if (expandString != null) {
141 expand = Arrays.asList(StringUtils.split(expandString, '&'));
142 }
143
144 final ClassLoader cl = Thread.currentThread().getContextClassLoader();
145 final ClassLoader ncl = new Loader( classpathElements,
146 ResourceDocletJSON.class.getClassLoader() );
147 Thread.currentThread().setContextClassLoader( ncl );
148
149
150 final String docProcessorOption = getOptionArg( root.options(), OPTION_DOC_PROCESSORS );
151 final String[] docProcessors = docProcessorOption != null ? docProcessorOption.split( ":" ) : null;
152 final DocProcessorWrapper docProcessor = new DocProcessorWrapper();
153 try {
154 if ( docProcessors != null && docProcessors.length > 0 ) {
155 final Class<?> clazz = Class.forName( docProcessors[0], true, Thread.currentThread().getContextClassLoader() );
156 final Class<? extends DocProcessor> dpClazz = clazz.asSubclass( DocProcessor.class );
157 docProcessor.add( dpClazz.newInstance() );
158 }
159 } catch ( Exception e ) {
160 LOG.log( Level.SEVERE, "Could not load docProcessors " + docProcessorOption, e );
161 }
162
163 try {
164 final ResourceDocType result = new ResourceDocType();
165
166 final ClassDoc[] classes = root.classes();
167 for ( ClassDoc classDoc : classes ) {
168 LOG.fine( "Writing class " + classDoc.qualifiedTypeName() );
169 final ClassDocType classDocType = new ClassDocType();
170 classDocType.setClassName( classDoc.qualifiedTypeName() );
171 classDocType.setCommentText( classDoc.commentText() );
172 docProcessor.processClassDoc( classDoc, classDocType );
173
174 for ( MethodDoc methodDoc : classDoc.methods() ) {
175
176 final MethodDocType methodDocType = new MethodDocType();
177 methodDocType.setMethodName( methodDoc.name() );
178 methodDocType.setCommentText( methodDoc.commentText() );
179 docProcessor.processMethodDoc( methodDoc, methodDocType );
180
181 addParamDocs( methodDoc, methodDocType, docProcessor );
182
183 addRequestRepresentationDoc( methodDoc, methodDocType, expand);
184
185 addResponseDoc( methodDoc, methodDocType, expand);
186
187 classDocType.getMethodDocs().add( methodDocType );
188 }
189
190 result.getDocs().add( classDocType );
191 }
192
193 try {
194 final Class<?>[] clazzes = getJAXBContextClasses( result, docProcessor );
195 final JAXBContext c = JAXBContext.newInstance( clazzes );
196 final Marshaller m = c.createMarshaller();
197 m.setProperty( Marshaller.JAXB_FORMATTED_OUTPUT, true );
198 final OutputStream out = new BufferedOutputStream( new FileOutputStream( output ) );
199
200
201 final String[] cdataElements = getCDataElements( docProcessor );
202 final XMLSerializer serializer = getXMLSerializer( out, cdataElements );
203
204 m.marshal( result, serializer );
205 out.close();
206
207 LOG.info( "Wrote " + output );
208
209 } catch (Exception e) {
210 LOG.log( Level.SEVERE, "Could not serialize ResourceDoc.", e );
211 return false;
212 }
213 } finally {
214 Thread.currentThread().setContextClassLoader( cl );
215 }
216
217 return true;
218 }
219
220 private static String[] getCDataElements( DocProcessor docProcessor ) {
221 final String[] original = new String[] { "ns1^commentText", "ns2^commentText", "^commentText" };
222 if ( docProcessor == null ) {
223 return original;
224 }
225 else {
226 final String[] cdataElements = docProcessor.getCDataElements();
227 if ( cdataElements == null || cdataElements.length == 0 ) {
228 return original;
229 }
230 else {
231
232 final String[] result = copyOf( original, original.length + cdataElements.length );
233 for ( int i = 0; i < cdataElements.length; i++ ) {
234 result[ original.length + i ] = cdataElements[i];
235 }
236 return result;
237 }
238 }
239 }
240
241 @SuppressWarnings("unchecked")
242 private static <T,U> T[] copyOf( U[] original, int newLength ) {
243 final T[] copy = ((Object)original.getClass() == (Object)Object[].class)
244 ? (T[]) new Object[newLength]
245 : (T[]) Array.newInstance(original.getClass().getComponentType(), newLength);
246 System.arraycopy(original, 0, copy, 0,
247 Math.min(original.length, newLength));
248 return copy;
249 }
250
251 private static Class<?>[] getJAXBContextClasses(
252 final ResourceDocType result, DocProcessor docProcessor ) {
253 final Class<?>[] clazzes;
254 if ( docProcessor == null ) {
255 clazzes = new Class<?>[1];
256 }
257 else {
258 final Class<?>[] requiredJaxbContextClasses = docProcessor.getRequiredJaxbContextClasses();
259 if ( requiredJaxbContextClasses != null ) {
260 clazzes = new Class<?>[1 + requiredJaxbContextClasses.length ];
261 for ( int i = 0; i < requiredJaxbContextClasses.length; i++ ) {
262 clazzes[i + 1] = requiredJaxbContextClasses[i];
263 }
264 }
265 else {
266 clazzes = new Class<?>[1];
267 }
268 }
269 clazzes[0] = result.getClass();
270 return clazzes;
271 }
272
273 private static XMLSerializer getXMLSerializer( OutputStream os, String[] cdataElements ) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
274
275 OutputFormat of = new OutputFormat();
276
277
278
279
280
281
282 of.setCDataElements( cdataElements );
283
284
285 of.setPreserveSpace(true);
286 of.setIndenting(true);
287
288
289 XMLSerializer serializer = new XMLSerializer(of);
290
291 serializer.setOutputByteStream( os );
292
293 return serializer;
294 }
295
296 private static void addResponseDoc(MethodDoc methodDoc,
297 final MethodDocType methodDocType, Collection<String> expand) {
298
299 final ResponseDocType responseDoc = new ResponseDocType();
300
301 final Tag returnTag = getSingleTagOrNull( methodDoc, "return" );
302 if ( returnTag != null ) {
303 responseDoc.setReturnDoc( returnTag.text() );
304 }
305
306 final Tag[] responseParamTags = methodDoc.tags( "response.param" );
307 for ( Tag responseParamTag : responseParamTags ) {
308
309 final WadlParamType wadlParam = new WadlParamType();
310 for ( Tag inlineTag : responseParamTag.inlineTags() ) {
311 final String tagName = inlineTag.name();
312 final String tagText = inlineTag.text();
313
314
315 if ( isEmpty( tagText ) ) {
316 if ( LOG.isLoggable( Level.FINE ) ) {
317 LOG.fine( "Skipping empty inline tag of @response.param in method " +
318 methodDoc.qualifiedName() + ": " + tagName );
319 }
320 continue;
321 }
322 if ( "@name".equals( tagName ) ) {
323 wadlParam.setName( tagText );
324 }
325 else if ( "@style".equals( tagName ) ) {
326 wadlParam.setStyle( tagText );
327 }
328 else if ( "@type".equals( tagName ) ) {
329 wadlParam.setType( QName.valueOf( tagText ) );
330 }
331 else if ( "@doc".equals( tagName ) ) {
332 wadlParam.setDoc( tagText );
333 }
334 else {
335 LOG.warning( "Unknown inline tag of @response.param in method " +
336 methodDoc.qualifiedName() + ": " + tagName +
337 " (value: " + tagText + ")" );
338 }
339 }
340 responseDoc.getWadlParams().add( wadlParam );
341 }
342
343 final Map<String, List<Tag>> tagsByStatus = getResponseRepresentationTags( methodDoc );
344 for ( Entry<String, List<Tag>> entry : tagsByStatus.entrySet() ) {
345 final RepresentationDocType representationDoc = new RepresentationDocType();
346 representationDoc.setStatus( Long.valueOf( entry.getKey() ) );
347 for ( Tag tag : entry.getValue() ) {
348 if ( tag.name().endsWith( ".qname" ) ) {
349 representationDoc.setElement( QName.valueOf( tag.text() ) );
350 }
351 else if ( tag.name().endsWith( ".mediaType" ) ) {
352 representationDoc.setMediaType( tag.text() );
353 }
354 else if ( tag.name().endsWith( ".example" ) ) {
355 representationDoc.setExample( getSerializedExample( tag, expand) );
356 }
357 else if ( tag.name().endsWith( ".doc" ) ) {
358 representationDoc.setDoc( tag.text() );
359 }
360 else {
361 LOG.warning( "Unknown response representation tag " + tag.name() );
362 }
363 }
364 responseDoc.getRepresentations().add( representationDoc );
365 }
366
367 methodDocType.setResponseDoc( responseDoc );
368 }
369
370 private static boolean isEmpty( String value ) {
371 return value == null || value.length() == 0 || value.trim().length() == 0 ? true : false;
372 }
373
374 private static void addRequestRepresentationDoc(MethodDoc methodDoc,
375 final MethodDocType methodDocType, Collection<String> expand) {
376 final Tag requestElement = getSingleTagOrNull( methodDoc, "request.representation.qname" );
377 final Tag requestExample = getSingleTagOrNull( methodDoc, "request.representation.example" );
378 if ( requestElement != null || requestExample != null ) {
379 final RequestDocType requestDoc = new RequestDocType();
380 final RepresentationDocType representationDoc = new RepresentationDocType();
381
382
383
384 if ( requestElement != null ) {
385 representationDoc.setElement( QName.valueOf( requestElement.text() ) );
386 }
387
388
389
390 if ( requestExample != null ) {
391 final String example = getSerializedExample( requestExample, expand);
392 if ( !isEmpty( example ) ) {
393 representationDoc.setExample( example );
394 }
395 else {
396 LOG.warning( "Could not get serialized example for method " + methodDoc.qualifiedName() );
397 }
398 }
399
400 requestDoc.setRepresentationDoc( representationDoc );
401 methodDocType.setRequestDoc( requestDoc );
402 }
403 }
404
405 private static Map<String, List<Tag>> getResponseRepresentationTags(
406 MethodDoc methodDoc ) {
407 final Map<String,List<Tag>> tagsByStatus = new HashMap<String, List<Tag>>();
408 for ( Tag tag : methodDoc.tags() ) {
409 final Matcher matcher = PATTERN_RESPONSE_REPRESENATION.matcher( tag.name() );
410 if ( matcher.matches() ) {
411 final String status = matcher.group( 1 );
412 List<Tag> tags = tagsByStatus.get( status );
413 if ( tags == null ) {
414 tags = new ArrayList<Tag>();
415 tagsByStatus.put( status, tags );
416 }
417 tags.add( tag );
418 }
419 }
420 return tagsByStatus;
421 }
422
423
424
425
426
427
428
429
430
431 private static String getSerializedExample(Tag tag, Collection<String> expand) {
432 if ( tag != null ) {
433 final Tag[] inlineTags = tag.inlineTags();
434 if ( inlineTags != null && inlineTags.length > 0 ) {
435 for ( Tag inlineTag : inlineTags ) {
436 if ( LOG.isLoggable( Level.FINE ) ) {
437 LOG.fine( "Have inline tag: " + print( inlineTag ) );
438 }
439 if ( "@link".equals( inlineTag.name() ) ) {
440 if ( LOG.isLoggable( Level.FINE ) ) {
441 LOG.fine( "Have link: " + print( inlineTag ) );
442 }
443 final SeeTag linkTag = (SeeTag) inlineTag;
444 return getSerializedLinkFromTag( linkTag, expand);
445 }
446 else if ( !isEmpty( inlineTag.text() ) ) {
447 return inlineTag.text();
448 }
449 }
450 }
451 else {
452 LOG.fine( "Have example: " + print( tag ) );
453 return tag.text();
454 }
455 }
456 return null;
457 }
458
459 private static Tag getSingleTagOrNull( MethodDoc methodDoc, String tagName ) {
460 final Tag[] tags = methodDoc.tags( tagName );
461 if ( tags != null && tags.length == 1 ) {
462 return tags[0];
463 }
464 return null;
465 }
466
467 private static void addParamDocs( MethodDoc methodDoc, final MethodDocType methodDocType, final DocProcessor docProcessor ) {
468
469 final Parameter[] parameters = methodDoc.parameters();
470 final ParamTag[] paramTags = methodDoc.paramTags();
471
472 if ( parameters != null && paramTags != null) {
473
474 Map<String, Parameter> params = new HashMap<String, Parameter>(parameters.length) {{
475 for (Parameter parameter : parameters)
476 {
477 put(parameter.name(), parameter);
478 }
479 }};
480
481 Map<String, ParamTag> tags = new HashMap<String, ParamTag>(parameters.length) {{
482 for (ParamTag paramTag : paramTags)
483 {
484 put(paramTag.parameterName(), paramTag);
485 }
486 }};
487
488 for (Entry<String, Parameter> parameterEntry : params.entrySet())
489 {
490 Parameter parameter = parameterEntry.getValue();
491 ParamTag paramTag = tags.get(parameterEntry.getKey());
492 if (paramTag != null) {
493
494 final ParamDocType paramDocType = new ParamDocType();
495 paramDocType.setParamName( paramTag.parameterName() );
496 paramDocType.setCommentText( paramTag.parameterComment() );
497 docProcessor.processParamTag( paramTag, parameter, paramDocType );
498
499 AnnotationDesc[] annotations = parameter.annotations();
500 if ( annotations != null ) {
501 for ( AnnotationDesc annotationDesc : annotations ) {
502 final AnnotationDocType annotationDocType = new AnnotationDocType();
503 final String typeName = annotationDesc.annotationType().qualifiedName();
504 annotationDocType.setAnnotationTypeName( typeName );
505 for ( ElementValuePair elementValuePair : annotationDesc.elementValues() ) {
506 final NamedValueType namedValueType = new NamedValueType();
507 namedValueType.setName( elementValuePair.element().name() );
508 namedValueType.setValue( elementValuePair.value().value().toString() );
509 annotationDocType.getAttributeDocs().add( namedValueType );
510 }
511 paramDocType.getAnnotationDocs().add( annotationDocType );
512 }
513 }
514
515 methodDocType.getParamDocs().add( paramDocType );
516 }
517 }
518 }
519 }
520
521 private static String getSerializedLinkFromTag(final SeeTag linkTag, Collection<String> expand) {
522 final MemberDoc referencedMember = linkTag.referencedMember();
523
524 if ( referencedMember == null ) {
525 LOG.warning("Referenced member of @link "+ print( linkTag ) +" cannot be resolved." );
526 return null;
527 }
528
529 if ( !referencedMember.isStatic() ) {
530 LOG.warning( "Referenced member of @link "+ print( linkTag ) +" is not static." +
531 " Right now only references to static members are supported." );
532 return null;
533 }
534
535
536
537 final ClassDoc containingClass = referencedMember.containingClass();
538 final Object object;
539 try {
540 Field declaredField = Class.forName( containingClass.qualifiedName(), false, Thread.currentThread().getContextClassLoader() ).getDeclaredField( referencedMember.name() );
541 if ( referencedMember.isFinal() ) {
542 declaredField.setAccessible( true );
543 }
544 object = declaredField.get( null );
545 LOG.log( Level.FINE, "Got object " + object );
546 } catch ( Exception e ) {
547 LOG.info( "Have classloader: " + ResourceDocletJSON.class.getClassLoader().getClass() );
548 LOG.info( "Have thread classloader " + Thread.currentThread().getContextClassLoader().getClass() );
549 LOG.info( "Have system classloader " + ClassLoader.getSystemClassLoader().getClass() );
550 LOG.log( Level.SEVERE, "Could not get field " + referencedMember.qualifiedName(), e );
551 return null;
552 }
553
554
555
556 try {
557 return marshallBean(object, expand);
558 } catch ( Exception e ) {
559 LOG.log( Level.SEVERE, "Could serialize bean to xml: " + object, e );
560 return null;
561 }
562 }
563
564 private static String marshallBean(Object object, Collection<String> expand) throws JAXBException, IOException
565 {
566
567 if (!expand.isEmpty()) {
568 new EntityCrawler().crawl(object, new DefaultExpandParameter(expand), getExpanders());
569 }
570
571 DefaultJaxbJsonMarshaller m = new DefaultJaxbJsonMarshaller();
572 final String result = m.marshal(object);
573 LOG.log(Level.FINE, "Got marshalled output:\n" + result);
574 return result;
575 }
576
577 private static String print( Tag tag ) {
578 final StringBuilder sb = new StringBuilder();
579 sb.append( tag.getClass() ).append( "[" );
580 sb.append( "firstSentenceTags=" ).append( toCSV( tag.firstSentenceTags() ) );
581 sb.append( ", inlineTags=" ).append( toCSV( tag.inlineTags() ) );
582 sb.append( ", kind=" ).append( tag.kind() );
583 sb.append( ", name=" ).append( tag.name() );
584 sb.append( ", text=" ).append( tag.text() );
585 sb.append( "]" );
586 return sb.toString();
587 }
588
589 static <T> String toCSV( Tag[] items ) {
590 if ( items == null ) {
591 return null;
592 }
593 return toCSV( Arrays.asList( items ) );
594 }
595
596 static <I> String toCSV( Collection<Tag> items ) {
597 return toCSV( items, ", ", null );
598 }
599
600 static <I> String toCSV( Collection<Tag> items, String separator, String delimiter ) {
601 if ( items == null ) {
602 return null;
603 }
604 if ( items.isEmpty() ) {
605 return "";
606 }
607 final StringBuilder sb = new StringBuilder();
608 for ( final Iterator<Tag> iter = items.iterator(); iter.hasNext(); ) {
609 if ( delimiter != null ) {
610 sb.append( delimiter );
611 }
612 final Tag item = iter.next();
613 sb.append( item.name() );
614 if ( delimiter != null ) {
615 sb.append( delimiter );
616 }
617 if ( iter.hasNext() ) {
618 sb.append( separator );
619 }
620 }
621 return sb.toString();
622 }
623
624
625
626
627
628
629
630
631 public static int optionLength( String option ) {
632 LOG.fine( "Invoked with option " + option );
633
634 if ( OPTION_OUTPUT.equals( option )
635 || OPTION_CLASSPATH.equals( option )
636 || OPTION_DOC_PROCESSORS.equals( option )
637 || OPTION_EXPAND.equals(option) ) {
638 return 2;
639 }
640
641 return 0;
642 }
643
644
645
646
647
648
649
650
651 public static boolean validOptions( String[][] options, DocErrorReporter reporter ) {
652 return validOption( OPTION_OUTPUT, "<path-to-file>", options, reporter )
653 && validOption( OPTION_CLASSPATH, "<path>", options, reporter );
654 }
655
656 private static boolean validOption( String optionName,
657 String reportOptionName,
658 String[][] options,
659 DocErrorReporter reporter ) {
660 final String option = getOptionArg( options, optionName );
661
662 final boolean foundOption = option != null && option.trim().length() > 0;
663 if ( !foundOption ) {
664 reporter.printError( optionName + " "+ reportOptionName +" must be specified." );
665 }
666 return foundOption;
667 }
668
669 private static String getOptionArg( String[][] options, String option ) {
670
671 for ( int i = 0; i < options.length; i++ ) {
672 String[] opt = options[i];
673
674 if ( opt[0].equals( option ) ) {
675 return opt[1];
676 }
677 }
678
679 return null;
680 }
681
682 static class Loader extends URLClassLoader {
683
684 public Loader(String[] paths, ClassLoader parent) {
685 super(getURLs(paths), parent);
686 }
687
688 Loader(String[] paths) {
689 super(getURLs(paths));
690 }
691
692 private static URL[] getURLs(String[] paths) {
693 final List<URL> urls = new ArrayList<URL>();
694 for (String path: paths) {
695 try {
696 urls.add(new File(path).toURI().toURL());
697 } catch (MalformedURLException e) {
698 throw new RuntimeException(e);
699 }
700 }
701 final URL[] us = urls.toArray(new URL[0]);
702 return us;
703 }
704
705 }
706
707 private static EntityExpanderResolver getExpanders()
708 {
709 return new ChainingEntityExpanderResolver(Lists.<EntityExpanderResolver>newArrayList(
710 new CollectionEntityExpanderResolver(),
711 new ListWrapperEntityExpanderResolver(),
712 new ExpandConstraintEntityExpanderResolver(),
713 new IdentityEntityExpanderResolver()
714 ));
715 }
716 }