All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sun.jersey.wadl.resourcedoc.ResourceDoclet Maven / Gradle / Ivy

Go to download

A doclet that generates a resourcedoc xml file: this file contains the javadoc documentation of resource classes, so that this can be used for extending generated wadl with useful documentation.

There is a newer version: 1.19.4
Show newest version
/*
 *
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 1997-2010 Sun Microsystems, Inc. All rights reserved.
 * 
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License. You can obtain
 * a copy of the License at https://jersey.dev.java.net/CDDL+GPL.html
 * or jersey/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at jersey/legal/LICENSE.txt.
 * Sun designates this particular file as subject to the "Classpath" exception
 * as provided by Sun in the GPL Version 2 section of the License file that
 * accompanied this code.  If applicable, add the following below the License
 * Header, with the fields enclosed by brackets [] replaced by your own
 * identifying information: "Portions Copyrighted [year]
 * [name of copyright owner]"
 * 
 * Contributor(s):
 * 
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */
package com.sun.jersey.wadl.resourcedoc;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.namespace.QName;

import org.apache.xml.serialize.OutputFormat;
import org.apache.xml.serialize.XMLSerializer;

import com.sun.javadoc.AnnotationDesc;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.DocErrorReporter;
import com.sun.javadoc.MemberDoc;
import com.sun.javadoc.MethodDoc;
import com.sun.javadoc.ParamTag;
import com.sun.javadoc.Parameter;
import com.sun.javadoc.RootDoc;
import com.sun.javadoc.SeeTag;
import com.sun.javadoc.Tag;
import com.sun.javadoc.AnnotationDesc.ElementValuePair;
import com.sun.jersey.server.wadl.generators.resourcedoc.model.AnnotationDocType;
import com.sun.jersey.server.wadl.generators.resourcedoc.model.ClassDocType;
import com.sun.jersey.server.wadl.generators.resourcedoc.model.MethodDocType;
import com.sun.jersey.server.wadl.generators.resourcedoc.model.NamedValueType;
import com.sun.jersey.server.wadl.generators.resourcedoc.model.ParamDocType;
import com.sun.jersey.server.wadl.generators.resourcedoc.model.RepresentationDocType;
import com.sun.jersey.server.wadl.generators.resourcedoc.model.RequestDocType;
import com.sun.jersey.server.wadl.generators.resourcedoc.model.ResourceDocType;
import com.sun.jersey.server.wadl.generators.resourcedoc.model.ResponseDocType;
import com.sun.jersey.server.wadl.generators.resourcedoc.model.WadlParamType;

