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.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  * This doclet creates a resourcedoc xml file. The ResourceDoc file contains the javadoc documentation
105  * of resource classes, so that this can be used for extending generated wadl with useful
106  * documentation.<br>
107  * Created on: Jun 7, 2008<br>
108  *
109  * @author <a href="mailto:martin.grotzke@freiheit.com">Martin Grotzke</a>
110  * @version $Id: ResourceDoclet.java 1908 2009-02-03 00:20:29Z magrokosmos $
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      * Start the doclet.
127      *
128      * @param root
129      * @return true if no exception is thrown
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         // LOG.info( "Have classpatch: " + classpath );
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         // configure an OutputFormat to handle CDATA
275         OutputFormat of = new OutputFormat();
276 
277         // specify which of your elements you want to be handled as CDATA.
278         // The use of the '^' between the namespaceURI and the localname
279         // seems to be an implementation detail of the xerces code.
280         // When processing xml that doesn't use namespaces, simply omit the
281         // namespace prefix as shown in the third CDataElement below.
282         of.setCDataElements( cdataElements );
283 
284         // set any other options you'd like
285         of.setPreserveSpace(true);
286         of.setIndenting(true);
287 
288         // create the serializer
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             // LOG.info( "Have responseparam tag: " + print( responseParamTag ) );
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                 /* skip empty tags
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             /* requestElement exists
383              */
384             if ( requestElement != null ) {
385                 representationDoc.setElement( QName.valueOf( requestElement.text() ) );
386             }
387 
388             /* requestExample exists
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      * Searches an <code>@link</code> tag within the inline tags of the specified tags
425      * and serializes the referenced instance.
426      * @param tag
427      * @param expand
428      * @return
429      * @author Martin Grotzke
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         /* Get referenced example bean
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         /* marshal the bean to xml
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         // expand stuff...
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      * Return array length for given option: 1 + the number of arguments that
626      * the option takes.
627      *
628      * @param option
629      * @return the number of args for the specified option
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      * Validate options.
646      *
647      * @param options
648      * @param reporter
649      * @return if the specified options are valid
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 }