View Javadoc

1   package com.atlassian.plugins.rest.doclet.generators.resourcedoc;
2   
3   import com.atlassian.annotations.tenancy.TenantAware;
4   import com.atlassian.plugins.rest.common.version.ApiVersion;
5   import com.atlassian.plugins.rest.doclet.generators.schema.RichClass;
6   import com.atlassian.plugins.rest.doclet.generators.schema.SchemaGenerator;
7   import com.atlassian.rest.annotation.ExcludeFromDoc;
8   import com.atlassian.rest.annotation.RestProperty;
9   import com.google.common.collect.Lists;
10  import com.sun.jersey.api.model.AbstractResource;
11  import com.sun.jersey.api.model.AbstractResourceMethod;
12  import com.sun.jersey.server.wadl.WadlGenerator;
13  import com.sun.jersey.server.wadl.generators.resourcedoc.WadlGeneratorResourceDocSupport;
14  import com.sun.jersey.server.wadl.generators.resourcedoc.model.ResourceDocType;
15  import com.sun.jersey.server.wadl.generators.resourcedoc.xhtml.Elements;
16  import com.sun.research.ws.wadl.Doc;
17  import com.sun.research.ws.wadl.Method;
18  import com.sun.research.ws.wadl.Representation;
19  import com.sun.research.ws.wadl.Resource;
20  import com.sun.research.ws.wadl.Response;
21  import org.slf4j.Logger;
22  import org.slf4j.LoggerFactory;
23  import org.w3c.dom.Document;
24  import org.w3c.dom.NamedNodeMap;
25  import org.w3c.dom.Node;
26  import org.w3c.dom.NodeList;
27  
28  import javax.ws.rs.core.MediaType;
29  import javax.xml.namespace.QName;
30  import javax.xml.parsers.DocumentBuilder;
31  import javax.xml.parsers.DocumentBuilderFactory;
32  import java.net.URL;
33  import java.util.HashMap;
34  import java.util.List;
35  import java.util.Set;
36  import java.util.stream.Collectors;
37  import java.util.stream.Stream;
38  
39  import static com.atlassian.annotations.tenancy.TenancyScope.TENANTLESS;
40  import static com.atlassian.plugins.rest.doclet.generators.resourcedoc.JsonOperations.toJson;
41  import static com.atlassian.plugins.rest.doclet.generators.resourcedoc.RestMethod.restMethod;
42  
43  /**
44   * This class generates the WADL description of rest resources and considers the rest plugin module descriptors
45   * configured inside the atlassian-plugin.xml file when generating the resource path.
46   * <p/>
47   * It builds up a map that contains a mapping of a package name to a resource path.
48   * The full resource path is concatenated of the following strings:
49   * 1) path as configured for rest plugin module descriptor: e.g. api
50   * 2) version as configured for rest plugin module descriptor e.g. 2.0.alpha1
51   * 3) path of the rest end point e.g. worklog
52   *
53   * <p/>
54   * e.g. /api/2.0.alpha1/worklog/
55   */
56  public class AtlassianWadlGeneratorResourceDocSupport extends WadlGeneratorResourceDocSupport {
57      @TenantAware(TENANTLESS)
58      private HashMap<String, ResourcePathInformation> resourcePathInformation;
59  
60      private static final Logger LOG = LoggerFactory.getLogger(AtlassianWadlGeneratorResourceDocSupport.class);
61      private static final String ATLASSIAN_PLUGIN_XML = "atlassian-plugin.xml";
62      private boolean generateSchemas = true;
63  
64      public AtlassianWadlGeneratorResourceDocSupport() {
65          super();
66      }
67  
68      public AtlassianWadlGeneratorResourceDocSupport(WadlGenerator wadlGenerator, ResourceDocType resourceDoc) {
69          super(wadlGenerator, resourceDoc);
70      }
71  
72      @Override
73      public void init() throws Exception {
74          super.init();
75          parseAtlassianPluginXML();
76      }
77  
78      public void setGenerateSchemas(Boolean generateSchemas) {
79          this.generateSchemas = generateSchemas;
80      }
81  
82      private void parseAtlassianPluginXML() {
83          //get the factory
84          DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
85  
86          try {
87              final URL resource = getClass().getClassLoader().getResource(ATLASSIAN_PLUGIN_XML);
88              if (resource == null) {
89                  return;
90              }
91              LOG.info("Found " + ATLASSIAN_PLUGIN_XML + " file! Looking for rest plugin module descriptors...");
92  
93              DocumentBuilder db = dbf.newDocumentBuilder();
94  
95              resourcePathInformation = new HashMap<String, ResourcePathInformation>();
96  
97              final Document document = db.parse(resource.toExternalForm());
98              final NodeList restPluginModuleDescriptors = document.getElementsByTagName("rest");
99              final int numPluginModuleDescriptors = restPluginModuleDescriptors.getLength();
100             LOG.info("Found " + numPluginModuleDescriptors + " rest plugin module descriptors.");
101 
102             for (int i = 0; i < numPluginModuleDescriptors; i++) {
103                 final Node node = restPluginModuleDescriptors.item(i);
104 
105                 final NamedNodeMap attributes = node.getAttributes();
106                 final Node pathItem = attributes.getNamedItem("path");
107                 final Node versionItem = attributes.getNamedItem("version");
108                 if (pathItem == null || versionItem == null) {
109                     continue;
110                 }
111 
112                 String resourcePath = pathItem.getNodeValue();
113                 String version = versionItem.getNodeValue();
114 
115                 LOG.info("Found rest end point with path '" + resourcePath + "' and version '" + version + "'");
116 
117                 //Remove leading slash
118                 if (resourcePath.indexOf("/") != -1) {
119                     resourcePath = resourcePath.substring(resourcePath.indexOf("/") + 1);
120                 }
121 
122                 final NodeList list = node.getChildNodes();
123                 for (int j = 0; j < list.getLength(); j++) {
124                     final Node child = list.item(j);
125                     if (child.getNodeName().equals("package")) {
126                         final String packageName = child.getFirstChild().getNodeValue();
127                         LOG.info("Map package '" + packageName + "' to resource path '" + resourcePath + "' and version '" + version + "'");
128                         resourcePathInformation.put(packageName, new ResourcePathInformation(resourcePath, version));
129                     }
130                 }
131             }
132         } catch (Exception ex) {
133             LOG.error("Failed to read " + ATLASSIAN_PLUGIN_XML + " and parse rest plugin module descriptor information. Reason", ex);
134         }
135     }
136 
137     @Override
138     public Resource createResource(AbstractResource r, String path) {
139 
140         removeMethodsExcludedFromDocs(r);
141         if (allMethodsExcluded(r)) {
142             return new Resource();
143         }
144 
145         final Resource result = super.createResource(r, path);
146         boolean resourcePathChanged = false;
147         for (String packageName : resourcePathInformation.keySet()) {
148             if (r.getResourceClass().getPackage().getName().startsWith(packageName)) {
149                 final ResourcePathInformation pathInformation = resourcePathInformation.get(packageName);
150                 final String newPath = buildResourcePath(result, pathInformation);
151                 result.setPath(newPath);
152                 resourcePathChanged = true;
153                 LOG.info("Setting resource path of rest end point '" + r.getResourceClass().getCanonicalName() + "' to '" + newPath + "'");
154                 break;
155             }
156         }
157         if (!resourcePathChanged) {
158             LOG.info("Resource path of rest end point '" + r.getResourceClass().getCanonicalName() + "' unchanged no mapping to rest plugin module descriptor found.");
159         }
160         return result;
161     }
162 
163     private boolean allMethodsExcluded(AbstractResource r) {
164         return r.getResourceMethods().isEmpty() && r.getSubResourceMethods().isEmpty();
165     }
166 
167     private void removeMethodsExcludedFromDocs(AbstractResource r) {
168         Set<AbstractResourceMethod> excludedMethods = Stream.concat(r.getResourceMethods().stream().filter(method -> isMethodExcluded(r, method)),
169                 r.getSubResourceMethods().stream().filter(method -> isMethodExcluded(r, method))).collect(Collectors.toSet());
170 
171         excludedMethods.forEach(method -> {
172             r.getResourceMethods().remove(method);
173             r.getSubResourceMethods().remove(method);
174         });
175     }
176 
177     private boolean isMethodExcluded(AbstractResource r, AbstractResourceMethod method) {
178         return method.isAnnotationPresent(ExcludeFromDoc.class) || r.getResourceClass().isAnnotationPresent(ExcludeFromDoc.class);
179     }
180 
181     private String buildResourcePath(Resource result, ResourcePathInformation pathInformation) {
182         if (ApiVersion.isNone(pathInformation.getVersion())) {
183             return pathInformation.getPath() + "/" + result.getPath();
184         } else {
185             return pathInformation.getPath() + "/" + pathInformation.getVersion() + "/" + result.getPath();
186         }
187     }
188 
189     @Override
190     public Method createMethod(final AbstractResource r, final AbstractResourceMethod m) {
191         final Method method = super.createMethod(r, m);
192         RestMethod restMethod = restMethod(r.getResourceClass(), m.getMethod());
193         if (restMethod.isExperimental()) {
194             method.getOtherAttributes().put(new QName("experimental"), Boolean.TRUE.toString());
195         }
196         if (restMethod.isDeprecated()) {
197             method.getOtherAttributes().put(new QName("deprecated"), Boolean.TRUE.toString());
198         }
199         return method;
200     }
201 
202     @Override
203     public Representation createRequestRepresentation(final AbstractResource r, final AbstractResourceMethod m, final MediaType mediaType) {
204         final Representation representation = super.createRequestRepresentation(r, m, mediaType);
205         if (generateSchemas) {
206             restMethod(r.getResourceClass(), m.getMethod()).getRequestType().ifPresent(richClass ->
207                     representation.getDoc().add(schemaDoc(richClass, RestProperty.Scope.REQUEST)));
208         }
209 
210         return representation;
211     }
212 
213     @Override
214     public List<Response> createResponses(final AbstractResource r, final AbstractResourceMethod m) {
215         List<Response> result = Lists.newArrayList();
216         for (Response response : super.createResponses(r, m)) {
217             if (generateSchemas) {
218                 addSchemaIfDefinedForStatus(r, m, response);
219             }
220             result.add(response);
221         }
222 
223         return result;
224     }
225 
226     private void addSchemaIfDefinedForStatus(AbstractResource resource, final AbstractResourceMethod method, final Response response) {
227         for (Long status : response.getStatus()) {
228             for (RichClass responseType : restMethod(resource.getResourceClass(), method.getMethod()).responseTypesFor(status.intValue())) {
229                 for (Representation representation : response.getRepresentation()) {
230                     representation.getDoc().add(schemaDoc(responseType, RestProperty.Scope.RESPONSE));
231                 }
232             }
233         }
234     }
235 
236     private Doc schemaDoc(RichClass model, final RestProperty.Scope scope) {
237         String schema = toJson(SchemaGenerator.generateSchema(model, scope));
238         final Doc doc = new Doc();
239 
240         final Elements element = Elements.el("p")
241                 .add(Elements.val("h6", "Schema"))
242                 .add(Elements.el("pre").add(Elements.val("code", schema)));
243 
244         doc.getContent().add(element);
245 
246         return doc;
247     }
248 
249     public class ResourcePathInformation {
250         private final String path;
251         private final String version;
252 
253         public ResourcePathInformation(String path, String version) {
254             this.path = path;
255             this.version = version;
256         }
257 
258         public String getVersion() {
259             return version;
260         }
261 
262         public String getPath() {
263             return path;
264         }
265     }
266 }