package com.temenos.interaction.core.hypermedia; /* * #%L * interaction-core * %% * Copyright (C) 2012 - 2016 Temenos Holdings N.V. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * #L% */ import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilderException; import org.apache.wink.common.internal.MultivaluedMapImpl; import org.odata4j.core.OEntity; import org.odata4j.format.xml.XmlFormatWriter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.temenos.interaction.core.command.InteractionContext; import com.temenos.interaction.core.web.RequestContext; /** * A {@link LinkGenerator} that generates a {@link Collection} of {@link Link} for a {@link Transition} using data from * a resource entity. * */ public class LinkGeneratorImpl implements LinkGenerator { private static final Logger logger = LoggerFactory.getLogger(LinkGeneratorImpl.class); private static final String NEW_REL_SUFFIX = "/new"; private static final String POPULATE_REL_SUFFIX = "/populate"; private static final String AA_POPULATE_REL_SUFFIX = "/aapopulate"; private ResourceStateMachine resourceStateMachine; private Transition transition; private InteractionContext interactionContext; private boolean allQueryParameters; public LinkGeneratorImpl(ResourceStateMachine resourceStateMachine, Transition transition, InteractionContext interactionContext) { this.resourceStateMachine = resourceStateMachine; this.transition = transition; this.interactionContext = interactionContext; } public LinkGeneratorImpl setAllQueryParameters(boolean allQueryParameters) { this.allQueryParameters = allQueryParameters; return this; } @Override public Collection<Link> createLink(MultivaluedMap<String, String> pathParameters, MultivaluedMap<String, String> queryParameters, Object entity) { Collection<Link> eLinks = new ArrayList<Link>(); Map<String, Object> transitionProperties = resourceStateMachine.getTransitionProperties(transition, entity, pathParameters, queryParameters); LinkToFieldAssociation linkToFieldAssociation = new LinkToFieldAssociationImpl(transition, transitionProperties); //Determine if links can be generated for the transition if (linkToFieldAssociation.isTransitionSupported()) { List<LinkProperties> linkPropertiesList = linkToFieldAssociation.getProperties(); //Generate one link for each item in linkPropertiesList and resolve fields using linkProperties for (LinkProperties linkProperties : linkPropertiesList) { eLinks.add(createLink(linkProperties, queryParameters, entity)); } } return eLinks; } private Link createLink(LinkProperties linkProperties, MultivaluedMap<String, String> queryParameters, Object entity) { assert (RequestContext.getRequestContext() != null); try { ResourceState targetState = transition.getTarget(); if (targetState == null) { // a dead link, target could not be found logger.error("Dead link to [" + transition.getId() + "]"); return null; } UriBuilder linkTemplate = UriBuilder.fromUri(RequestContext.getRequestContext().getBasePath()); if (targetState instanceof DynamicResourceState) { return createLinkForDynamicResource(linkTemplate, linkProperties, targetState, entity); } else { return createLinkForResource(linkTemplate, linkProperties, targetState, queryParameters, entity); } } catch (IllegalArgumentException e) { logger.warn("Dead link [" + transition + "]", e); return null; } catch (UriBuilderException e) { logger.error("Dead link [" + transition + "]", e); throw e; } } private void configureLink(UriBuilder linkTemplate, Map<String, Object> transitionProperties, String targetResourcePath) { // Pass uri parameters as query parameters if they are not // replaceable in the path, and replace any token. Map<String, String> uriParameters = transition.getCommand().getUriParameters(); MultivaluedMap<String, String> outQueryParams = new MultivaluedMapImpl<String, String>(); if (interactionContext != null) { MultivaluedMap<String, String> outQueryParamsTemp = interactionContext.getOutQueryParameters(); for (Map.Entry<String, List<String>> param : outQueryParamsTemp.entrySet()) { for(String paramValue: param.getValue()) { try{ outQueryParams.add(param.getKey(), URLEncoder.encode(paramValue, "UTF-8")); }catch(UnsupportedEncodingException uee){ logger.error("ERROR unable to encode " + param.getKey(), uee); } } } } if (uriParameters != null) { for (String key : uriParameters.keySet()) { String value = uriParameters.get(key); if (!targetResourcePath.contains("{" + key + "}")) { String paramValue = HypermediaTemplateHelper.templateReplace(value, transitionProperties); if (paramValue != null) { outQueryParams.putSingle(key, paramValue); } } } } for (Map.Entry<String, List<String>> param : outQueryParams.entrySet()) { for(String paramValue: param.getValue()) { linkTemplate.queryParam(param.getKey(), paramValue); } } } private String getTargetRelValue(ResourceState targetState) { String rel = targetState.getRel(); if (transition.getSource() == targetState) { rel = "self"; } return rel; } private String createLinkForState(ResourceState targetState) { StringBuilder rel = new StringBuilder(XmlFormatWriter.related); rel.append(targetState.getName()); return rel.toString(); } private String createLinkForProfile(Transition transition) { return transition.getLabel() != null && !transition.getLabel().equals("") ? transition.getLabel() : transition.getTarget().getName(); } private void addQueryParams(MultivaluedMap<String, String> queryParameters, boolean allQueryParameters, UriBuilder linkTemplate, String targetResourcePath, Map<String, String> uriParameters) { if (queryParameters != null && allQueryParameters) { for (String param : queryParameters.keySet()) { if (!targetResourcePath.contains("{" + param + "}") && (uriParameters == null || !uriParameters.containsKey(param))) { linkTemplate.queryParam(param, queryParameters.getFirst(param)); } } } } private Link createLinkForDynamicResource(UriBuilder linkTemplate, LinkProperties linkProperties, ResourceState targetState, Object entity) { // We are dealing with a dynamic target // Identify real target state Map<String, Object> linkPropertiesMap = linkProperties.getTransitionProperties(); ResourceStateAndParameters stateAndParams = resourceStateMachine.resolveDynamicState((DynamicResourceState) targetState, linkPropertiesMap, interactionContext); if (stateAndParams == null) { // Bail out as we failed to resolve resource return null; } else { targetState = stateAndParams.getState(); } String targetPath = targetState.getPath(); configureLink(linkTemplate, linkPropertiesMap, targetPath); linkTemplate.path(targetPath); String rel = getTargetRelValue(targetState); String method = transition.getCommand().getMethod(); if (rel.contains(NEW_REL_SUFFIX) || rel.contains(POPULATE_REL_SUFFIX) || rel.contains(AA_POPULATE_REL_SUFFIX) ) { method = "POST"; } if ("item".equals(rel) || "collection".equals(rel)) { rel = createLinkForState(targetState); } Map<String, String> uriParameters = transition.getCommand().getUriParameters(); if (stateAndParams.getParams() != null) { // Add query parameters for (ParameterAndValue paramAndValue : stateAndParams.getParams()) { String param = paramAndValue.getParameter(); String value = paramAndValue.getValue(); if ("id".equalsIgnoreCase(param)) { linkPropertiesMap.put(param, value); if(rel.contains(POPULATE_REL_SUFFIX) && (uriParameters == null || !uriParameters.containsKey(param))) { linkTemplate.queryParam(param, value); } } else if(uriParameters == null || !uriParameters.containsKey(param)) { //Add query param only if it's not already present in the path linkTemplate.queryParam(param, value); } } } // Links in the transition properties are already encoded so // build the href using encoded map. URI href = linkTemplate.buildFromEncodedMap(linkPropertiesMap); Transition resolvedTransition = rebuildTransitionWithResolvedTarget(targetState); return buildLink(resolvedTransition, linkProperties, entity, rel, href, method); } private Link createLinkForResource(UriBuilder linkTemplate, LinkProperties linkProperties, ResourceState targetState, MultivaluedMap<String, String> queryParameters, Object entity) { Map<String, Object> encodedLinkPropertiesMap = new HashMap<String, Object>(); for (String key : linkProperties.getTransitionProperties().keySet()) { Object value = linkProperties.getTransitionProperties().get(key); if (value != null) { try { String encodedValue = URLEncoder.encode(value.toString(), "UTF-8"); encodedLinkPropertiesMap.put(key, encodedValue); } catch (UnsupportedEncodingException e) { logger.error("ERROR unable to encode " + key, e); } } } String targetPath = targetState.getPath(); linkTemplate.path(targetPath); configureLink(linkTemplate, encodedLinkPropertiesMap, targetPath); String rel = getTargetRelValue(targetState); // Pass any query parameters addQueryParams(queryParameters, allQueryParameters, linkTemplate, targetState.getPath(), transition.getCommand().getUriParameters()); // Build href from template URI href; if (entity != null && resourceStateMachine.getTransformer() == null) { logger.debug("Building link with entity (No Transformer) [" + entity + "] [" + transition + "]"); href = linkTemplate.build(entity); } else { // Links in the transition properties are already encoded so // build the href using encoded map. href = linkTemplate.buildFromEncodedMap(encodedLinkPropertiesMap); } return buildLink(transition, linkProperties, entity, rel, href, transition.getCommand().getMethod()); } private Link buildLink(Transition resolvedTransition, LinkProperties linkProperties, Object entity, String rel, URI href, String method) { Link link; if (linkProperties.getTransitionProperties().containsKey("profileOEntity") && "self".equals(rel) && entity instanceof OEntity) { // Create link adding profile to href to be resolved later on AtomXMLProvider link = new Link(resolvedTransition, rel, href.toASCIIString() + "#@" + createLinkForProfile(resolvedTransition), method); } else { // Create link as normal behaviour String fieldLabel = linkProperties.getTargetFieldFullyQualifiedName(); String linkFieldLabel = fieldLabel; if (fieldLabel != null && fieldLabel.contains(".")) { linkFieldLabel = resolvedTransition.getSource().getEntityName() + "_" + fieldLabel; } link = new Link(resolvedTransition, rel, href.toASCIIString(), method, linkFieldLabel); } logger.debug("Created link for transition [" + resolvedTransition + "] [title=" + resolvedTransition.getId() + ", rel=" + rel + ", method=" + method + ", href=" + href.toString() + "(ASCII=" + href.toASCIIString() + ")]"); return link; } private Transition rebuildTransitionWithResolvedTarget(ResourceState resolvedTarget) { Transition updatedtransition = new Transition.Builder() .source(this.transition.getSource()) .target(resolvedTarget) .label(this.transition.getLabel()) .method(this.transition.getCommand().getMethod()) .flags(this.transition.getCommand().getFlags()) .evaluation(this.transition.getCommand().getEvaluation()) .locator(this.transition.getLocator()) .uriParameters(this.transition.getCommand().getUriParameters()) .linkId(this.transition.getLinkId()) .sourceField(this.transition.getSourceField()) .build(); return updatedtransition; } }