View Javadoc

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