/* * Copyright 2011 TaskDock, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.versly.rest.wsdoc.impl; import org.apache.commons.lang3.StringUtils; import org.versly.rest.wsdoc.DocumentationRestApi; import java.io.*; import java.util.*; import java.util.regex.Pattern; public class RestDocumentation implements Serializable { private static final long serialVersionUID = -3416760457544073264L; public static final String DEFAULT_API = "default"; private Map<String, RestApi> _apis = new LinkedHashMap(); public RestApi getRestApi(String apiBaseUrl) { if (!_apis.containsKey(apiBaseUrl)) _apis.put(apiBaseUrl, new RestApi(apiBaseUrl)); return _apis.get(apiBaseUrl); } public Collection<RestApi> getApis() { return _apis.values(); } /** * Read and return a serialized {@link RestDocumentation} instance from <code>in</code>, * as serialized by {@link #toStream}. */ public static RestDocumentation fromStream(InputStream in) throws IOException, ClassNotFoundException { ObjectInputStream ois = null; try { ois = new ObjectInputStream(in); return (RestDocumentation) ois.readObject(); } finally { if (ois != null) ois.close(); } } public void toStream(OutputStream out) throws IOException { ObjectOutputStream oos = new ObjectOutputStream(out); oos.writeObject(this); oos.flush(); } /** * This inspects the method paths and establishes parent/child relationships. This helps in particular * with generating RAML documentation, as RAML represents endpoints hierarchically. */ public void postProcess() { for (RestApi api : _apis.values()) { String mount = api.getMount(); if (null != mount && mount.length() > 0) { api.getResourceDocumentation(api.getMount()); } for (RestApi.Resource visitor : api.getResources()) { for (RestApi.Resource visitee : api.getResources()) { if (visitee != visitor && visitee.path.startsWith(visitor.path + "/") && (visitee._parent == null || visitee._parent.path.length() < visitor.path.length())) { if (visitee._parent != null) { visitee._parent._children.remove(visitee); } visitee._parent = visitor; visitor._children.add(visitee); } } } } } public class RestApi implements Serializable { private static final long serialVersionUID = 5665219205108618731L; public static final String DEFAULT_IDENTIFIER = "(default)"; private Map<String, Resource> _resources = new LinkedHashMap(); private String _identifier; private String _apiMount; private String _apiTitle; private String _apiVersion; private String _apiDocumentation; private HashSet<String> _traits = new HashSet<String>(); public RestApi(String identifier) { _identifier = identifier; } public Object readResolve() throws ObjectStreamException { _identifier = Utils.fillTemplate(_identifier); _apiMount = Utils.fillTemplate(_apiMount); _apiTitle = Utils.fillTemplate(_apiTitle); _apiVersion = Utils.fillTemplate(_apiVersion); return this; } public String getIdentifier() { return _identifier; } public String getMount() { return _apiMount; } public void setMount(String apiBaseUrl) { if (null != apiBaseUrl && !apiBaseUrl.trim().isEmpty()) { _apiMount = apiBaseUrl; } } public String getApiTitle() { return _apiTitle; } public void setApiTitle(String apiTitle) { if (null != apiTitle && !apiTitle.trim().isEmpty()) { _apiTitle = apiTitle; } } public String getApiVersion() { return _apiVersion; } public void setApiVersion(String apiVersion) { if (null != apiVersion && !apiVersion.trim().isEmpty()) { _apiVersion = apiVersion; } } public String getApiDocumentation() { return _apiDocumentation; } public void setApiDocumentation(String apiDocumentation) { if (null != apiDocumentation && !apiDocumentation.trim().isEmpty()) { _apiDocumentation = apiDocumentation; } } public String getIndentedApiDocumentationText(int indent) { if (_apiDocumentation != null) { String whitespace = StringUtils.leftPad("", indent); return whitespace + _apiDocumentation.replaceAll("\n", "\n" + whitespace); } return ""; } public HashSet<String> getTraits() { return _traits; } public void setTraits(HashSet<String> traits) { this._traits = traits; } public String getIndentedApiTraits(int indent) { StringBuilder retval = new StringBuilder(); for (String trait : _traits) { retval.append(StringUtils.leftPad("", indent)); retval.append("- "); retval.append(trait); retval.append(":\n"); retval.append(StringUtils.leftPad("", 2*indent)); retval.append("description: TBD\n"); } return retval.toString(); } public Collection<Resource> getResources() { return _resources.values(); } public void merge(RestApi api) { if (null == _apiTitle || _apiTitle.trim().isEmpty()) { _apiTitle = api.getApiTitle(); } if (null == _apiVersion || _apiVersion.trim().isEmpty()) { _apiVersion = api.getApiVersion(); } if (null == _apiMount || _apiMount.trim().isEmpty()) { _apiMount = api.getMount(); } if (null == _apiDocumentation || _apiDocumentation.trim().isEmpty()) { _apiDocumentation = api.getApiDocumentation(); } _resources.putAll(api._resources); _traits.addAll(api._traits); } public Resource getResourceDocumentation(String path) { if (!_resources.containsKey(path)) _resources.put(path, new Resource(path)); return _resources.get(path); } public RestApi filter(Iterable<Pattern> excludePatterns) { RestApi filtered = new RestApi(_identifier); filtered.setMount(_apiMount); filtered.setApiTitle(_apiTitle); filtered.setApiVersion(_apiVersion); filtered.setApiDocumentation(_apiDocumentation); filtered.setTraits(_traits); OUTER: for (Map.Entry<String, Resource> entry : _resources.entrySet()) { for (Pattern excludePattern : excludePatterns) if (excludePattern.matcher(entry.getKey()).matches()) continue OUTER; filtered._resources.put(entry.getKey(), entry.getValue()); } return filtered; } public class Resource implements Serializable { private static final long serialVersionUID = -3436348850301436626L; private String path; private Map<String, Method> _methods = new LinkedHashMap(); private Resource _parent; private Collection<Resource> _children = new LinkedList<Resource>(); public Resource(String path) { this.path = path; } public String getPath() { return path; } public Collection<Method> getRequestMethodDocs() { return _methods.values(); } public Resource getParent() { return _parent; } ; public String getPathLeaf() { return (_parent != null) ? path.substring(_parent.path.length()) : path; } public Collection<Resource> getChildren() { return _children; } /** * Creates and returns a new {@link Method} instance, and adds it to * the current resource location. If this is invoked multiple times * with the same argument, the same {@link Method} object * will be returned. */ public Method newMethodDocumentation(String meth) { if (_methods.containsKey(meth)) { return _methods.get(meth); } Method method = new Method(meth); _methods.put(meth, method); return method; } public UrlFields getResourceUrlSubstitutions() { UrlFields aggregateUrlFields = new UrlFields(); for (Method method : _methods.values()) { UrlFields fields = method.getMethodSpecificUrlSubstitutions(); aggregateUrlFields.getFields().putAll(fields.getFields()); } return aggregateUrlFields; } private Object readResolve() throws ObjectStreamException { path = Utils.fillTemplate(path); return this; } public class Method implements Serializable { private String _meth; private HashSet<String> _docScopes; private HashSet<String> _traits; private HashSet<String> _authScopes; private JsonType _requestBody; private UrlFields _urlSubstitutions = new UrlFields(); private UrlFields _urlParameters = new UrlFields(); private JsonType _responseBody; private String _commentText; private boolean _isMultipartRequest; private String _requestSchema; private String _responseSchema; private String _responseExample; private String _requestExample; public HashSet<String> getDocScopes() { return _docScopes; } public void setDocScopes(HashSet<String> scopes) { this._docScopes = scopes; } public HashSet<String> getTraits() { return _traits; } public String getTraitsAsString() { StringBuilder sb = new StringBuilder("[ "); for (String trait : _traits) { sb.append(trait).append(","); } sb.setCharAt(sb.length() - 1, ']'); return sb.toString(); } public void setTraits(HashSet<String> traits) { this._traits = traits; } public HashSet<String> getAuthScopes() { return _authScopes; } public String getAuthScopesAsString() { if (null != _authScopes && _authScopes.size() > 0) { StringBuilder sb = new StringBuilder(); sb.append("["); for (String authScope : _authScopes) { sb.append("\""); sb.append(authScope); sb.append("\","); } if (sb.length() > 1) { sb.setCharAt(sb.length() - 1, ']'); } else { sb.append("]"); } return sb.toString(); } return null; } public void setAuthScopes(HashSet<String> _authScopes) { this._authScopes = _authScopes; } public String getResponseSchema() { return _responseSchema; } public void setResponseSchema(String _responseSchema) { this._responseSchema = _responseSchema; } public String getRequestSchema() { return _requestSchema; } public void setRequestSchema(String _requestSchema) { this._requestSchema = _requestSchema; } public void setResponseExample(String wsDocResponseSchema) { this._responseExample = wsDocResponseSchema; } public String getResponseExample() { return _responseExample; } public void setRequestExample(String wsDocRequestSchema) { this._requestExample = wsDocRequestSchema; } public String getRequestExample() { return _requestExample; } public Method(String meth) { this._meth = meth; } public String getRequestMethod() { return _meth; } public JsonType getRequestBody() { return _requestBody; } public void setRequestBody(JsonType body) { _requestBody = body; } public UrlFields getUrlSubstitutions() { return _urlSubstitutions; } /** * Get the URI parameters specific to this method (useful in RAML where the parent hierarchy will already include it's own) * * @return */ public UrlFields getMethodSpecificUrlSubstitutions() { Resource parent = _parent; Map<String, UrlFields.UrlField> methodFields = new HashMap<String, UrlFields.UrlField>(_urlSubstitutions.getFields()); while (parent != null) { Iterator<Method> iter = parent.getRequestMethodDocs().iterator(); while (iter.hasNext()) { for (String key : iter.next()._urlSubstitutions.getFields().keySet()) { methodFields.remove(key); } } parent = parent._parent; } UrlFields urlFields = new UrlFields(); urlFields.getFields().putAll(methodFields); return urlFields; } public UrlFields getUrlParameters() { return _urlParameters; } public JsonType getResponseBody() { return _responseBody; } public void setResponseBody(JsonType body) { _responseBody = body; } public String getCommentText() { return _commentText; } public String getIndentedCommentText(int indent) { if (_commentText != null) { String whitespace = StringUtils.leftPad("", indent); return whitespace + _commentText.split("\n @")[0].replaceAll("\n", "\n" + whitespace); } return null; } public void setCommentText(String text) { _commentText = text; } public boolean isMultipartRequest() { return _isMultipartRequest; } public void setMultipartRequest(boolean multipart) { _isMultipartRequest = multipart; } /** * An HTML-safe, textual key that uniquely identifies this endpoint. */ public String getKey() { String key = path + "_" + _meth; for (String param : _urlParameters.getFields().keySet()) { key += "_" + param; } return key; } } public class UrlFields implements Serializable { private Map<String, UrlField> _jsonFields = new LinkedHashMap(); public class UrlField implements Serializable { private JsonType fieldType; private String fieldDescription; public UrlField(JsonType type, String desc) { fieldType = type; fieldDescription = desc; } public JsonType getFieldType() { return fieldType; } public void setFieldType(JsonType fieldType) { this.fieldType = fieldType; } public String getFieldDescription() { return fieldDescription; } public void setFieldDescription(String fieldDescription) { this.fieldDescription = fieldDescription; } } public Map<String, UrlField> getFields() { return _jsonFields; } public void addField(String name, JsonType jsonType, String description) { _jsonFields.put(name, new UrlField(jsonType, description)); } } } } }