/*******************************************************************************
* Copyright 2012 Pearson Education
*
* 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.semantictools.frame.api;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import org.semantictools.context.renderer.model.ContextProperties;
import org.semantictools.context.renderer.model.DocumentMetadata;
import org.semantictools.context.renderer.model.HttpMethod;
import org.semantictools.context.renderer.model.MethodDocumentation;
import org.semantictools.context.renderer.model.Person;
import org.semantictools.context.renderer.model.QueryParam;
import org.semantictools.context.renderer.model.ResponseInfo;
import org.semantictools.context.renderer.model.ServiceDocumentation;
import org.semantictools.context.renderer.model.ServiceFileManager;
import org.semantictools.context.view.ServiceDocumentationPrinter;
import org.semantictools.context.view.StringUtil;
import org.semantictools.frame.model.Frame;
import org.semantictools.frame.model.Uri;
import org.semantictools.index.model.ServiceDocumentationList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ServiceDocumentationManager {
private static final Logger logger = LoggerFactory.getLogger(ServiceDocumentationManager.class);
private static final String ABSTRACT = "abstract";
private static final String AUTHORS = "authors";
private static final String CONTAINER_RDFTYPE = "container.rdfType";
private static final String CONTAINER_GET_MEDIATYPE = "container.GET.mediaType";
private static final String CONTAINER_GET_PARAM = "container.GET?";
private static final String CONTENT_NEGOTIATION = "contentNegotiation";
private static final String DATE = "date";
private static final String DEFAULT_MEDIA_TYPE = "GET.default.mediaType";
private static final String EDITORS = "editors";
private static final String ENABLE_VERSION_HISTORY = "enableVersionHistory";
private static final String GET_INSTRUCTIONS = "GET.instructions";
private static final String GET_REQUEST_BODY = "GET.requestBody";
private static final String GET_REQUEST_BODY_DEFAULT = "The request body must be empty.";
private static final String GET_REQUEST_HEADERS = "GET.requestHeaders";
private static final String GET_STATUS = "GET.status.";
private static final String GET_SUMMARY = "GET.summary";
private static final String HTML_FORMAT_DOCUMENTATION = "htmlFormatDocumentation";
private static final String INTRODUCTION = "introduction";
private static final String MEDIATYPE = "mediaType";
private static final String MEDIA_TYPE_URI_PREFIX = "mediaType.uri.";
private static final String METHODS = "methods";
private static final String POST_CREATED_DESCRIPTION = "POST.created.description";
private static final String POST_PROCESSING_RULES = "POST.processing.rules";
private static final String POST_REQUEST_MEDIATYPE = "POST.request.mediaType";
private static final String POST_RESPONSE_MEDIATYPE = "POST.response.mediaType";
private static final String PUT_INSTRUCTIONS = "PUT.instructions";
private static final String PUT_RULES = "PUT.rules";
private static final String QUERY_PARAM = "GET?";
private static final String RDFTYPE = "rdfType";
private static final String REPRESENTATIONS_HEADING = "representations.heading";
private static final String REPRESENTATIONS_TEXT = "representations.text";
private static final String STATUS = "status";
private static final String SUBTITLE = "subtitle";
private static final String TITLE = "title";
private static final String URL_TEMPLATES = "urlTemplates";
private Map<String, ServiceDocumentationList> map = new HashMap<String, ServiceDocumentationList>();
private ContextManager contextManager;
private TypeManager typeManager;
private ServiceFileManager serviceFileManager;
private ServiceDocumentationPrinter printer;
private DocumentMetadata global;
public ServiceDocumentationManager(
TypeManager typeManager,
DocumentMetadata global,
ContextManager contextManager,
ServiceFileManager fileManager,
ServiceDocumentationPrinter printer
) {
this.typeManager = typeManager;
this.global = global;
this.contextManager = contextManager;
serviceFileManager = fileManager;
this.printer = printer;
}
public List<ServiceDocumentationList> getServiceDocumentationLists() {
return new ArrayList<ServiceDocumentationList>(map.values());
}
/**
* Scans the given directory (and child directories recursively) for
* files named "service.properties". The files that are found are then loaded.
*/
public void scan(File dir) throws ServiceDocumentationSyntaxError, IOException {
File[] fileList = dir.listFiles();
for (int i=0; i<fileList.length; i++) {
File file = fileList[i];
if (file.getName().equals("service.properties")) {
load(file);
}
if (file.isDirectory()) {
scan(file);
}
}
}
public void load(File propertiesFile)
throws IOException, ServiceDocumentationSyntaxError {
Properties properties = new Properties();
FileInputStream input = new FileInputStream(propertiesFile);
try {
properties.load(input);
parseProperties(properties);
} finally {
input.close();
}
}
private void parseProperties(Properties properties) {
ServiceDocumentation sink = new ServiceDocumentation(global);
ServiceMethodInfo methodInfo = new ServiceMethodInfo();
sink.setDefaultMediaType(properties.getProperty(DEFAULT_MEDIA_TYPE));
for (Map.Entry<Object, Object> e : properties.entrySet()) {
String key = e.getKey().toString();
String value = e.getValue().toString();
if (key.startsWith("[")) {
sink.putReference(key, value);
continue;
}
if (MEDIATYPE.equals(key)) {
setMediaType(sink, value, properties);
} else if (TITLE.equals(key)) {
sink.setTitle(value);
} else if (SUBTITLE.equals(key)) {
sink.setSubtitle(value);
} else if (RDFTYPE.equals(key)) {
setRdfType(sink, value);
} else if (CONTAINER_RDFTYPE.equals(key)) {
sink.setContainerType(new Uri(value));
} else if (CONTAINER_GET_MEDIATYPE.equals(key)) {
setContainerGetMediaType(sink, value);
} else if (key.startsWith(CONTAINER_GET_PARAM)) {
addContainerGetParam(sink, key, value);
} else if (CONTENT_NEGOTIATION.equals(key)) {
sink.setContentNegotiation("true".equals(value));
} else if (STATUS.equals(key)) {
sink.setStatus(value);
} else if (DATE.equals(key)) {
sink.setDate(value);
} else if (ABSTRACT.equals(key)) {
sink.setAbstactText(value);
} else if (EDITORS.equals(key)) {
setEditors(sink, value);
} else if (ENABLE_VERSION_HISTORY.equals(key)) {
sink.setHistoryLink("true".equalsIgnoreCase(value));
} else if (AUTHORS.equals(key)) {
setAuthors(sink, value);
} else if (INTRODUCTION.equals(key)) {
sink.setIntroduction(value);
} else if (METHODS.equals(key)) {
setMethods(sink, value);
} else if (GET_SUMMARY.equals(key)) {
setGetSummary(sink, value);
} else if (GET_INSTRUCTIONS.equals(key)) {
sink.setGetInstructions(value);
} else if (GET_REQUEST_BODY.equals(key)) {
setGetRequestBody(sink, value);
} else if (GET_REQUEST_HEADERS.equals(key)) {
setGetRequestHeaders(sink, value);
} else if (key.startsWith(QUERY_PARAM)) {
addQueryParam(sink, key, value);
} else if (POST_REQUEST_MEDIATYPE.equals(key)) {
setPostRequestMediaType(sink, value);
} else if (POST_RESPONSE_MEDIATYPE.equals(key)) {
setPostResponseMediaType(sink, value);
} else if (URL_TEMPLATES.equals(key)) {
sink.setUrlTemplateText(value);
} else if (HTML_FORMAT_DOCUMENTATION.equals(key)) {
sink.setHtmlFormatDocumentation(value);
} else if (POST_PROCESSING_RULES.equals(key)) {
sink.setPostProcessingRules(value);
} else if (REPRESENTATIONS_HEADING.equals(key)) {
sink.setRepresentationHeading(value);
} else if (REPRESENTATIONS_TEXT.equals(key)) {
sink.setRepresentationText(value);
} else if (PUT_INSTRUCTIONS.equals(key)) {
sink.setPutInstructions(value);
} else if (PUT_RULES.equals(key)) {
setPutRules(sink, value);
} else if (POST_CREATED_DESCRIPTION.equals(key)) {
methodInfo.setPostCreatedDescription(value);
}
}
validate(sink);
addDefaults(properties, sink, methodInfo);
put(sink);
}
private void setContainerGetMediaType(ServiceDocumentation sink, String value) {
MethodDocumentation method = sink.getContainerGetDocumentation();
if (method == null) {
sink.setContainerGetDocumentation(method = new MethodDocumentation());
}
List<String> list = method.getResponseMediaTypes();
StringTokenizer tokens = new StringTokenizer(value);
while (tokens.hasMoreTokens()) {
list.add(tokens.nextToken());
}
}
private void setPostRequestMediaType(ServiceDocumentation sink, String value) {
MethodDocumentation method = sink.getPostDocumentation();
if (method == null) {
method = new MethodDocumentation();
sink.setPostDocumentation(method);
}
List<String> list = method.getRequestContentTypes();
StringTokenizer tokens = new StringTokenizer(value);
while (tokens.hasMoreTokens()) {
list.add(tokens.nextToken());
}
}
private void setGetStatus(Properties properties, ServiceDocumentation sink) {
MethodDocumentation method = sink.getGetDocumentation();
for (ResponseInfo info : ResponseInfo.all) {
StringBuilder builder = new StringBuilder();
builder.append(GET_STATUS);
builder.append(info.getStatusCode());
String key = builder.toString();
String description = properties.getProperty(key);
if (description != null) {
info = info.copy(description);
method.add(info);
}
}
}
private void setPutRules(ServiceDocumentation sink, String value) {
StringTokenizer tokens = new StringTokenizer(value, "\r\n");
while (tokens.hasMoreTokens()) {
String rule = tokens.nextToken().trim();
sink.getPutRules().add(rule);
}
}
private void addContainerGetParam(ServiceDocumentation sink, String key,
String value) {
MethodDocumentation method = sink.getContainerGetDocumentation();
if (method == null) {
method = new MethodDocumentation();
sink.setContainerGetDocumentation(method);
}
int qmark = key.indexOf('?');
String paramName = key.substring(qmark+1);
QueryParam param = new QueryParam();
param.setName(paramName);
param.setDescription(value);
method.add(param);
}
private void addQueryParam(ServiceDocumentation doc, String key, String value) {
MethodDocumentation method = doc.getGetDocumentation();
if (method == null) {
method = new MethodDocumentation();
doc.setGetDocumentation(method);
}
int qmark = key.indexOf('?');
String paramName = key.substring(qmark+1);
QueryParam param = new QueryParam();
param.setName(paramName);
param.setDescription(value);
method.add(param);
}
private void setPostResponseMediaType(ServiceDocumentation sink, String value) {
sink.setPostResponseMediaType(value);
}
private void setRdfType(ServiceDocumentation sink, String value) {
sink.setRdfType(new Uri(value));
}
private void put(ServiceDocumentation sink) {
ServiceDocumentationList list = map.get(sink.getRdfType().stringValue());
if (list == null) {
list = new ServiceDocumentationList(sink.getRdfType().stringValue());
map.put(sink.getRdfType().stringValue(), list);
}
list.add(sink);
}
private void setMediaType(ServiceDocumentation sink, String value, Properties properties) {
StringTokenizer tokenizer = new StringTokenizer(value, ",");
while (tokenizer.hasMoreTokens()) {
String mediaType = tokenizer.nextToken().trim();
if (mediaType.length()==0) continue;
if ("*/*".equals(mediaType)) {
sink.setAllowArbitraryFormat(true);
} else if ("text/html".equals(mediaType)) {
sink.setAllowHtmlFormat(true);
} else {
ContextProperties context = contextManager.getContextPropertiesByMediaType(mediaType);
if (context == null) {
String key = MEDIA_TYPE_URI_PREFIX + mediaType;
String uri = properties.getProperty(key);
if (uri != null) {
sink.getMediaTypeUriMap().put(mediaType, uri);
continue;
}
throw new ServiceDocumentationSyntaxError("MediaType not found: " + mediaType);
}
sink.add(context);
}
}
}
private void setGetRequestHeaders(ServiceDocumentation sink, String value) {
MethodDocumentation method = sink.getGetDocumentation();
if (method == null) {
method = new MethodDocumentation();
sink.setGetDocumentation(method);
}
if ("default".equals(value)) {
setGetRequestHeadersDefault(sink);
}
}
private void setGetRequestBody(ServiceDocumentation sink, String value) {
MethodDocumentation method = sink.getGetDocumentation();
if (method == null) {
method = new MethodDocumentation();
sink.setGetDocumentation(method);
}
if ("default".equals(value)) {
method.setRequestBodyRequirement(GET_REQUEST_BODY_DEFAULT);
} else {
method.setRequestBodyRequirement(value);
}
}
private void setGetSummary(ServiceDocumentation sink, String value) {
MethodDocumentation method = sink.getGetDocumentation();
if (method == null) {
method = new MethodDocumentation();
sink.setGetDocumentation(method);
}
method.setSummary(value);
}
private void setMethods(ServiceDocumentation sink, String value) {
String[] array = value.split("\\s+");
for (int i=0; i<array.length; i++) {
String term = array[i];
HttpMethod method = HttpMethod.getByName(term);
if (method != null) {
sink.add(method);
}
}
}
private void validate(ServiceDocumentation sink) {
if (sink.getRdfType() == null) {
throw new ServiceDocumentationSyntaxError("rdfType is not not defined");
}
if (sink.listContextProperties().isEmpty()) {
throw new ServiceDocumentationSyntaxError("'mediaType property' is missing");
}
Frame frame = typeManager.getFrameByUri(sink.getRdfType().stringValue());
if (frame == null) {
throw new RuntimeException("Frame not found: " + sink.getRdfType().stringValue());
}
sink.setFrame(frame);
}
private void setEditors(ServiceDocumentation sink, String value) {
StringTokenizer tokens = new StringTokenizer(value, "\n");
while (tokens.hasMoreTokens()) {
String text = tokens.nextToken().trim();
if (text.length()>0) {
sink.getEditors().add(parsePerson(text));
}
}
}
private Person parsePerson(String line) {
String personName = line;
String orgName = null;
int comma = line.indexOf(',');
if (comma > 0) {
personName = line.substring(0, comma).trim();
orgName = line.substring(comma+1).trim();
}
Person person = new Person();
person.setPersonName(personName);
person.setOrgName(orgName);
return person;
}
private void setAuthors(ServiceDocumentation sink, String value) {
StringTokenizer tokens = new StringTokenizer(value, "\n");
while (tokens.hasMoreTokens()) {
String text = tokens.nextToken().trim();
if (text.length()>0) {
sink.getEditors().add(parsePerson(text));
}
}
}
private String getLocalName(String uri) {
int hash = uri.lastIndexOf('#');
int slash = uri.lastIndexOf('/');
int delim = Math.max(hash, slash);
String localName = uri.substring(delim+1);
return localName;
}
private void addDefaults(Properties properties, ServiceDocumentation doc, ServiceMethodInfo methodInfo) {
List<ContextProperties> contextList = doc.listContextProperties();
if (contextList.isEmpty()) return;
String typeName = doc.getRdfType().getLocalName();
setServiceDocumentationFile(doc);
setCssFile(doc);
setMethods(doc);
setPostResponseMediaTypeRef(doc, typeName);
setTitle(doc, typeName);
setAbstractText(doc, typeName);
setIntroduction(doc, typeName);
setRepresentationHeading(doc, typeName);
setPostDoc(doc, methodInfo, typeName);
setGetDoc(properties, doc, typeName);
setContainerGetDoc(properties, doc, typeName);
setPutDoc(doc, typeName);
setDeleteDoc(doc, typeName);
setRepresentationText(doc, typeName);
}
private void setContainerGetDoc(Properties properties, ServiceDocumentation doc, String typeName) {
Frame frame = doc.getFrame();
List<Frame> containerList = frame.getContainerList();
if (containerList == null || containerList.isEmpty()) {
return;
}
MethodDocumentation method = doc.getContainerGetDocumentation();
if (method == null) {
method = new MethodDocumentation();
doc.setContainerGetDocumentation(method);
}
Uri containerType = null;
String containerURI = null;
if (doc.getContainerType() != null) {
containerType = doc.getContainerType();
} else {
containerType = new Uri(containerList.get(0).getUri());
doc.setContainerType(containerType);
}
// TODO: deal with the cases where there is more than one acceptable containerFormat or
// more than one container type.
String summary = method.getSummary();
if (summary == null) {
summary = MessageFormat.format(
"To get a paginated list of {0} resources from a {1}, the client submits an HTTP " +
"GET request to the container endpoint in accordance with the following rules:",
typeName, containerType.getLocalName());
method.setSummary(summary);
}
String containerFormat = null;
List<String> list = method.getResponseMediaTypes();
if (list.isEmpty()) {
List<ContextProperties> contextList =
contextManager.listContextPropertiesForClass(containerType.stringValue());
if (!contextList.isEmpty()) {
ContextProperties p = contextList.get(0);
containerFormat = p.getMediaType();
}
}
if (containerFormat == null) {
List<String> responseTypeList = method.getResponseMediaTypes();
containerFormat = responseTypeList.isEmpty() ? "???" : responseTypeList.get(0);
}
if (list.isEmpty()) {
list.add(containerFormat);
}
// Request Headers
method.addRequestHeader("Authorization", "Authorization parameters dictated by the OAuth Body Hash Protocol");
// Status Codes
if (!method.contains(ResponseInfo.OK)) {
String description = MessageFormat.format(
"<p>The request was successful.</p>\n" +
"<p>The response body contains the first <a href=\"http://www.w3.org/TR/ldp/#paging\">Page</a>\n" +
"of {0} resources in the format defined by the <code>{1}</code> media type. This Page has its own URL\n" +
"which is distinct from the URL of the <code>{2}</code>. The URL of the first Page is supplied in\n" +
"the <code>Content-Location</code> header.</p>\n" +
"<p>Clients should be aware that a server may send a 303 redirect to the first Page (see below)\n" +
"instead of delivering this Page directly in the entity body. Likewise a server may send a 301\n" +
"or 307 redirect.",
typeName, containerFormat, containerType.getLocalName());
method.add(ResponseInfo.OK.copy(description));
}
method.add(ResponseInfo.MOVED_PERMANENTLY);
if (!method.contains(ResponseInfo.SEE_OTHER)) {
String description = MessageFormat.format(
"This status code signals that the <code>{0}</code> itself cannot be delivered in the response.\n" +
"Instead, the client may receive the first page from a paginated list of {1} resources at the\n" +
"address specified by the <code>Location</code> header.",
containerType.getLocalName(), typeName);
method.add(ResponseInfo.SEE_OTHER.copy(description));
}
method.add(ResponseInfo.TEMPORARY_REDIRECT);
method.add(ResponseInfo.UNAUTHORIZED);
method.add(ResponseInfo.NOT_FOUND);
method.add(ResponseInfo.INTERNAL_SERVER_ERROR);
}
private void setCssFile(ServiceDocumentation doc) {
File htmlFile = doc.getServiceDocumentationFile();
String cssPath = serviceFileManager.getRelativeCssPath(htmlFile);
doc.setCss(cssPath);
}
private void setServiceDocumentationFile(ServiceDocumentation doc) {
doc.setServiceDocumentationFile(serviceFileManager.getServiceDocumentationFile(doc.getRdfType().stringValue()));
}
private void setMethods(ServiceDocumentation doc) {
if (doc.getMethodList().isEmpty()) {
doc.add(HttpMethod.POST);
doc.add(HttpMethod.GET);
doc.add(HttpMethod.PUT);
doc.add(HttpMethod.DELETE);
}
}
private void setPostResponseMediaTypeRef(ServiceDocumentation doc, String typeName) {
if (doc.getPostResponseMediaTypeRef() == null) {
String value = contextManager.createIdMediaTypeRef(typeName);
doc.setPostResponseTypeRef(value);
}
}
private void setGetDoc(Properties properties, ServiceDocumentation doc, String typeName) {
if (doc.getGetDocumentation()==null) {
MethodDocumentation method = new MethodDocumentation();
doc.setGetDocumentation(method);
String pattern = doc.getGetInstructions();
if (pattern == null) {
pattern =
"To get a representation of a particular {0} instance, the client submits an HTTP GET request to the resource''s " +
"REST endpoint, in accordance with the following rules:";
}
method.setSummary(format(pattern, typeName));
setGetRequestHeadersDefault(doc);
method.setRequestBodyRequirement(GET_REQUEST_BODY_DEFAULT);
}
setGetStatus(properties, doc);
setGetResponseDefault(doc);
}
private void setGetResponseDefault(ServiceDocumentation doc) {
MethodDocumentation method = doc.getGetDocumentation();
if (!method.contains(ResponseInfo.OK)) {
ResponseInfo okInfo = null;
List<ContextProperties> list = doc.listContextProperties();
if (list.size()==1) {
String mediaType = list.get(0).getMediaType();
String pattern = "The request was successful. " +
"<P>The response contains a JSON document in the format defined by the <code>{0}</code> media type.";
okInfo = ResponseInfo.OK.copy(format(pattern, mediaType));
} else {
StringBuilder builder = new StringBuilder();
builder.append("The request was successful.\n");
builder.append("<p>The response contains a document in one of the formats specified by the Accept header.</p>");
okInfo = ResponseInfo.OK.copy(builder.toString());
addAcceptHeader(doc, method);
}
method.add(okInfo);
}
addResponse(method, ResponseInfo.UNAUTHORIZED);
addResponse(method, ResponseInfo.MOVED_PERMANENTLY);
addResponse(method, ResponseInfo.TEMPORARY_REDIRECT);
addResponse(method, ResponseInfo.NOT_FOUND);
if (doc.hasMultipleFormats()) {
addResponse(method, ResponseInfo.NOT_ACCEPTABLE);
}
addResponse(method, ResponseInfo.INTERNAL_SERVER_ERROR);
}
private void addAcceptHeader(ServiceDocumentation doc, MethodDocumentation method) {
List<ContextProperties> list = doc.listContextProperties();
int count = list.size() + doc.getMediaTypeUriMap().size() + (doc.isAllowArbitraryFormat() ? 2 : 0) +
(doc.isAllowHtmlFormat() ? 2 : 0);
StringBuilder builder = new StringBuilder();
if (count == 1 && list.size()==1) {
builder.append(list.get(0).getMediaType());
} else {
builder.append("A comma-separated list containing at least one of the following media types: \n");
builder.append("<UL>\n");
LinkManager linkManager = new LinkManager(doc.getServiceDocumentationFile());
for (ContextProperties context : list) {
String mediaType = context.getMediaType();
String href = linkManager.relativize(context.getMediaTypeDocFile());
builder.append(" <LI><code><a href=\"");
builder.append(href);
builder.append("\">");
builder.append(mediaType);
builder.append("</a></code>\n");
}
for (Map.Entry<String, String> e : doc.getMediaTypeUriMap().entrySet()) {
String mediaType = e.getKey();
String href = e.getValue();
builder.append(" <LI><code><a href=\"");
builder.append(href);
builder.append("\">");
builder.append(mediaType);
builder.append("</a></code>\n");
}
if (doc.isAllowHtmlFormat()) {
builder.append(" <LI><code>text/html</code>\n");
}
if (doc.isAllowArbitraryFormat()) {
String type = doc.getRdfType().getLocalName();
builder.append(" <LI> ...any other media type for ");
builder.append(article(type));
builder.append(type);
builder.append(" resource");
}
builder.append("</UL>\n");
String defaultMediaType = doc.getDefaultMediaType();
if (defaultMediaType != null) {
builder.append("The Accept header is optional. If omitted, the <code>");
builder.append(defaultMediaType);
builder.append(" format is assumed by default.");
}
}
method.addRequestHeader("Accept", builder.toString());
}
private void setGetRequestHeadersDefault(ServiceDocumentation doc) {
MethodDocumentation method = doc.getGetDocumentation();
method.addRequestHeader("Authorization", "<em>Authorization parameters dictated by the OAuth Body Hash Protocol</em>");
if (doc.isContentNegotiation()) {
addAcceptHeader(doc, method);
}
}
private void setPutDoc(ServiceDocumentation doc, String typeName) {
if (doc.getPutDocumentation()==null) {
MethodDocumentation method = new MethodDocumentation();
doc.setPutDocumentation(method);
String pattern = doc.getPutInstructions();
if (pattern == null) {
pattern =
"To update a particular {0} instance, the client submits an HTTP PUT request to the resource''s " +
"REST endpoint in accordance with the following rules:";
}
method.setSummary(format(pattern, typeName));
addContentTypeHeader(doc, method);
method.addRequestHeader("AUTHORIZATION", "<em>Authorization parameters dictated by the OAuth Body Hash Protocol</em>");
method.setRequestBodyRequirement(
"The request body must contain a JSON document in the format defined by the Content-Type request header." );
if (!method.contains(ResponseInfo.OK)) {
ResponseInfo info = ResponseInfo.OK.copy("The request was successful.");
method.add(info);
}
addResponse(method, ResponseInfo.UNAUTHORIZED);
addResponse(method, ResponseInfo.NOT_FOUND);
addResponse(method, ResponseInfo.INTERNAL_SERVER_ERROR);
}
}private void addContentTypeHeader(ServiceDocumentation doc, MethodDocumentation method) {
List<ContextProperties> list = doc.listContextProperties();
if (list.size()==1) {
String mediaType = list.get(0).getMediaType();
method.addRequestHeader("Content-Type", "<code>" + mediaType + "</code>");
} else {
StringBuilder builder = new StringBuilder();
builder.append("One of:\n");
builder.append("<UL>\n");
for (ContextProperties context : list) {
builder.append(" <LI>");
builder.append(context.getMediaType());
builder.append("\n");
}
for (Map.Entry<String,String> e : doc.getMediaTypeUriMap().entrySet()) {
String mediaType = e.getKey();
builder.append(" <LI>");
builder.append(mediaType);
builder.append("\n");
}
builder.append("</UL>\n");
if (doc.isAllowArbitraryFormat()) {
String typeName = doc.getRdfType().getLocalName();
builder.append("<p>Or any arbitrary media type for ");
builder.append(article(typeName));
builder.append(typeName);
builder.append(".</p>");
}
method.addRequestHeader("Content-Type", builder.toString());
}
}
private void setDeleteDoc(ServiceDocumentation doc, String typeName) {
if (doc.getDeleteDocumentation()==null) {
MethodDocumentation method = new MethodDocumentation();
doc.setDeleteDocumentation(method);
String pattern =
"To delete a particular {0} instance, the client submits an HTTP DELETE request to the resource''s " +
"REST endpoint in accordance with the following rules:";
method.setSummary(format(pattern, typeName));
method.addRequestHeader("Authorization", "<em>Authorization parameters dictated by the OAuth Body Hash Protocol</em>");
if (doc.hasMultipleFormats()) {
StringBuilder builder = new StringBuilder();
builder.append("The format for one specific representation of the ");
builder.append(typeName);
builder.append(" resource that is to be deleted. If the Content-Type header is not specified, then all ");
builder.append("representations of the resource will be deleted.");
method.addRequestHeader("Content-Type", builder.toString());
}
method.setRequestBodyRequirement("The request body must be empty.");
if (!method.contains(ResponseInfo.OK)) {
ResponseInfo info = ResponseInfo.OK.copy("The request was successful and the resource has been deleted.");
method.add(info);
}
addResponse(method, ResponseInfo.UNAUTHORIZED);
addResponse(method, ResponseInfo.NOT_FOUND);
addResponse(method, ResponseInfo.INTERNAL_SERVER_ERROR);
}
}
private void addResponse(MethodDocumentation method, ResponseInfo response) {
if (!method.contains(response)) {
method.add(response);
}
}
private void setPostDoc(ServiceDocumentation doc, ServiceMethodInfo methodInfo, String typeName) {
List<HttpMethod> list = doc.getMethodList();
if (!list.contains(HttpMethod.POST)) {
return;
}
MethodDocumentation method=doc.getPostDocumentation();
if (method == null) {
method = new MethodDocumentation();
doc.setPostDocumentation(method);
}
if (method.getSummary()==null) {
String pattern =
"To create a new {0} instance within the server, a client submits an HTTP POST request to the server''s " +
"{0} container endpoint in accordance with the following rules: ";
method.setSummary(format(pattern, typeName));
}
if (method.getRequestBodyRequirement() == null) {
method.setRequestBodyRequirement(
"The request body MUST be a JSON document that conforms to the format specified by the Content-Type header.");
}
applyRequestMediaTypes(doc, method);
String idMediaType = doc.getPostResponseMediaType();
String createdDescription = null;
// TODO: Support the option of multiple response types, in which case one of them
// will be a default, but others may be requested via the Accept header.
if (idMediaType == null) {
createdDescription = "The request has succeeded.\n" +
"<p>The reponse will contain an empty body.</p>";
} else {
ContextProperties context = contextManager.getContextPropertiesByMediaType(idMediaType);
if (context == null) {
throw new ServiceDocumentationSyntaxError("Unknown media type: " + idMediaType);
}
LinkManager linkManager = new LinkManager(doc.getServiceDocumentationFile());
String href = linkManager.relativize(context.getMediaTypeDocFile());
StringBuilder anchor = new StringBuilder();
appendAnchor(anchor, href, idMediaType);
createdDescription = methodInfo.getPostCreatedDescription();
if (createdDescription == null) {
createdDescription =
"The request has succeeded.\n" +
"<p>The response contains a small JSON document that provides the endpoint URI for the newly created " +
"<code>{0}</code> resource. This JSON document must conform to the <code>{1}</code> format. " +
"The <code>Content-Type</code> header of the response will be set to this media type.";
}
createdDescription = format(createdDescription, typeName, anchor.toString());
}
method.addRequestHeader("Authorization", "<em>Authorization parameters dictated by the OAuth Body Hash Protocol</em>");
addResponse(method, ResponseInfo.CREATED.copy(createdDescription));
addResponse(method, ResponseInfo.BAD_REQUEST);
addResponse(method, ResponseInfo.UNAUTHORIZED);
addResponse(method, ResponseInfo.INTERNAL_SERVER_ERROR);
}
private void applyRequestMediaTypes(ServiceDocumentation doc, MethodDocumentation method) {
List<String> typeList = method.getRequestContentTypes();
String value = null;
if (!typeList.isEmpty()) {
if (typeList.size() == 1) {
value = typeList.get(0);
} else {
StringBuilder builder = new StringBuilder();
builder.append("<em>One of</em>\n");
builder.append("<ul>\n");
for (String type : typeList) {
builder.append("<li><code>");
builder.append(type);
builder.append("</code>\n");
}
builder.append("</ul>");
value = builder.toString();
}
} else if (doc.isAllowArbitraryFormat()) {
value = "<em>An arbitrary media type</em>";
}
if (value != null) {
method.addRequestHeader("Content-Type", value);
}
}
private void appendAnchor(StringBuilder builder, String href, String text) {
builder.append("<a href=\"");
builder.append(href);
builder.append("\">");
builder.append(text);
builder.append("</a>");
}
private void setRepresentationText(ServiceDocumentation doc, String typeName) {
if (doc.getRepresentationText() == null) {
List<ContextProperties> list = doc.listContextProperties();
if (list.size()==1) {
String mediaType = list.get(0).getMediaType();
String mediaTypeRef = list.get(0).getMediaTypeRef();
String pattern =
"<p><code>{0}</code> resources manipulated via this REST API are represented as JSON documents in " +
"the <code>{1}</code> format. For detailed information about this media type, see {2}.</p>";
doc.setRepresentationText(format(pattern, typeName, mediaType, mediaTypeRef));
} else {
StringBuilder builder = new StringBuilder();
builder.append("<code>{0}</code> resources accessed through this REST API are represented by documents in ");
builder.append(" a variety of formats including");
if (doc.isAllowArbitraryFormat()) {
builder.append(" (but not limited to)");
}
builder.append(":\n");
builder.append("<div class=\"mediatype\">\n");
LinkManager linkManager = new LinkManager(doc.getServiceDocumentationFile());
for (ContextProperties context : list) {
String mediaType = context.getMediaType();
File mediaTypeFile = context.getMediaTypeDocFile();
String href = linkManager.relativize(mediaTypeFile);
builder.append(" <div><a href=\"");
builder.append(href);
builder.append("\">");
builder.append(mediaType);
builder.append("</a></div>\n");
}
for (Map.Entry<String,String> e : doc.getMediaTypeUriMap().entrySet()) {
String mediaType = e.getKey();
String href = e.getValue();
builder.append(" <div>");
appendAnchor(builder, href, mediaType);
builder.append("</div>\n");
}
if (doc.isAllowHtmlFormat()) {
builder.append(" <div>text/html</div>");
}
builder.append("</div>\n");
if (doc.isAllowArbitraryFormat()) {
builder.append("<p>This list is not exhaustive; in general the REST API supports arbitrary\n");
builder.append("content types for {0} resources. Thus, a client may POST ");
builder.append(article(typeName));
builder.append(typeName);
builder.append(" representation in some arbitrary format and later GET that representation from the server.\n");
builder.append("The API supports multiple representations of a given resource simultaneously.</p>");
}
builder.append("<p>{0} resources have ");
appendAnchor(builder, "http://www.w3.org/Provider/Style/URI.html", "Cool URLs");
builder.append(". This means that there is a single URL for each resource and the URL does not\n");
builder.append("change for different representations (or versions) of the resource. Each representation is\n");
builder.append("defined by a different media type, and clients access specific representations\n");
builder.append("through ");
appendAnchor(builder, "http://www.w3.org/Protocols/rfc2616/rfc2616-sec12.html", "content negotiation");
builder.append(".</p>");
doc.setRepresentationText(format(builder.toString(), typeName));
}
MethodDocumentation method = doc.getContainerGetDocumentation();
if (method != null) {
String text = null;
List<String> mediaTypeList = method.getResponseMediaTypes();
if (mediaTypeList.size()==1 && doc.getContainerType()!=null) {
String containerName = doc.getContainerType().getLocalName();
String containerArticle = StringUtil.article(containerName);
String containerMediaType = mediaTypeList.get(0);
ContextProperties context = contextManager.getContextPropertiesByMediaType(containerMediaType);
if (context == null) {
logger.warn("ContextProperties not found: " + containerMediaType);
} else {
String containerFormat = context.getMediaTypeRef();
String pattern =
"<p>It is also possible to obtain a paginated list of <code>{0}</code> resources from " +
"{1} <code>{2}</code> in the <code>{3}</code> format. For detailed information about this media type, " +
"see {4}."
;
text = MessageFormat.format(pattern, typeName, containerArticle, containerName, containerMediaType, containerFormat);
}
}
// TODO: handle the case where mediaTypeList.size() > 1
if (text != null) {
StringBuilder builder = new StringBuilder(doc.getRepresentationText());
builder.append("\n");
builder.append(text);
doc.setRepresentationText(builder.toString());
}
}
}
}
private String article(String typeName) {
char c = Character.toUpperCase(typeName.charAt(0));
return (c=='A' || c=='E' || c=='I' || c=='O' || c=='U') ? "an " : "a ";
}
private void setRepresentationHeading(ServiceDocumentation doc,
String typeName) {
if (doc.getRepresentationHeading() == null) {
String pattern = "{0} Representations";
doc.setRepresentationHeading(format(pattern, typeName));
}
}
private void setIntroduction(ServiceDocumentation doc, String typeName) {
if (doc.getIntroduction() == null) {
String value = null;
if (doc.getMethodList().size()==1) {
String pattern =
"<P>This specification defines a REST API for " + actionList(doc) +
"<code>{0}</code> resources via an HTTP " + doc.getMethodList().get(0).getName() + " request.</P>";
value = format(pattern, typeName);
} else {
String pattern =
"<P>This specification defines a REST API for " + actionList(doc) +
"<code>{0}</code> resources. Following common conventions, the API " +
"uses a different HTTP verb for each type of operation: " + methodUsage(doc) +
"</P>\n" +
"<P>Implementations of this REST API may be incomplete; a given server " +
"might support only a subset of the HTTP verbs. A server that supports the complete API will \n" +
"expose two different kinds of endpoints: a <em>container</em> endpoint for receiving POST \n" +
"requests and <em>item</em> endpoints for manipulating individual instances. This specification \n" +
"document does not prescribe a method for discovering the endpoint URLs.</P>"
;
value = format(pattern, typeName);
}
doc.setIntroduction(value);
}
}
private String methodUsage(ServiceDocumentation doc) {
StringBuilder builder = new StringBuilder();
List<HttpMethod> list = doc.getMethodList();
for (int i=0; i<list.size(); i++) {
HttpMethod method = list.get(i);
if (i>0 && i==list.size()-1) {
builder.append(" and ");
} else if (i>0) {
builder.append(", ");
}
if (method == HttpMethod.POST) {
builder.append("<code>POST</code> for create");
} else if (method == HttpMethod.GET) {
builder.append("<code>GET</code> for read");
} else if (method == HttpMethod.PUT) {
builder.append("<code>PUT</code> for update");
} else if (method == HttpMethod.DELETE) {
builder.append("<code>DELETE</code> for delete");
}
}
builder.append(' ');
return builder.toString();
}
private void setTitle(ServiceDocumentation doc, String typeName) {
if (doc.getTitle() == null) {
List<ContextProperties> list = doc.listContextProperties();
if (list.size() == 1) {
String mediaType = list.get(0).getMediaType();
doc.setTitle(format("A REST API for {0} Resources<br/>" +
"in the <code>{1}</code> Format", typeName, mediaType));
} else {
doc.setTitle(format("A REST API for {0} Resources in multiple formats", typeName));
}
}
// if (doc.getSubtitle() == null) {
// doc.setSubtitle(format("in a format defined by the <code>{0}</code> Media Type", doc.getMediaType()));
// }
}
private void setAbstractText(ServiceDocumentation doc, String typeName) {
if (doc.getAbstactText()==null) {
String pattern =
"This specification defines a REST API for " + actionList(doc) +
"<code>{0}</code> resources.";
doc.setAbstactText(format(pattern, typeName));
}
}
private String actionList(ServiceDocumentation doc) {
StringBuilder builder = new StringBuilder();
List<HttpMethod> list = doc.getMethodList();
for (int i=0; i<list.size(); i++) {
HttpMethod method = list.get(i);
if (i>0 && i==list.size()-1) {
builder.append(" and ");
} else if (i>0) {
builder.append(", ");
}
builder.append(method.getGerund());
}
builder.append(' ');
return builder.toString();
}
private String format(String pattern, Object... arg) {
return MessageFormat.format(pattern, arg);
}
public ServiceDocumentationList getServiceDocumentationByRdfType(String rdfTypeURI) {
return map.get(rdfTypeURI);
}
public File getServiceDocumentationFile(String rdfTypeURI) {
return serviceFileManager.getServiceDocumentationFile(rdfTypeURI);
}
public void writeAll() throws IOException {
for (List<ServiceDocumentation> list : map.values()) {
for (ServiceDocumentation doc : list) {
write(doc);
}
}
}
private void write(ServiceDocumentation doc) throws IOException {
if (doc == null) return;
String text = printer.print(doc);
File htmlFile = serviceFileManager.getServiceDocumentationFile(doc.getRdfType().stringValue());
htmlFile.getParentFile().mkdirs();
OutputStream out = new FileOutputStream(htmlFile);
OutputStreamWriter writer = new OutputStreamWriter(out);
try {
writer.write(text);
writer.flush();
} finally {
writer.close();
}
}
static class ServiceMethodInfo {
private String postCreatedDescription;
public String getPostCreatedDescription() {
return postCreatedDescription;
}
public void setPostCreatedDescription(String postCreatedDescription) {
this.postCreatedDescription = postCreatedDescription;
}
}
}