/**
 * This doclet creates a resourcedoc xml file. The ResourceDoc file contains the javadoc documentation
 * of resource classes, so that this can be used for extending generated wadl with useful
 * documentation.
* Created on: Jun 7, 2008
* * @author Martin Grotzke * @version $Id: ResourceDoclet.java 3252 2010-01-26 23:13:50Z pavel_bucek $ */ public class ResourceDoclet { private static final Pattern PATTERN_RESPONSE_REPRESENATION = Pattern.compile( "@response\\.representation\\.([\\d]+)\\..*" ); private static final String OPTION_OUTPUT = "-output"; private static final String OPTION_CLASSPATH = "-classpath"; private static final String OPTION_DOC_PROCESSORS = "-processors"; private static final Logger LOG = Logger.getLogger( ResourceDoclet.class .getName() ); /** * Start the doclet. * * @param root * @return true if no exception is thrown */ public static boolean start( RootDoc root ) { final String output = getOptionArg( root.options(), OPTION_OUTPUT ); final String classpath = getOptionArg( root.options(), OPTION_CLASSPATH ); // LOG.info( "Have classpatch: " + classpath ); final String[] classpathElements = classpath.split( ":" ); final ClassLoader cl = Thread.currentThread().getContextClassLoader(); final ClassLoader ncl = new Loader( classpathElements, ResourceDoclet.class.getClassLoader() ); Thread.currentThread().setContextClassLoader( ncl ); final String docProcessorOption = getOptionArg( root.options(), OPTION_DOC_PROCESSORS ); final String[] docProcessors = docProcessorOption != null ? docProcessorOption.split( ":" ) : null; final DocProcessorWrapper docProcessor = new DocProcessorWrapper(); try { if ( docProcessors != null && docProcessors.length > 0 ) { final Class clazz = Class.forName( docProcessors[0], true, Thread.currentThread().getContextClassLoader() ); final Class dpClazz = clazz.asSubclass( DocProcessor.class ); docProcessor.add( dpClazz.newInstance() ); } } catch ( Exception e ) { LOG.log( Level.SEVERE, "Could not load docProcessors " + docProcessorOption, e ); } try { final ResourceDocType result = new ResourceDocType(); final ClassDoc[] classes = root.classes(); for ( ClassDoc classDoc : classes ) { LOG.fine( "Writing class " + classDoc.qualifiedTypeName() ); final ClassDocType classDocType = new ClassDocType(); classDocType.setClassName( classDoc.qualifiedTypeName() ); classDocType.setCommentText( classDoc.commentText() ); docProcessor.processClassDoc( classDoc, classDocType ); for ( MethodDoc methodDoc : classDoc.methods() ) { final MethodDocType methodDocType = new MethodDocType(); methodDocType.setMethodName( methodDoc.name() ); methodDocType.setCommentText( methodDoc.commentText() ); docProcessor.processMethodDoc( methodDoc, methodDocType ); addParamDocs( methodDoc, methodDocType, docProcessor ); addRequestRepresentationDoc( methodDoc, methodDocType ); addResponseDoc( methodDoc, methodDocType ); classDocType.getMethodDocs().add( methodDocType ); } result.getDocs().add( classDocType ); } try { final Class[] clazzes = getJAXBContextClasses( result, docProcessor ); final JAXBContext c = JAXBContext.newInstance( clazzes ); final Marshaller m = c.createMarshaller(); m.setProperty( Marshaller.JAXB_FORMATTED_OUTPUT, true ); final OutputStream out = new BufferedOutputStream( new FileOutputStream( output ) ); final String[] cdataElements = getCDataElements( docProcessor ); final XMLSerializer serializer = getXMLSerializer( out, cdataElements ); m.marshal( result, serializer ); out.close(); LOG.info( "Wrote " + output ); } catch (Exception e) { LOG.log( Level.SEVERE, "Could not serialize ResourceDoc.", e ); return false; } } finally { Thread.currentThread().setContextClassLoader( cl ); } return true; } private static String[] getCDataElements( DocProcessor docProcessor ) { final String[] original = new String[] { "ns1^commentText", "ns2^commentText", "^commentText" }; if ( docProcessor == null ) { return original; } else { final String[] cdataElements = docProcessor.getCDataElements(); if ( cdataElements == null || cdataElements.length == 0 ) { return original; } else { final String[] result = copyOf( original, original.length + cdataElements.length ); for ( int i = 0; i < cdataElements.length; i++ ) { result[ original.length + i ] = cdataElements[i]; } return result; } } } @SuppressWarnings("unchecked") private static T[] copyOf( U[] original, int newLength ) { final T[] copy = ((Object)original.getClass() == (Object)Object[].class) ? (T[]) new Object[newLength] : (T[]) Array.newInstance(original.getClass().getComponentType(), newLength); System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy; } private static Class[] getJAXBContextClasses( final ResourceDocType result, DocProcessor docProcessor ) { final Class[] clazzes; if ( docProcessor == null ) { clazzes = new Class[1]; } else { final Class[] requiredJaxbContextClasses = docProcessor.getRequiredJaxbContextClasses(); if ( requiredJaxbContextClasses != null ) { clazzes = new Class[1 + requiredJaxbContextClasses.length ]; for ( int i = 0; i < requiredJaxbContextClasses.length; i++ ) { clazzes[i + 1] = requiredJaxbContextClasses[i]; } } else { clazzes = new Class[1]; } } clazzes[0] = result.getClass(); return clazzes; } private static XMLSerializer getXMLSerializer( OutputStream os, String[] cdataElements ) throws InstantiationException, IllegalAccessException, ClassNotFoundException { // configure an OutputFormat to handle CDATA OutputFormat of = new OutputFormat(); // specify which of your elements you want to be handled as CDATA. // The use of the '^' between the namespaceURI and the localname // seems to be an implementation detail of the xerces code. // When processing xml that doesn't use namespaces, simply omit the // namespace prefix as shown in the third CDataElement below. of.setCDataElements( cdataElements ); // set any other options you'd like of.setPreserveSpace(true); of.setIndenting(true); // create the serializer XMLSerializer serializer = new XMLSerializer(of); serializer.setOutputByteStream( os ); return serializer; } private static void addResponseDoc( MethodDoc methodDoc, final MethodDocType methodDocType ) { final ResponseDocType responseDoc = new ResponseDocType(); final Tag returnTag = getSingleTagOrNull( methodDoc, "return" ); if ( returnTag != null ) { responseDoc.setReturnDoc( returnTag.text() ); } final Tag[] responseParamTags = methodDoc.tags( "response.param" ); for ( Tag responseParamTag : responseParamTags ) { // LOG.info( "Have responseparam tag: " + print( responseParamTag ) ); final WadlParamType wadlParam = new WadlParamType(); for ( Tag inlineTag : responseParamTag.inlineTags() ) { final String tagName = inlineTag.name(); final String tagText = inlineTag.text(); /* skip empty tags */ if ( isEmpty( tagText ) ) { if ( LOG.isLoggable( Level.FINE ) ) { LOG.fine( "Skipping empty inline tag of @response.param in method " + methodDoc.qualifiedName() + ": " + tagName ); } continue; } if ( "@name".equals( tagName ) ) { wadlParam.setName( tagText ); } else if ( "@style".equals( tagName ) ) { wadlParam.setStyle( tagText ); } else if ( "@type".equals( tagName ) ) { wadlParam.setType( QName.valueOf( tagText ) ); } else if ( "@doc".equals( tagName ) ) { wadlParam.setDoc( tagText ); } else { LOG.warning( "Unknown inline tag of @response.param in method " + methodDoc.qualifiedName() + ": " + tagName + " (value: " + tagText + ")" ); } } responseDoc.getWadlParams().add( wadlParam ); } final Map> tagsByStatus = getResponseRepresentationTags( methodDoc ); for ( Entry> entry : tagsByStatus.entrySet() ) { final RepresentationDocType representationDoc = new RepresentationDocType(); representationDoc.setStatus( Long.valueOf( entry.getKey() ) ); for ( Tag tag : entry.getValue() ) { if ( tag.name().endsWith( ".qname" ) ) { representationDoc.setElement( QName.valueOf( tag.text() ) ); } else if ( tag.name().endsWith( ".mediaType" ) ) { representationDoc.setMediaType( tag.text() ); } else if ( tag.name().endsWith( ".example" ) ) { representationDoc.setExample( getSerializedExample( tag ) ); } else if ( tag.name().endsWith( ".doc" ) ) { representationDoc.setDoc( tag.text() ); } else { LOG.warning( "Unknown response representation tag " + tag.name() ); } } responseDoc.getRepresentations().add( representationDoc ); } methodDocType.setResponseDoc( responseDoc ); } private static boolean isEmpty( String value ) { return value == null || value.length() == 0 || value.trim().length() == 0 ? true : false; } private static void addRequestRepresentationDoc( MethodDoc methodDoc, final MethodDocType methodDocType ) { final Tag requestElement = getSingleTagOrNull( methodDoc, "request.representation.qname" ); final Tag requestExample = getSingleTagOrNull( methodDoc, "request.representation.example" ); if ( requestElement != null || requestExample != null ) { final RequestDocType requestDoc = new RequestDocType(); final RepresentationDocType representationDoc = new RepresentationDocType(); /* requestElement exists */ if ( requestElement != null ) { representationDoc.setElement( QName.valueOf( requestElement.text() ) ); } /* requestExample exists */ if ( requestExample != null ) { final String example = getSerializedExample( requestExample ); if ( !isEmpty( example ) ) { representationDoc.setExample( example ); } else { LOG.warning( "Could not get serialized example for method " + methodDoc.qualifiedName() ); } } requestDoc.setRepresentationDoc( representationDoc ); methodDocType.setRequestDoc( requestDoc ); } } private static Map> getResponseRepresentationTags( MethodDoc methodDoc ) { final Map> tagsByStatus = new HashMap>(); for ( Tag tag : methodDoc.tags() ) { final Matcher matcher = PATTERN_RESPONSE_REPRESENATION.matcher( tag.name() ); if ( matcher.matches() ) { final String status = matcher.group( 1 ); List tags = tagsByStatus.get( status ); if ( tags == null ) { tags = new ArrayList(); tagsByStatus.put( status, tags ); } tags.add( tag ); } } return tagsByStatus; } /** * Searches an @link tag within the inline tags of the specified tags * and serializes the referenced instance. * @param tag * @return * @author Martin Grotzke */ private static String getSerializedExample( Tag tag ) { if ( tag != null ) { final Tag[] inlineTags = tag.inlineTags(); if ( inlineTags != null && inlineTags.length > 0 ) { for ( Tag inlineTag : inlineTags ) { if ( LOG.isLoggable( Level.FINE ) ) { LOG.fine( "Have inline tag: " + print( inlineTag ) ); } if ( "@link".equals( inlineTag.name() ) ) { if ( LOG.isLoggable( Level.FINE ) ) { LOG.fine( "Have link: " + print( inlineTag ) ); } final SeeTag linkTag = (SeeTag) inlineTag; return getSerializedLinkFromTag( linkTag ); } else if ( !isEmpty( inlineTag.text() ) ) { return inlineTag.text(); } } } else { LOG.fine( "Have example: " + print( tag ) ); return tag.text(); } } return null; } private static Tag getSingleTagOrNull( MethodDoc methodDoc, String tagName ) { final Tag[] tags = methodDoc.tags( tagName ); if ( tags != null && tags.length == 1 ) { return tags[0]; } return null; } private static void addParamDocs( MethodDoc methodDoc, final MethodDocType methodDocType, final DocProcessor docProcessor ) { final Parameter[] parameters = methodDoc.parameters(); final ParamTag[] paramTags = methodDoc.paramTags(); /* only use both javadoc and reflection information when the number * of params are the same */ if ( parameters != null && paramTags != null && parameters.length == paramTags.length ) { for ( int i = 0; i < parameters.length; i++ ) { final Parameter parameter = parameters[i]; /* TODO: this only works if the params and tags are in the same * order. If the param tags are mixed up, the comments for parameters * will be wrong. */ final ParamTag paramTag = paramTags[i]; final ParamDocType paramDocType = new ParamDocType(); paramDocType.setParamName( paramTag.parameterName() ); paramDocType.setCommentText( paramTag.parameterComment() ); docProcessor.processParamTag( paramTag, parameter, paramDocType ); AnnotationDesc[] annotations = parameter.annotations(); if ( annotations != null ) { for ( AnnotationDesc annotationDesc : annotations ) { final AnnotationDocType annotationDocType = new AnnotationDocType(); final String typeName = annotationDesc.annotationType().qualifiedName(); annotationDocType.setAnnotationTypeName( typeName ); for ( ElementValuePair elementValuePair : annotationDesc.elementValues() ) { final NamedValueType namedValueType = new NamedValueType(); namedValueType.setName( elementValuePair.element().name() ); namedValueType.setValue( elementValuePair.value().value().toString() ); annotationDocType.getAttributeDocs().add( namedValueType ); } paramDocType.getAnnotationDocs().add( annotationDocType ); } } methodDocType.getParamDocs().add( paramDocType ); } } } private static String getSerializedLinkFromTag( final SeeTag linkTag ) { final MemberDoc referencedMember = linkTag.referencedMember(); if ( referencedMember == null ) { throw new NullPointerException( "Referenced member of @link "+ print( linkTag ) +" cannot be resolved." ); } if ( !referencedMember.isStatic() ) { LOG.warning( "Referenced member of @link "+ print( linkTag ) +" is not static." + " Right now only references to static members are supported." ); return null; } /* Get referenced example bean */ final ClassDoc containingClass = referencedMember.containingClass(); final Object object; try { Field declaredField = Class.forName( containingClass.qualifiedName(), false, Thread.currentThread().getContextClassLoader() ).getDeclaredField( referencedMember.name() ); if ( referencedMember.isFinal() ) { declaredField.setAccessible( true ); } object = declaredField.get( null ); LOG.log( Level.FINE, "Got object " + object ); } catch ( Exception e ) { LOG.info( "Have classloader: " + ResourceDoclet.class.getClassLoader().getClass() ); LOG.info( "Have thread classloader " + Thread.currentThread().getContextClassLoader().getClass() ); LOG.info( "Have system classloader " + ClassLoader.getSystemClassLoader().getClass() ); LOG.log( Level.SEVERE, "Could not get field " + referencedMember.qualifiedName(), e ); return null; } /* marshal the bean to xml */ try { final JAXBContext jaxbContext = JAXBContext.newInstance( object.getClass() ); final StringWriter stringWriter = new StringWriter(); final Marshaller marshaller = jaxbContext.createMarshaller(); marshaller.setProperty( Marshaller.JAXB_FORMATTED_OUTPUT, true ); marshaller.marshal( object, stringWriter ); final String result = stringWriter.getBuffer().toString(); LOG.log( Level.FINE, "Got marshalled output:\n" + result ); return result; } catch ( Exception e ) { LOG.log( Level.SEVERE, "Could serialize bean to xml: " + object, e ); return null; } } private static String print( Tag tag ) { final StringBuilder sb = new StringBuilder(); sb.append( tag.getClass() ).append( "[" ); sb.append( "firstSentenceTags=" ).append( toCSV( tag.firstSentenceTags() ) ); sb.append( ", inlineTags=" ).append( toCSV( tag.inlineTags() ) ); sb.append( ", kind=" ).append( tag.kind() ); sb.append( ", name=" ).append( tag.name() ); sb.append( ", text=" ).append( tag.text() ); sb.append( "]" ); return sb.toString(); } static String toCSV( Tag[] items ) { if ( items == null ) { return null; } return toCSV( Arrays.asList( items ) ); } static String toCSV( Collection items ) { return toCSV( items, ", ", null ); } static String toCSV( Collection items, String separator, String delimiter ) { if ( items == null ) { return null; } if ( items.isEmpty() ) { return ""; } final StringBuilder sb = new StringBuilder(); for ( final Iterator iter = items.iterator(); iter.hasNext(); ) { if ( delimiter != null ) { sb.append( delimiter ); } final Tag item = iter.next(); sb.append( item.name() ); if ( delimiter != null ) { sb.append( delimiter ); } if ( iter.hasNext() ) { sb.append( separator ); } } return sb.toString(); } /** * Return array length for given option: 1 + the number of arguments that * the option takes. * * @param option * @return the number of args for the specified option */ public static int optionLength( String option ) { LOG.fine( "Invoked with option " + option ); if ( OPTION_OUTPUT.equals( option ) || OPTION_CLASSPATH.equals( option ) || OPTION_DOC_PROCESSORS.equals( option ) ) { return 2; } return 0; } /** * Validate options. * * @param options * @param reporter * @return if the specified options are valid */ public static boolean validOptions( String[][] options, DocErrorReporter reporter ) { return validOption( OPTION_OUTPUT, "", options, reporter ) && validOption( OPTION_CLASSPATH, "", options, reporter ); } private static boolean validOption( String optionName, String reportOptionName, String[][] options, DocErrorReporter reporter ) { final String option = getOptionArg( options, optionName ); final boolean foundOption = option != null && option.trim().length() > 0; if ( !foundOption ) { reporter.printError( optionName + " "+ reportOptionName +" must be specified." ); } return foundOption; } private static String getOptionArg( String[][] options, String option ) { for ( int i = 0; i < options.length; i++ ) { String[] opt = options[i]; if ( opt[0].equals( option ) ) { return opt[1]; } } return null; } static class Loader extends URLClassLoader { public Loader(String[] paths, ClassLoader parent) { super(getURLs(paths), parent); } Loader(String[] paths) { super(getURLs(paths)); } private static URL[] getURLs(String[] paths) { final List urls = new ArrayList(); for (String path: paths) { try { urls.add(new File(path).toURI().toURL()); } catch (MalformedURLException e) { throw new RuntimeException(e); } } final URL[] us = urls.toArray(new URL[0]); return us; } } }