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
45
46
47
48
49
50
51
52
53
54
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
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
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 }