/*
* Copyright (c) 2010 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.restapi.rsdl;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import org.ovirt.engine.api.common.util.FileUtils;
import org.ovirt.engine.api.common.util.ReflectionHelper;
import org.ovirt.engine.api.model.Actionable;
import org.ovirt.engine.api.model.Body;
import org.ovirt.engine.api.model.DetailedLink;
import org.ovirt.engine.api.model.DetailedLinks;
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.RSDL;
import org.ovirt.engine.api.model.Request;
import org.ovirt.engine.api.model.Response;
import org.ovirt.engine.api.model.Schema;
import org.ovirt.engine.api.model.Url;
import org.ovirt.engine.api.resource.CreationResource;
import org.ovirt.engine.api.restapi.resource.BackendApiResource;
import org.ovirt.engine.core.compat.LogCompat;
import org.ovirt.engine.core.compat.LogFactoryCompat;
import org.yaml.snakeyaml.Yaml;
public class RsdlBuilder {
private RSDL rsdl;
private String entryPoint;
private BackendApiResource apiResource;
private Map<String, Action> parametersMetaData;
private String rel;
private String href;
private Schema schema;
private String description;
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";
protected static final LogCompat LOG = LogFactoryCompat.getLog(RsdlBuilder.class);
private static final String RESOURCES_PACKAGE = "org.ovirt.engine.api.resource";
private static final String PARAMS_METADATA = "rsdl_metadata_v-3.1.yaml";
public RsdlBuilder(BackendApiResource apiResource) {
this.apiResource = apiResource;
this.entryPoint = apiResource.getUriInfo().getBaseUri().getPath();
this.parametersMetaData = loadParametersMetaData();
}
public Map<String, Action> loadParametersMetaData() {
parametersMetaData = new HashMap<String, Action>();
try {
InputStream stream = FileUtils.get(RESOURCES_PACKAGE, PARAMS_METADATA);
if (stream != null) {
Object result = new Yaml().load(stream);
for (Action action : ((MetaData)result).getActions()) {
parametersMetaData.put(action.getName(), action);
}
}
LOG.error("Parameters metatdata file not found.");
} catch (Exception e) {
LOG.error("Loading parameters metatdata failed.", e);
}
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);
}
return rsdl;
}
public RSDL build() {
try {
rsdl = construct();
rsdl.setRel(getRel());
rsdl.setHref(getHref());
rsdl.setDescription(getDescription());
rsdl.setSchema(getSchema());
} catch (Exception e) {
e.printStackTrace();
LOG.error("RSDL generation failure.", e);
}
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 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 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 rel(String rel) {
link.setRel(rel);
return this;
}
public LinkBuilder requestParameter(final String requestParameter) {
link.setRequest(new Request());
link.getRequest().setBody(new Body(){{setType(requestParameter);}});
return this;
}
public LinkBuilder responseType(final String responseType) {
link.setResponse(new Response(){{setType(responseType);}});
return this;
}
public LinkBuilder httpMethod(HttpMethod httpMethod) {
if(!link.isSetRequest()) {
link.setRequest(new Request());
}
link.getRequest().setHttpMethod(httpMethod);
return this;
}
public DetailedLink build() {
return addParametersMetadata(link);
}
}
public Collection<DetailedLink> getLinks() throws ClassNotFoundException, IOException {
//SortedSet<Link> results = new TreeSet<Link>();
List<DetailedLink> results = new ArrayList<DetailedLink>();
List<Class<?>> classes = ReflectionHelper.getClasses(RESOURCES_PACKAGE);
for (String path : apiResource.getRels()) {
Class<?> resource = findResource(path, classes);
results.addAll(describe(resource, entryPoint + "/" + path, new HashMap<String, Type>()));
}
return results;
}
private Class<?> findResource(String path, List<Class<?>> classes) throws ClassNotFoundException, IOException {
path = "/" + path;
for (Class<?> clazz : classes) {
if (path.equals(getPath(clazz))) {
return clazz;
}
}
return null;
}
private String getPath(Class<?> clazz) {
Path pathAnnotation = clazz.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<DetailedLink>();
if (resource!=null) {
for (Method m : resource.getMethods()) {
handleMethod(prefix, results, m, resource, parametersMap);
}
}
return results;
}
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)) {
Class<?> returnType = findReturnType(m, resource, parametersMap);
String returnTypeStr = getReturnTypeStr(returnType);
if (m.isAnnotationPresent(javax.ws.rs.GET.class)) {
handleGet(prefix, results, returnTypeStr);
} else if (m.isAnnotationPresent(PUT.class)) {
handlePut(prefix, results, returnTypeStr);
} 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, returnTypeStr, path);
} 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(returnType, prefix + "/" + path, new HashMap<String, Type>(parametersMap)));
}
} else {
if (m.getName().equals(ADD)) {
handleAdd(prefix, results, m);
}
}
}
}
private void handleAction(String prefix, Collection<DetailedLink> results, String returnValueStr, String path) {
results.add(new RsdlBuilder.LinkBuilder().url(prefix + "/" + path).rel(path).requestParameter(ACTION).responseType(returnValueStr).httpMethod(HttpMethod.POST).build());
}
private void handleDelete(String prefix, Collection<DetailedLink> results, Method m) {
if (m.getParameterTypes().length>1) {
Class<?>[] parameterTypes = m.getParameterTypes();
Annotation[][] parameterAnnotations = m.getParameterAnnotations();
for (int i=0; i<parameterTypes.length; i++) {
//ignore the id parameter (string), that's annotated with @PathParam
if (!( parameterTypes[i].equals(String.class) && (!(parameterAnnotations[i].length==0)))) {
results.add(new RsdlBuilder.LinkBuilder().url(prefix + "/{" + getSingleForm(prefix) + ":id}").rel(DELETE).requestParameter(parameterTypes[i].getSimpleName()).httpMethod(HttpMethod.DELETE).build());
return; //we can break, because we excpect only one parameter.
}
}
} else {
results.add(new RsdlBuilder.LinkBuilder().url(prefix + "/{" + getSingleForm(prefix) + ":id}").rel(DELETE).httpMethod(HttpMethod.DELETE).build());
}
}
private void handlePut(String prefix, Collection<DetailedLink> results, String returnValueStr) {
results.add(new RsdlBuilder.LinkBuilder().url(prefix).rel(UPDATE).requestParameter(returnValueStr).responseType(returnValueStr).httpMethod(HttpMethod.PUT).build());
}
private void handleGet(String prefix, Collection<DetailedLink> results, String returnValueStr) {
DetailedLink link = new RsdlBuilder.LinkBuilder().url(prefix).rel(GET).responseType(returnValueStr).httpMethod(HttpMethod.GET).build();
results.add(link);
}
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.getRequest() != null) {
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) {
param.setValue(value.toString());
}
ps.getParameters().add(param);
}
link.getRequest().getUrl().getParametersSets().add(ps);
}
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) {
header.setValue(value.toString());
}
link.getRequest().getHeaders().getHeaders().add(header);
}
}
if (action.getRequest().getBody() != null) {
if (action.getRequest().getBody().getSignatures() != null) {
for (Signature signature : action.getRequest().getBody().getSignatures()) {
ParametersSet ps = new ParametersSet();
for (Entry<Object, Object> mandatoryKeyValuePair : signature.getMandatoryArguments().entrySet()) {
Parameter mandatory_param = new Parameter();
mandatory_param.setName(mandatoryKeyValuePair.getKey().toString());
mandatory_param.setType(mandatoryKeyValuePair.getValue().toString());
mandatory_param.setMandatory(true);
ps.getParameters().add(mandatory_param);
}
for (Entry<Object, Object> optionalKeyValuePair : signature.getOptionalArguments().entrySet()) {
Parameter optional_param = new Parameter();
optional_param.setName(optionalKeyValuePair.getKey().toString());
optional_param.setType(optionalKeyValuePair.getValue().toString());
optional_param.setMandatory(false);
ps.getParameters().add(optional_param);
}
link.getRequest().getBody().getParametersSets().add(ps);
}
}
}
}
}
return link;
}
private void handleAdd(String prefix, Collection<DetailedLink> results, Method m) {
Class<?>[] parameterTypes = m.getParameterTypes();
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";
}
}
return s;
}
/**
* get the class name, without package prefix
* @param returnValue
* @return
*/
private String getReturnTypeStr(Class<?> returnValue) {
int lastIndexOf = returnValue.getSimpleName().lastIndexOf(".");
String entityType = lastIndexOf==-1 ? returnValue.getSimpleName() : returnValue.getSimpleName().substring(lastIndexOf);
return entityType;
}
private Class<?> findReturnType(Method m, 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<TypeVariable<?>, Type> map = new HashMap<TypeVariable<?>, Type>();
for (int i=0; i<p.getActualTypeArguments().length; i++) {
if (!map.containsKey(clazz.getTypeParameters()[i])) {
map.put(clazz.getTypeParameters()[i], p.getActualTypeArguments()[i]);
}
}
if (map.containsKey(m.getGenericReturnType())) {
String type = map.get(m.getGenericReturnType()).toString();
try {
Class<?> returnClass = Class.forName(type.substring(type.lastIndexOf(' ')+1));
return returnClass;
} catch (ClassNotFoundException e) {
break;
}
}
}
}
if (parametersMap.containsKey(m.getGenericReturnType().toString())) {
try {
Type type = parametersMap.get(m.getGenericReturnType().toString());
Class<?> returnClass = Class.forName(type.toString().substring(type.toString().indexOf(' ') +1));
return returnClass;
} catch (ClassNotFoundException e) {
return m.getReturnType();
}
} else {
return m.getReturnType();
}
}
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;
int endPos = prefix.endsWith("s") ? prefix.length() -1 : prefix.length();
return prefix.substring(startIndex, endPos);
}
}