/* * #%L * Wisdom-Framework * %% * Copyright (C) 2015 Wisdom Framework * %% * 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. * #L% */ package org.wisdom.raml.visitor; import com.google.common.base.Strings; import org.apache.commons.lang.StringUtils; import org.raml.model.*; import org.raml.model.parameter.AbstractParam; import org.raml.model.parameter.FormParameter; import org.raml.model.parameter.QueryParameter; import org.raml.model.parameter.UriParameter; import org.wisdom.source.ast.model.ControllerModel; import org.wisdom.source.ast.model.ControllerRouteModel; import org.wisdom.source.ast.model.RouteParamModel; import org.wisdom.source.ast.visitor.Visitor; import java.math.BigDecimal; import java.util.*; import static java.util.Collections.singletonList; /** * <p> * {@link ControllerModel} visitor implementation. It populates a given {@link Raml} model thanks to the * wisdom {@link ControllerModel}. * </p> * * @author barjo */ public class RamlControllerVisitor implements Visitor<ControllerModel<Raml>, Raml> { /** * Visit the Wisdom Controller source model in order to populate the raml model. * * @param element The wisdom controller model (we visit it). * @param raml The raml model (we construct it). */ @Override public void visit(ControllerModel element, Raml raml) { raml.setTitle(element.getName()); if (element.getDescription() != null && !element.getDescription().isEmpty()) { DocumentationItem doc = new DocumentationItem(); doc.setContent(element.getDescription()); doc.setTitle("Description"); raml.setDocumentation(singletonList(doc)); } if (element.getVersion() != null && !element.getVersion().isEmpty()) { raml.setVersion(element.getVersion()); } //noinspection unchecked navigateTheRoutes(element.getRoutes(), null, raml); } /** * Navigate through the Controller routes, and create {@link org.raml.model.Resource} from them. * If the <code>parent</code> is not null, then the created route will be added has children of the parent, otherwise * a new Resource is created and will be added directly to the <code>raml</code> model. * * @param routes The @{link ControllerRoute} * @param parent The parent {@link Resource} * @param raml The {@link Raml} model */ private void navigateTheRoutes(NavigableMap<String, Collection<ControllerRouteModel<Raml>>> routes, Resource parent, Raml raml) { //nothing to see here if (routes == null || routes.isEmpty()) { return; } String headUri = routes.firstKey(); System.out.println(routes); System.out.println("Parent " + parent); Collection<ControllerRouteModel<Raml>> siblings = routes.get(headUri); String relativeUri; Resource res = new Resource(); if (parent != null) { res.setParentResource(parent); res.setParentUri(parent.getUri()); //Get the relative part of the url relativeUri = normalizeActionPath(parent, headUri); res.setRelativeUri(relativeUri); parent.getResources().put(res.getRelativeUri(), res); } else { // We don't have a parent, check whether we should create one. if (headUri.endsWith("/")) { // We have to create a 'fake' parent when we have such kind of url: /foo/ // We create a parent /foo and a sub-resource /, this is because /foo and /foo/ are different // Create a parent - this parent doest not have any action attached. String parentUri = normalizeParentPath(headUri); // However we do have a tricky case here, if parentURi == "/", we are the parent. if (! parentUri.equals("/")) { parent = new Resource(); parent.setParentUri(""); parent.setRelativeUri(parentUri); raml.getResources().put(parentUri, parent); // Now manage the current resource, it's uri is necessarily / relativeUri = "/"; res.setParentUri(parent.getUri()); res.setRelativeUri(relativeUri); parent.getResources().put(relativeUri, res); } else { // We are the root. res.setParentUri(""); relativeUri = normalizeParentPath(headUri); res.setRelativeUri(relativeUri); raml.getResources().put(res.getRelativeUri(), res); } } else { // No parent res.setParentUri(""); relativeUri = normalizeParentPath(headUri); res.setRelativeUri(relativeUri); raml.getResources().put(res.getRelativeUri(), res); } } //Add the action from the brother routes for (ControllerRouteModel<Raml> bro : siblings) { addActionFromRouteElem(bro, res); } //visit the children route NavigableMap<String, Collection<ControllerRouteModel<Raml>>> child = routes.tailMap(headUri, false); //no more route element if (child.isEmpty()) { return; } final String next = child.firstKey(); final Resource maybeParent = findParent(next, raml); navigateTheRoutes(child, maybeParent, raml); } private Resource findParent(String next, Raml raml) { Resource parent = null; // We iterate until the end because resources are sorted by design. The last matching resources has the // longest common prefix. for (Resource resource : traverse(raml)) { if (next.startsWith(resource.getUri() + "/")) { parent = resource; } } return parent; } private Collection<Resource> traverse(Raml raml) { List<Resource> resources = new ArrayList<>(); for (Resource resource : raml.getResources().values()) { resources.add(resource); traverse(resource, resources); } return resources; } private void traverse(Resource resource, List<Resource> resources) { for (Resource res : resource.getResources().values()) { resources.add(res); traverse(res, resources); } } /** * A method normalizing "action" path. In RAML action path must always starts with a "/". * * @param parent the parent resource * @param uri the path to normalize * @return the normalized path */ private String normalizeActionPath(Resource parent, String uri) { String relativeUri = extractRelativeUrl(uri, parent.getUri()); if (!relativeUri.startsWith("/")) { relativeUri = "/" + relativeUri; } return relativeUri; } /** * A method normalizing "resource" path. In RAML resource path must neither be empty ("/" is used in this case), * not ends with "/" (as all uri must start with "/"). * * @param uri the uri to normalized * @return the normalized path */ private String normalizeParentPath(String uri) { String relativeUri = extractRelativeUrl(uri, null); if (relativeUri.endsWith("/") && relativeUri.length() != 1) { relativeUri = StringUtils.removeEndIgnoreCase(relativeUri, "/"); } return relativeUri; } /** * Get the relative route uri from its resURI/fullUri and parentUri. * * @param resURI the route full uri. * @param parentUri the route parent uri. * @return The route relative uri. */ private static String extractRelativeUrl(String resURI, String parentUri) { if (Strings.isNullOrEmpty(parentUri)) { if (resURI.isEmpty()) { return "/"; } return resURI; } String url; //Get the relative part of the url String root = parentUri; if (!root.endsWith("/")) { root += "/"; } if (!resURI.startsWith(root)) { url = resURI; } else { url = resURI.substring(parentUri.length(), resURI.length()); } if (url.isEmpty()) { return "/"; } return url; } /** * Add the body specification to the given action. * <p> * <p> * Body can contain one example for each content-type supported. The example must be define in the same order as * the content-type. * <p> * </p> * * @param elem The ControllerRouteModel that contains the body specification. * @param action The Action on which to add the body specification. */ private void addBodyToAction(ControllerRouteModel<Raml> elem, Action action) { action.setBody(new LinkedHashMap<String, MimeType>(elem.getBodyMimes().size())); //the samples must be define in the same order as the accept! Iterator<String> bodySamples = elem.getBodySamples().iterator(); for (String mime : elem.getBodyMimes()) { MimeType mimeType = new MimeType(mime); if (bodySamples.hasNext()) { mimeType.setExample(bodySamples.next()); } action.getBody().put(mime, mimeType); } } /** * Add the response specification to the given action. * * @param elem The ControllerRouteModel that contains the response specification. * @param action The Action on which to add the body specification. */ private void addResponsesToAction(ControllerRouteModel<Raml> elem, Action action) { for (String mime : elem.getResponseMimes()) { Response resp = new Response(); resp.setBody(Collections.singletonMap(mime, new MimeType(mime))); action.getResponses().put("200", resp); //TODO enhance with sample } } /** * Set the resource action from the wisdom route element. * * @param elem The wisdom route element that we are visiting * @param resource The raml resource corresponding to the route element */ private void addActionFromRouteElem(ControllerRouteModel<Raml> elem, Resource resource) { Action action = new Action(); action.setType(ActionType.valueOf(elem.getHttpMethod().name())); action.setDescription(elem.getDescription()); //handle body addBodyToAction(elem, action); //Handle responses addResponsesToAction(elem, action); //handle all route params for (RouteParamModel<Raml> param : elem.getParams()) { AbstractParam ap = null; //the param to add //Fill the param info depending on its type switch (param.getParamType()) { case FORM: MimeType formMime = action.getBody().get("application/x-www-form-urlencoded"); if (formMime == null) { //create default form mimeType formMime = new MimeType("application/x-www-form-urlencoded"); } if (formMime.getFormParameters() == null) { //why raml, why you ain't init that formMime.setFormParameters(new LinkedHashMap<String, List<FormParameter>>(2)); } ap = new FormParameter(); formMime.getFormParameters().put(param.getName(), singletonList((FormParameter) ap)); break; case PARAM: case PATH_PARAM: if (!ancestorOrIHasParam(resource, param.getName())) { ap = new UriParameter(); resource.getUriParameters().put(param.getName(), (UriParameter) ap); } //we do nothing if the param has already been define in the resouce or its ancestor. break; case QUERY: ap = new QueryParameter(); action.getQueryParameters().put(param.getName(), (QueryParameter) ap); break; case BODY: default: break; //body is handled at the method level. } if (ap == null) { //no param has been created, we skip. continue; } //Set param type ParamType type = typeConverter(param.getValueType()); if (type != null) { ap.setType(type); } //set required, usually thanks to the notnull constraints annotation. if(param.isMandatory()){ ap.setRequired(true); } else { ap.setRequired(false); } //set minimum if specified if(param.getMin()!=null){ ap.setMinimum(BigDecimal.valueOf(param.getMin())); //TODO warn if type is not number/integer } //set maximum if specified if(param.getMax()!=null){ ap.setMinimum(BigDecimal.valueOf(param.getMax())); //TODO warn if type is not number/integer } //set default value if (param.getDefaultValue() != null) { ap.setRequired(false); ap.setDefaultValue(param.getDefaultValue()); } } resource.getActions().put(action.getType(), action); } /** * Check if the given resource or its ancestor have the uri param of given name. * * @param resource The resource on which to check. * @param uriParamName Name of the uri Param we are looking for. * @return <code>true</code> if this or its ancestor resource have the param of given name already define. */ private static Boolean ancestorOrIHasParam(final Resource resource, String uriParamName) { Resource ancestor = resource; while (ancestor != null) { if (ancestor.getUriParameters().containsKey(uriParamName)) { return true; } ancestor = ancestor.getParentResource(); } return false; } /** * Convert a string version of the type name into a ParamType enum or null if nothing correspond. * * @param typeName The type name. * @return the {@link ParamType} corresponding to the given type name. */ private static ParamType typeConverter(String typeName) { if (typeName == null || typeName.isEmpty()) { return null; } if ("Number".equals(typeName) || "Long".equalsIgnoreCase(typeName) || "Integer".equals(typeName) || "int".equals(typeName)) { return ParamType.NUMBER; } if ("Boolean".equalsIgnoreCase(typeName)) { return ParamType.BOOLEAN; } if ("String".equals(typeName)) { return ParamType.STRING; } if ("Date".equals(typeName)) { return ParamType.DATE; } if ("File".equals(typeName)) { return ParamType.FILE; } return null; } }