/*
* Copyright (c) 2014 Red Hat, 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.ovirt.engine.api.rsdl;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import org.ovirt.engine.api.model.Actionable;
import org.ovirt.engine.api.model.BaseResources;
import org.ovirt.engine.api.model.DetailedLink;
import org.ovirt.engine.api.model.DetailedLinks;
import org.ovirt.engine.api.model.GeneralMetadata;
import org.ovirt.engine.api.model.Header;
import org.ovirt.engine.api.model.Headers;
import org.ovirt.engine.api.model.HttpMethod;
import org.ovirt.engine.api.model.Parameter;
import org.ovirt.engine.api.model.ParametersSet;
import org.ovirt.engine.api.model.Request;
import org.ovirt.engine.api.model.Response;
import org.ovirt.engine.api.model.Rsdl;
import org.ovirt.engine.api.model.Schema;
import org.ovirt.engine.api.model.Statistics;
import org.ovirt.engine.api.model.Url;
import org.ovirt.engine.api.resource.CreationResource;
import org.ovirt.engine.api.resource.SystemResource;
public class RsdlBuilder {
private static final String COLLECTION_PARAMETER_RSDL = "collection";
private static final String COLLECTION_PARAMETER_YAML = "--COLLECTION";
private static final String DEPRECATED_PARAMETER_YAML = "--DEPRECATED";
private Rsdl rsdl;
private Map<String, Action> parametersMetaData;
private String rel;
private String href;
private Schema schema;
private GeneralMetadata generalMetadata;
private String description;
private List<String> rels;
private MetaData metadata;
private static final String ACTION = "Action";
private static final String DELETE = "delete";
private static final String UPDATE = "update";
private static final String GET = "get";
private static final String ADD = "add";
private static final String RESOURCES_PACKAGE = "org.ovirt.engine.api.resource";
public RsdlBuilder(List<String> rels, MetaData metadata) {
this.rels = rels;
this.metadata = metadata;
this.parametersMetaData = addParametersMetaData();
}
public Map<String, Action> addParametersMetaData() {
parametersMetaData = new HashMap<>();
for (Action action : metadata.getActions()) {
parametersMetaData.put(action.getName(), action);
}
return parametersMetaData;
}
private Rsdl construct() throws ClassNotFoundException, IOException {
Rsdl rsdl = new Rsdl();
rsdl.setLinks(new DetailedLinks());
for (DetailedLink link : getLinks()) {
rsdl.getLinks().getLinks().add(link);
}
uniteDuplicateLinks(rsdl);
Collections.sort(rsdl.getLinks().getLinks(),
Comparator.comparing(DetailedLink::getHref).thenComparing(DetailedLink::getRel));
return rsdl;
}
public Rsdl build() throws ClassNotFoundException, IOException {
rsdl = construct();
rsdl.setRel(getRel());
rsdl.setHref(getHref());
rsdl.setDescription(getDescription());
rsdl.setSchema(getSchema());
rsdl.setGeneral(getGeneralMetadata());
return rsdl;
}
public RsdlBuilder rel(String rel) {
this.rel = rel;
return this;
}
public RsdlBuilder href(String href) {
this.href = href;
return this;
}
public RsdlBuilder schema(Schema schema) {
this.schema = schema;
return this;
}
public RsdlBuilder generalMetadata(GeneralMetadata entryPoint) {
this.generalMetadata = entryPoint;
return this;
}
public RsdlBuilder description(String description) {
this.description = description;
return this;
}
public String getHref() {
return this.href;
}
public String getRel() {
return this.rel;
}
public Schema getSchema() {
return schema;
}
public GeneralMetadata getGeneralMetadata() {
return generalMetadata;
}
public String getDescription() {
return this.description;
}
@Override
public String toString() {
return "RSDL Href: " + getHref() +
", Description:" + getDescription() +
", Links: " + (rsdl != null ? rsdl.isSetLinks() ? rsdl.getLinks().getLinks().size() : "0" : "0") + ".";
}
public class LinkBuilder {
private DetailedLink link = new DetailedLink();
public LinkBuilder url(String url) {
link.setHref(url);
return this;
}
public LinkBuilder description(String description) {
link.setDescription(description);
return this;
}
public LinkBuilder rel(String rel) {
link.setRel(rel);
return this;
}
public LinkBuilder requestParameter(final String requestParameter) {
org.ovirt.engine.api.model.Body body = new org.ovirt.engine.api.model.Body();
body.setType(requestParameter);
Request request = new Request();
request.setBody(body);
link.setRequest(request);
return this;
}
public LinkBuilder responseType(final String responseType) {
Response response = new Response();
response.setType(responseType);
link.setResponse(response);
return this;
}
public LinkBuilder httpMethod(HttpMethod httpMethod) {
if(!link.isSetRequest()) {
link.setRequest(new Request());
}
link.getRequest().setHttpMethod(httpMethod);
return this;
}
public DetailedLink build() {
if (!link.getRequest().isSetBody()) {
link.getRequest().setBody(new org.ovirt.engine.api.model.Body());
}
return addParametersMetadata(link);
}
}
public Collection<DetailedLink> getLinks() throws ClassNotFoundException, IOException {
//SortedSet<Link> results = new TreeSet<Link>();
List<DetailedLink> results = new ArrayList<>();
for (String path : rels) {
Class<?> resource = findResource(path);
results.addAll(describe(resource, path, new HashMap<>()));
}
return results;
}
private Class<?> findResource(String path) throws ClassNotFoundException, IOException {
for (Method locator : SystemResource.class.getDeclaredMethods()) {
if (path.equals(getPath(locator))) {
return locator.getReturnType();
}
}
return null;
}
private String getPath(Method method) {
Path pathAnnotation = method.getAnnotation(Path.class);
return pathAnnotation==null ? null : pathAnnotation.value();
}
public List<DetailedLink> describe(Class<?> resource, String prefix, Map<String, Type> parametersMap) throws ClassNotFoundException {
//SortedSet<Link> results = new TreeSet<Link>();
List<DetailedLink> results = new ArrayList<>();
if (resource!=null) {
for (Method m : resource.getMethods()) {
if (isConcreteReturnType(m, resource)) {
handleMethod(prefix, results, m, resource, parametersMap);
}
}
}
return results;
}
private boolean isConcreteReturnType(Method method, Class<?> resource) {
for (Method m : resource.getMethods()) {
if (!m.equals(method)
&& m.getName().equals(method.getName())
&& parameterTypesEqual(m.getParameterTypes(), method.getParameterTypes())
&& method.getReturnType().isAssignableFrom(m.getReturnType())) {
return false;
}
}
return true;
}
private boolean parameterTypesEqual(Class<?>[] types1, Class<?>[] types2) {
if (types1.length!=types2.length) {
return false;
} else {
for (int i=0; i<types1.length; i++) {
if (!(types1[i].isAssignableFrom(types2[i]) || types2[i].isAssignableFrom(types1[i]))) {
return false;
}
}
return true;
}
}
private void addToGenericParamsMap (Class<?> resource, Type[] paramTypes, Type[] genericParamTypes, Map<String, Type> parametersMap) {
for (int i=0; i<genericParamTypes.length; i++) {
if (paramTypes[i].toString().length() == 1) {
//if the parameter type is generic - don't add to map, as it might override a more meaningful value:
//for example, without this check we could replace <"R", "Template"> with <"R", "R">, and lose information.
} else {
//if the length is greater than 1, we have an actual type (e.g: "CdRoms"), and we want to add it to the
//map, even if it overrides an existing value.
parametersMap.put(genericParamTypes[i].toString(), paramTypes[i]);
}
}
}
private void handleMethod(String prefix, Collection<DetailedLink> results, Method m, Class<?> resource, Map<String, Type> parametersMap) throws ClassNotFoundException {
if (isRequiresDescription(m)) {
Type genericReturnType = m.getGenericReturnType();
Class<?> concreteReturnType = findConcreteType(genericReturnType, resource, parametersMap);
if (concreteReturnType == null) {
concreteReturnType = m.getReturnType();
}
Type[] genericParameterTypes = m.getGenericParameterTypes();
Class<?>[] concreteParameterTypes = m.getParameterTypes();
for (int i = 0; i < concreteParameterTypes.length; i++) {
Class<?> concreteParameterType = findConcreteType(genericParameterTypes[i], resource, parametersMap);
if (concreteParameterType != null) {
concreteParameterTypes[i] = concreteParameterType;
}
}
if (m.isAnnotationPresent(javax.ws.rs.GET.class)) {
handleGet(prefix, results, concreteReturnType);
} else if (m.isAnnotationPresent(PUT.class)) {
handlePut(prefix, results, concreteReturnType);
} else if (m.isAnnotationPresent(javax.ws.rs.DELETE.class)) {
handleDelete(prefix, results, m);
} else if (m.isAnnotationPresent(Path.class)) {
String path = m.getAnnotation(Path.class).value();
if (isAction(m)) {
handleAction(prefix, results, path, m);
} else {
if (isSingleEntityResource(m)) {
path = "{" + getSingleForm(prefix) + ":id}";
}
if (m.getGenericReturnType() instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType)m.getGenericReturnType();
addToGenericParamsMap(resource, parameterizedType.getActualTypeArguments(), m.getReturnType().getTypeParameters(), parametersMap);
}
results.addAll(describe(concreteReturnType, prefix + "/" + path, new HashMap<>(parametersMap)));
}
} else {
if (m.getName().equals(ADD)) {
handleAdd(prefix, results, concreteParameterTypes);
}
}
}
}
private void handleAction(String prefix, Collection<DetailedLink> results, String path, Method m) {
Class<?>[] parameterTypes = m.getParameterTypes();
assert parameterTypes.length == 1;
String returnValueStr = parameterTypes[0].getSimpleName();
DetailedLink link = new RsdlBuilder.LinkBuilder().url(prefix + "/" + path).rel(path).requestParameter(ACTION).responseType(returnValueStr).httpMethod(HttpMethod.POST).build();
addCommonActionParameters(link);
addAsyncMatrixParameter(link);
results.add(link);
}
private void handleDelete(String prefix, Collection<DetailedLink> results, Method m) {
DetailedLink link = new RsdlBuilder.LinkBuilder().url(prefix)
.rel(DELETE)
.httpMethod(HttpMethod.DELETE)
.build();
Class<?>[] parameterTypes = m.getParameterTypes();
if (parameterTypes.length > 0) {
link.getRequest().getBody().setType(parameterTypes[0].getSimpleName());
}
addCommonActionParameters(link);
addAsyncMatrixParameter(link);
results.add(link);
}
private void handlePut(String prefix, Collection<DetailedLink> results, Class<?> returnType) {
String returnTypeStr = getReturnTypeStr(returnType);
DetailedLink link = new RsdlBuilder.LinkBuilder().url(prefix).rel(UPDATE).requestParameter(returnTypeStr).responseType(returnTypeStr).httpMethod(HttpMethod.PUT).build();
addAsyncMatrixParameter(link);
results.add(link);
}
private void handleGet(String prefix, Collection<DetailedLink> results, Class<?> returnType) {
String returnTypeStr = getReturnTypeStr(returnType);
DetailedLink link = new RsdlBuilder.LinkBuilder().url(prefix).rel(GET).responseType(returnTypeStr).httpMethod(HttpMethod.GET).build();
if (BaseResources.class.isAssignableFrom(returnType) && returnType != Statistics.class) {
addMaxMatrixParameter(link);
}
results.add(link);
}
/**
* Adds to a link the parameters that are common to all actions, like {@code async} and {@code grace_period.expiry}.
* These parameters will be added to all the signatures that have a body of type {@code Action}.
*
* @param link the link where the parameters will be added
*/
private void addCommonActionParameters(DetailedLink link) {
Request request = link.getRequest();
if (request != null) {
org.ovirt.engine.api.model.Body body = request.getBody();
if (body != null) {
String type = body.getType();
if (ACTION.equals(type)) {
List<ParametersSet> parametersSets = body.getParametersSets();
for (ParametersSet parametersSet : parametersSets) {
List<Parameter> parameters = parametersSet.getParameters();
parameters.add(newGracePeriodParameter());
parameters.add(newAsyncActionParameter());
}
}
}
}
}
/**
* Creates the definition of the {@code grace_period.expiry} action parameter.
*/
private Parameter newAsyncActionParameter() {
Parameter parameter = new Parameter();
parameter.setName("action.grace_period.expiry");
parameter.setRequired(false);
parameter.setType("xs:long");
return parameter;
}
/**
* Creates the definition of the {@code grace_period} action parameter.
*/
private Parameter newGracePeriodParameter() {
Parameter parameter = new Parameter();
parameter.setName("action.async");
parameter.setRequired(false);
parameter.setType("xs:boolean");
return parameter;
}
/**
* Adds to a link the {@code async} matrix parameter.
*
* @param link the link where the parameters will be added
*/
private void addAsyncMatrixParameter(DetailedLink link) {
Parameter parameter = new Parameter();
parameter.setName("async");
parameter.setRequired(false);
parameter.setType("xs:boolean");
parameter.setValue("true|false");
parameter.setContext("matrix");
addUrlParameter(link, parameter);
}
/**
* Adds to a link the {@code max} matrix parameter.
*
* @param link the link where the parameters will be added
*/
private void addMaxMatrixParameter(DetailedLink link) {
Parameter parameter = new Parameter();
parameter.setName("max");
parameter.setRequired(false);
parameter.setType("xs:int");
parameter.setValue("max results");
parameter.setContext("matrix");
addUrlParameter(link, parameter);
}
/**
* Adds to a link the a URL parameter, creating all the intermediate objects if they don't exist.
*
* @param link the link where the parameters will be added
* @param parameter the parameter to add
*/
private void addUrlParameter(DetailedLink link, Parameter parameter) {
Request request = link.getRequest();
if (request == null) {
request = new Request();
link.setRequest(request);
}
Url url = request.getUrl();
if (url == null) {
url = new Url();
request.setUrl(url);
}
List<ParametersSet> parametersSets = url.getParametersSets();
ParametersSet parametersSet;
if (parametersSets.isEmpty()) {
parametersSet = new ParametersSet();
parametersSets.add(parametersSet);
}
else {
parametersSet = parametersSets.get(0);
}
parametersSet.getParameters().add(parameter);
}
private DetailedLink addParametersMetadata(DetailedLink link) {
String link_name = link.getHref() + "|rel=" + link.getRel();
if (this.parametersMetaData.containsKey(link_name)) {
Action action = this.parametersMetaData.get(link_name);
if (action.getDescription() != null) {
link.setDescription(action.getDescription());
}
if (action.getRequest() != null) {
addUrlParams(link, action);
addHeaderParams(link, action);
addBodyParams(link, action);
}
}
return link;
}
private void addBodyParams(DetailedLink link, Action action) {
if (action.getRequest().getBody() != null) {
link.getRequest().getBody().setRequired(action.getRequest().getBody().isRequired());
if (action.getRequest().getBody().getSignatures() != null) {
for (Signature signature : action.getRequest().getBody().getSignatures()) {
ParametersSet ps = new ParametersSet();
if (signature.getDeprecated() != null) {
ps.setDeprecated(signature.getDeprecated());
}
if (signature.getDescription() != null) {
ps.setDescription(signature.getDescription());
}
addBodyParams(ps, signature.getMandatoryArguments().entrySet(), true);
addBodyParams(ps, signature.getOptionalArguments().entrySet(), false);
link.getRequest().getBody().getParametersSets().add(ps);
}
}
}
}
private void addBodyParams(ParametersSet ps, Set<Entry<Object, Object>> entrySet, boolean required) {
for (Entry<Object, Object> paramData : entrySet) {
Parameter param = createBodyParam(paramData, required);
ps.getParameters().add(param);
}
}
private Parameter createBodyParam(Entry<Object, Object> mandatoryKeyValuePair, boolean required) {
Parameter param = new Parameter();
param.setRequired(required);
String paramName = getParamName(mandatoryKeyValuePair);
param.setName(paramName);
if (mandatoryKeyValuePair.getKey().toString().contains(COLLECTION_PARAMETER_YAML)) {
handleCollection(mandatoryKeyValuePair, required, param);
} else {
param.setType(mandatoryKeyValuePair.getValue().toString());
}
if (mandatoryKeyValuePair.getKey().toString().contains(DEPRECATED_PARAMETER_YAML)) {
param.setDeprecated(true);
}
return param;
}
private void handleCollection(Entry<Object, Object> mandatoryKeyValuePair, boolean required, Parameter param) {
param.setType(COLLECTION_PARAMETER_RSDL);
@SuppressWarnings("unchecked")
Map<Object, Object> listParams = (Map<Object, Object>)mandatoryKeyValuePair.getValue();
param.setParametersSet(new ParametersSet());
for (Entry<Object, Object> listParamData : listParams.entrySet()) {
Parameter listParam = createBodyParam(listParamData, required);
param.getParametersSet().getParameters().add(listParam);
}
}
private String getParamName(Entry<Object, Object> mandatoryKeyValuePair) {
String paramName = mandatoryKeyValuePair.getKey().toString();
if (paramName.contains("--")) {
paramName = paramName.substring(0, paramName.indexOf("--"));
}
return paramName;
}
private void addHeaderParams(DetailedLink link, Action action) {
// Add the parameters that are specified in the metadata:
if (action.getRequest().getHeaders() != null && !action.getRequest().getHeaders().isEmpty()) {
link.getRequest().setHeaders(new Headers());
for (Object key : action.getRequest().getHeaders().keySet()) {
Header header = new Header();
header.setName(key.toString());
Object value = action.getRequest().getHeaders().get(key);
if (value != null) {
ParamData paramData = (ParamData) value;
header.setValue(paramData.getValue());
header.setRequired(paramData.getRequired() == null ? Boolean.FALSE : paramData.getRequired());
header.setDeprecated(paramData.getDeprecated());
}
link.getRequest().getHeaders().getHeaders().add(header);
}
}
// All the operations that potentially modify the state of the system accept the "Correlation-Id" header, so
// instead of adding it explicitly in the metadata file it is better to add it implicitly:
if (!GET.equals(link.getRel())) {
addCorrelationIdHeader(link);
}
// All the operations that potentially send a body (everything except GET) should also specify
// the "Content-Type" header, so instead of explicitly adding it in the metadata file it is better to add it
// implicity:
if (!GET.equals(link.getRel())) {
addContentTypeHeader(link);
}
// All the operations that create a new entity (those whose rel is "add") support the "Expect" header with the
// "201-created" value, so instead of explicitly adding it in the metadata file it is better to add it
// implicitly:
if (ADD.equals(link.getRel())) {
addExpectHeader(link, "201-created");
}
// All the operations that update entities (those whose rel is "update") support the "Expect" header with the
// "202-accepted" value, so instead of explicitly adding it in the metadata file it is better to add it
// implicitly:
if (UPDATE.equals(link.getRel())) {
addExpectHeader(link, "202-accepted");
}
}
/**
* Adds the description of the {@code Correlation-Id} header to a link.
*
* @param link the link where the description of the header will be added
*/
private void addCorrelationIdHeader(DetailedLink link) {
Headers headers = link.getRequest().getHeaders();
if (headers == null) {
headers = new Headers();
link.getRequest().setHeaders(headers);
}
Header header = new Header();
header.setName("Correlation-Id");
header.setValue("any string");
header.setRequired(false);
headers.getHeaders().add(header);
}
/**
* Adds the description of the {@code Content-Type} header to a link.
*
* @param link the link where the description of the header will be added
*/
private void addContentTypeHeader(DetailedLink link) {
Headers headers = link.getRequest().getHeaders();
if (headers == null) {
headers = new Headers();
link.getRequest().setHeaders(headers);
}
Header header = new Header();
header.setName("Content-Type");
header.setValue("application/xml|json");
header.setRequired(true);
headers.getHeaders().add(header);
}
/**
* Adds the description of the {@code Expect} header to a link.
*
* @param link the link where the description of the header will be added
* @param value the value of the header
*/
private void addExpectHeader(DetailedLink link, String value) {
Headers headers = link.getRequest().getHeaders();
if (headers == null) {
headers = new Headers();
link.getRequest().setHeaders(headers);
}
Header header = new Header();
header.setName("Expect");
header.setValue(value);
header.setRequired(false);
headers.getHeaders().add(header);
}
private void addUrlParams(DetailedLink link, Action action) {
if (action.getRequest().getUrlparams() != null && !action.getRequest().getUrlparams().isEmpty()) {
link.getRequest().setUrl(new Url());
ParametersSet ps = new ParametersSet();
for (Object key : action.getRequest().getUrlparams().keySet()) {
Parameter param = new Parameter();
param.setName(key.toString());
Object value = action.getRequest().getUrlparams().get(key);
if (value != null) {
ParamData urlParamData = (ParamData)value;
param.setType(urlParamData.getType());
param.setContext(urlParamData.getContext());
param.setValue(urlParamData.getValue());
param.setRequired(urlParamData.getRequired()==null ? Boolean.FALSE : urlParamData.getRequired());
param.setDeprecated(urlParamData.getDeprecated());
}
ps.getParameters().add(param);
}
link.getRequest().getUrl().getParametersSets().add(ps);
}
}
private void handleAdd(String prefix, Collection<DetailedLink> results, Class<?>[] parameterTypes) {
assert parameterTypes.length == 1;
String s = parameterTypes[0].getSimpleName();
s = handleExcpetionalCases(s, prefix); //TODO: refactor to a more generic solution
results.add(new RsdlBuilder.LinkBuilder().url(prefix).rel(ADD).requestParameter(s).responseType(s).httpMethod(HttpMethod.POST).build());
}
private String handleExcpetionalCases(String s, String prefix) {
if (s.equals("BaseDevice")) {
if (prefix.contains("cdroms")) {
return "CdRom";
}
if (prefix.contains("nics")) {
return "NIC";
}
if (prefix.contains("disks")) {
return "Disk";
}
if (prefix.contains("watchdogs")) {
return "WatchDog";
}
}
return s;
}
/**
* get the class name, without package prefix
*/
private String getReturnTypeStr(Class<?> returnValue) {
int lastIndexOf = returnValue.getSimpleName().lastIndexOf(".");
String entityType = lastIndexOf==-1 ? returnValue.getSimpleName() : returnValue.getSimpleName().substring(lastIndexOf);
return entityType;
}
private Class<?> findConcreteType(Type generic, Class<?> resource, Map<String, Type> parametersMap) throws ClassNotFoundException {
for (Type superInterface : resource.getGenericInterfaces()) {
if (superInterface instanceof ParameterizedType) {
ParameterizedType p = (ParameterizedType)superInterface;
Class<?> clazz = Class.forName(p.getRawType().toString().substring(p.getRawType().toString().lastIndexOf(' ')+1));
Map<String, Type> map = new HashMap<>();
for (int i=0; i<p.getActualTypeArguments().length; i++) {
if (!map.containsKey(clazz.getTypeParameters()[i].toString())) {
map.put(clazz.getTypeParameters()[i].toString(), p.getActualTypeArguments()[i]);
}
}
if (map.containsKey(generic.toString())) {
String type = map.get(generic.toString()).toString();
try {
Class<?> returnClass = Class.forName(type.substring(type.lastIndexOf(' ')+1));
return returnClass;
} catch (ClassNotFoundException e) {
break;
}
}
}
}
if (parametersMap.containsKey(generic.toString())) {
try {
Type type = parametersMap.get(generic.toString());
Class<?> returnClass = Class.forName(type.toString().substring(type.toString().indexOf(' ') +1));
return returnClass;
} catch (ClassNotFoundException e) {
return null;
}
} else {
return null;
}
}
private boolean isSingleEntityResource(Method m) {
Annotation[][] parameterAnnotations = m.getParameterAnnotations();
for (int i=0; i<parameterAnnotations.length; i++) {
for (int j=0; j<parameterAnnotations[j].length; j++) {
if (parameterAnnotations[i][j].annotationType().equals(PathParam.class)) {
return true;
}
}
}
return false;
}
private boolean isAction(Method m) {
return m.isAnnotationPresent(Actionable.class);
}
private boolean isRequiresDescription(Method m) {
boolean pathRelevant = !(m.isAnnotationPresent(Path.class) && m.getAnnotation(Path.class).value().contains(":"));
boolean returnValueRelevant = !m.getReturnType().equals(CreationResource.class);
return pathRelevant && returnValueRelevant;
}
//might need to truncate the plural 's', for example:
//for "{api}/hosts/{host:id}/nics" return "nic"
//but for "{api}/hosts/{host:id}/storage" return "storage" (don't truncate last character)
private String getSingleForm(String prefix) {
int startIndex = prefix.lastIndexOf('/')+1;
prefix = prefix.substring(startIndex);
if (prefix.endsWith("ies")) {
return prefix.replaceAll("ies$", "y");
}
if (prefix.endsWith("s")) {
return prefix.replaceAll("s$", "");
}
return prefix;
}
/**
* There is a special kind of url: a url that may receive a body (with parameters in it),
* or may not. For example, when deleting a datacenter, the user may pass nothing in the body,
* or may pass <action><force>true</force></action>.
*
* RSDL builder will encounter both signatures during construction, and when it encounters the
* first is has no knowledge of the second yet, so it must create both linke.
*
* This method will be called at the end of construction, and search for such duplicate links.
* It will unite these pairs into a single link with required=false in the <body>.
*/
private void uniteDuplicateLinks(Rsdl rsdl) {
Map<String, DetailedLink> linksMap = new HashMap<>();
Collection<DetailedLink> linksToDelete = new LinkedList<>();
for (DetailedLink link : rsdl.getLinks().getLinks()) {
String linkId = link.getHref() + link.getRel();
if (linksMap.containsKey(linkId)) {
//duplicate found, determine which of the two should be deleted
DetailedLink linkToDelete = decideWhichToDelete(linksMap.get(linkId), link);
if (linkToDelete!=null) {
linksToDelete.add(linkToDelete);
}
} else {
linksMap.put(linkId, link);
}
}
for (DetailedLink link : linksToDelete) {
rsdl.getLinks().getLinks().remove(link);
}
}
private DetailedLink decideWhichToDelete(DetailedLink link1, DetailedLink link2) {
String link1ParamType = link1.getRequest().getBody().getType();
String link2ParamType = link2.getRequest().getBody().getType();
//Verify for both links that body is not mandatory
if ( (
(link1.getRequest().getBody().isRequired() != null)
&&
(link2.getRequest().getBody().isRequired() != null)
)
&&
(
(Boolean.FALSE.equals(link1.getRequest().getBody().isRequired()))
&&
(Boolean.FALSE.equals(link2.getRequest().getBody().isRequired()))
)
) {
if (link1ParamType!=null && link2ParamType==null) {
return link2;
}
if (link1ParamType==null && link2ParamType!=null) {
return link1;
}
}
return null;
}
}