org.hl7.fhir.instance.conf.ServerConformanceProvider.java Source code

Java tutorial

Introduction

Here is the source code for org.hl7.fhir.instance.conf.ServerConformanceProvider.java

Source

package org.hl7.fhir.instance.conf;

/*
 * #%L
 * HAPI FHIR Structures - DSTU2 (FHIR v0.5.0)
 * %%
 * Copyright (C) 2014 - 2015 University Health Network
 * %%
 * 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%
 */
import static org.apache.commons.lang3.StringUtils.isNotBlank;

import java.io.IOException;
import java.io.InputStream;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.jar.Manifest;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.Conformance;
import org.hl7.fhir.instance.model.Conformance.ConditionalDeleteStatus;
import org.hl7.fhir.instance.model.Conformance.ConformanceRestComponent;
import org.hl7.fhir.instance.model.Conformance.ConformanceRestResourceComponent;
import org.hl7.fhir.instance.model.Conformance.ConformanceRestResourceSearchParamComponent;
import org.hl7.fhir.instance.model.Conformance.ConformanceStatementKind;
import org.hl7.fhir.instance.model.Conformance.ResourceInteractionComponent;
import org.hl7.fhir.instance.model.Conformance.RestfulConformanceMode;
import org.hl7.fhir.instance.model.Conformance.SystemRestfulInteraction;
import org.hl7.fhir.instance.model.Conformance.TypeRestfulInteraction;
import org.hl7.fhir.instance.model.Conformance.UnknownContentCode;
import org.hl7.fhir.instance.model.Enumerations.ConformanceResourceStatus;
import org.hl7.fhir.instance.model.Enumerations.ResourceType;
import org.hl7.fhir.instance.model.IdType;
import org.hl7.fhir.instance.model.OperationDefinition;
import org.hl7.fhir.instance.model.OperationDefinition.OperationDefinitionParameterComponent;
import org.hl7.fhir.instance.model.OperationDefinition.OperationParameterUse;
import org.hl7.fhir.instance.model.api.IBaseResource;

import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.Initialize;
import ca.uhn.fhir.rest.annotation.Metadata;
import ca.uhn.fhir.rest.annotation.Read;
import ca.uhn.fhir.rest.method.BaseMethodBinding;
import ca.uhn.fhir.rest.method.DynamicSearchMethodBinding;
import ca.uhn.fhir.rest.method.IParameter;
import ca.uhn.fhir.rest.method.OperationMethodBinding;
import ca.uhn.fhir.rest.method.OperationMethodBinding.ReturnType;
import ca.uhn.fhir.rest.method.OperationParameter;
import ca.uhn.fhir.rest.method.SearchMethodBinding;
import ca.uhn.fhir.rest.method.SearchParameter;
import ca.uhn.fhir.rest.server.Constants;
import ca.uhn.fhir.rest.server.IServerConformanceProvider;
import ca.uhn.fhir.rest.server.ResourceBinding;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;

/**
 * Server FHIR Provider which serves the conformance statement for a RESTful
 * server implementation
 * 
 * <p>
 * Note: This class is safe to extend, but it is important to note that the same
 * instance of {@link Conformance} is always returned unless
 * {@link #setCache(boolean)} is called with a value of <code>false</code>. This
 * means that if you are adding anything to the returned conformance instance on
 * each call you should call <code>setCache(false)</code> in your provider
 * constructor.
 * </p>
 */
public class ServerConformanceProvider implements IServerConformanceProvider<Conformance> {

    private boolean myCache = true;
    private volatile Conformance myConformance;
    private IdentityHashMap<OperationMethodBinding, String> myOperationBindingToName;
    private HashMap<String, List<OperationMethodBinding>> myOperationNameToBindings;
    private String myPublisher = "Not provided";
    private RestfulServer myRestfulServer;

    public ServerConformanceProvider(RestfulServer theRestfulServer) {
        myRestfulServer = theRestfulServer;
    }

    /*
     * Add a no-arg constructor and seetter so that the
     * ServerConfirmanceProvider can be Spring-wired with
     * the RestfulService avoiding the potential reference
     * cycle that would happen.
     */
    public ServerConformanceProvider() {
        super();
    }

    public void setRestfulServer(RestfulServer theRestfulServer) {
        myRestfulServer = theRestfulServer;
    }

    private void checkBindingForSystemOps(ConformanceRestComponent rest, Set<SystemRestfulInteraction> systemOps,
            BaseMethodBinding<?> nextMethodBinding) {
        if (nextMethodBinding.getRestOperationType() != null) {
            String sysOpCode = nextMethodBinding.getRestOperationType().getCode();
            if (sysOpCode != null) {
                SystemRestfulInteraction sysOp;
                try {
                    sysOp = SystemRestfulInteraction.fromCode(sysOpCode);
                } catch (Exception e) {
                    sysOp = null;
                }
                if (sysOp == null) {
                    return;
                }
                if (systemOps.contains(sysOp) == false) {
                    systemOps.add(sysOp);
                    rest.addInteraction().setCode(sysOp);
                }
            }
        }
    }

    private Map<String, List<BaseMethodBinding<?>>> collectMethodBindings() {
        Map<String, List<BaseMethodBinding<?>>> resourceToMethods = new TreeMap<String, List<BaseMethodBinding<?>>>();
        for (ResourceBinding next : myRestfulServer.getResourceBindings()) {
            String resourceName = next.getResourceName();
            for (BaseMethodBinding<?> nextMethodBinding : next.getMethodBindings()) {
                if (resourceToMethods.containsKey(resourceName) == false) {
                    resourceToMethods.put(resourceName, new ArrayList<BaseMethodBinding<?>>());
                }
                resourceToMethods.get(resourceName).add(nextMethodBinding);
            }
        }
        for (BaseMethodBinding<?> nextMethodBinding : myRestfulServer.getServerBindings()) {
            String resourceName = "";
            if (resourceToMethods.containsKey(resourceName) == false) {
                resourceToMethods.put(resourceName, new ArrayList<BaseMethodBinding<?>>());
            }
            resourceToMethods.get(resourceName).add(nextMethodBinding);
        }
        return resourceToMethods;
    }

    private String createOperationName(OperationMethodBinding theMethodBinding) {
        return theMethodBinding.getName().substring(1);
    }

    /**
     * Gets the value of the "publisher" that will be placed in the generated
     * conformance statement. As this is a mandatory element, the value should not
     * be null (although this is not enforced). The value defaults to
     * "Not provided" but may be set to null, which will cause this element to be
     * omitted.
     */
    public String getPublisher() {
        return myPublisher;
    }

    @Override
    @Metadata
    public Conformance getServerConformance(HttpServletRequest theRequest) {
        if (myConformance != null && myCache) {
            return myConformance;
        }

        Conformance retVal = new Conformance();

        retVal.setPublisher(myPublisher);
        retVal.setDate(conformanceDate());
        retVal.setFhirVersion("1.0.2"); // TODO: pull from model
        retVal.setAcceptUnknown(UnknownContentCode.EXTENSIONS); // TODO: make this configurable - this is a fairly big effort since the parser
        // needs to be modified to actually allow it

        retVal.getImplementation().setDescription(myRestfulServer.getImplementationDescription());
        retVal.setKind(ConformanceStatementKind.INSTANCE);
        retVal.getSoftware().setName(myRestfulServer.getServerName());
        retVal.getSoftware().setVersion(myRestfulServer.getServerVersion());
        retVal.addFormat(Constants.CT_FHIR_XML);
        retVal.addFormat(Constants.CT_FHIR_JSON);

        ConformanceRestComponent rest = retVal.addRest();
        rest.setMode(RestfulConformanceMode.SERVER);

        Set<SystemRestfulInteraction> systemOps = new HashSet<SystemRestfulInteraction>();
        Set<String> operationNames = new HashSet<String>();

        Map<String, List<BaseMethodBinding<?>>> resourceToMethods = collectMethodBindings();
        for (Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) {

            if (nextEntry.getKey().isEmpty() == false) {
                Set<TypeRestfulInteraction> resourceOps = new HashSet<TypeRestfulInteraction>();
                ConformanceRestResourceComponent resource = rest.addResource();
                String resourceName = nextEntry.getKey();
                RuntimeResourceDefinition def = myRestfulServer.getFhirContext()
                        .getResourceDefinition(resourceName);
                resource.getTypeElement().setValue(def.getName());
                resource.getProfile().setReference(
                        (def.getResourceProfile(myRestfulServer.getServerBaseForRequest(theRequest))));

                TreeSet<String> includes = new TreeSet<String>();

                // Map<String, Conformance.RestResourceSearchParam> nameToSearchParam =
                // new HashMap<String,
                // Conformance.RestResourceSearchParam>();
                for (BaseMethodBinding<?> nextMethodBinding : nextEntry.getValue()) {
                    if (nextMethodBinding.getRestOperationType() != null) {
                        String resOpCode = nextMethodBinding.getRestOperationType().getCode();
                        if (resOpCode != null) {
                            TypeRestfulInteraction resOp;
                            try {
                                resOp = TypeRestfulInteraction.fromCode(resOpCode);
                            } catch (Exception e) {
                                resOp = null;
                            }
                            if (resOp != null) {
                                if (resourceOps.contains(resOp) == false) {
                                    resourceOps.add(resOp);
                                    resource.addInteraction().setCode(resOp);
                                }
                                if ("vread".equals(resOpCode)) {
                                    // vread implies read
                                    resOp = TypeRestfulInteraction.READ;
                                    if (resourceOps.contains(resOp) == false) {
                                        resourceOps.add(resOp);
                                        resource.addInteraction().setCode(resOp);
                                    }
                                }

                                if (nextMethodBinding.isSupportsConditional()) {
                                    switch (resOp) {
                                    case CREATE:
                                        resource.setConditionalCreate(true);
                                        break;
                                    case DELETE:
                                        resource.setConditionalDelete(ConditionalDeleteStatus.SINGLE);
                                        break;
                                    case UPDATE:
                                        resource.setConditionalUpdate(true);
                                        break;
                                    default:
                                        break;
                                    }
                                }
                            }
                        }
                    }

                    checkBindingForSystemOps(rest, systemOps, nextMethodBinding);

                    if (nextMethodBinding instanceof SearchMethodBinding) {
                        handleSearchMethodBinding(rest, resource, resourceName, def, includes,
                                (SearchMethodBinding) nextMethodBinding);
                    } else if (nextMethodBinding instanceof DynamicSearchMethodBinding) {
                        handleDynamicSearchMethodBinding(resource, def, includes,
                                (DynamicSearchMethodBinding) nextMethodBinding);
                    } else if (nextMethodBinding instanceof OperationMethodBinding) {
                        OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
                        String opName = myOperationBindingToName.get(methodBinding);
                        if (operationNames.add(opName)) {
                            // Only add each operation (by name) once
                            rest.addOperation().setName(methodBinding.getName()).getDefinition()
                                    .setReference("OperationDefinition/" + opName);
                        }
                    }

                    Collections.sort(resource.getInteraction(), new Comparator<ResourceInteractionComponent>() {
                        @Override
                        public int compare(ResourceInteractionComponent theO1, ResourceInteractionComponent theO2) {
                            TypeRestfulInteraction o1 = theO1.getCode();
                            TypeRestfulInteraction o2 = theO2.getCode();
                            if (o1 == null && o2 == null) {
                                return 0;
                            }
                            if (o1 == null) {
                                return 1;
                            }
                            if (o2 == null) {
                                return -1;
                            }
                            return o1.ordinal() - o2.ordinal();
                        }
                    });

                }

                for (String nextInclude : includes) {
                    resource.addSearchInclude(nextInclude);
                }
            } else {
                for (BaseMethodBinding<?> nextMethodBinding : nextEntry.getValue()) {
                    checkBindingForSystemOps(rest, systemOps, nextMethodBinding);
                    if (nextMethodBinding instanceof OperationMethodBinding) {
                        OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
                        String opName = myOperationBindingToName.get(methodBinding);
                        if (operationNames.add(opName)) {
                            rest.addOperation().setName(methodBinding.getName()).getDefinition()
                                    .setReference("OperationDefinition/" + opName);
                        }
                    }
                }
            }
        }

        myConformance = retVal;
        return retVal;
    }

    private Date conformanceDate() {
        String buildDate = getBuildDateFromManifest();
        if (buildDate != null) {
            DateFormat dateFormat = new SimpleDateFormat();
            try {
                return dateFormat.parse(buildDate);
            } catch (ParseException e) {
                // fall through
            }
        }
        return new Date();
    }

    private String getBuildDateFromManifest() {
        if (myRestfulServer != null && myRestfulServer.getServletContext() != null) {
            InputStream inputStream = myRestfulServer.getServletContext()
                    .getResourceAsStream("/META-INF/MANIFEST.MF");
            if (inputStream != null) {
                try {
                    Manifest manifest = new Manifest(inputStream);
                    return manifest.getMainAttributes().getValue("Build-Time");
                } catch (IOException e) {
                    // fall through
                }
            }
        }
        return null;
    }

    private void handleDynamicSearchMethodBinding(ConformanceRestResourceComponent resource,
            RuntimeResourceDefinition def, TreeSet<String> includes,
            DynamicSearchMethodBinding searchMethodBinding) {
        includes.addAll(searchMethodBinding.getIncludes());

        List<RuntimeSearchParam> searchParameters = new ArrayList<RuntimeSearchParam>();
        searchParameters.addAll(searchMethodBinding.getSearchParams());
        sortRuntimeSearchParameters(searchParameters);

        if (!searchParameters.isEmpty()) {

            for (RuntimeSearchParam nextParameter : searchParameters) {

                String nextParamName = nextParameter.getName();

                // String chain = null;
                String nextParamUnchainedName = nextParamName;
                if (nextParamName.contains(".")) {
                    // chain = nextParamName.substring(nextParamName.indexOf('.') + 1);
                    nextParamUnchainedName = nextParamName.substring(0, nextParamName.indexOf('.'));
                }

                String nextParamDescription = nextParameter.getDescription();

                /*
                 * If the parameter has no description, default to the one from the
                 * resource
                 */
                if (StringUtils.isBlank(nextParamDescription)) {
                    RuntimeSearchParam paramDef = def.getSearchParam(nextParamUnchainedName);
                    if (paramDef != null) {
                        nextParamDescription = paramDef.getDescription();
                    }
                }

                ConformanceRestResourceSearchParamComponent param = resource.addSearchParam();

                param.setName(nextParamName);
                // if (StringUtils.isNotBlank(chain)) {
                // param.addChain(chain);
                // }
                param.setDocumentation(nextParamDescription);
                // param.setType(nextParameter.getParamType());
            }
        }
    }

    private void handleSearchMethodBinding(ConformanceRestComponent rest, ConformanceRestResourceComponent resource,
            String resourceName, RuntimeResourceDefinition def, TreeSet<String> includes,
            SearchMethodBinding searchMethodBinding) {
        includes.addAll(searchMethodBinding.getIncludes());

        List<IParameter> params = searchMethodBinding.getParameters();
        List<SearchParameter> searchParameters = new ArrayList<SearchParameter>();
        for (IParameter nextParameter : params) {
            if ((nextParameter instanceof SearchParameter)) {
                searchParameters.add((SearchParameter) nextParameter);
            }
        }
        sortSearchParameters(searchParameters);
        if (!searchParameters.isEmpty()) {
            // boolean allOptional = searchParameters.get(0).isRequired() == false;
            //
            // OperationDefinition query = null;
            // if (!allOptional) {
            // RestOperation operation = rest.addOperation();
            // query = new OperationDefinition();
            // operation.setDefinition(new ResourceReferenceDt(query));
            // query.getDescriptionElement().setValue(searchMethodBinding.getDescription());
            // query.addUndeclaredExtension(false,
            // ExtensionConstants.QUERY_RETURN_TYPE, new CodeDt(resourceName));
            // for (String nextInclude : searchMethodBinding.getIncludes()) {
            // query.addUndeclaredExtension(false,
            // ExtensionConstants.QUERY_ALLOWED_INCLUDE, new StringDt(nextInclude));
            // }
            // }

            for (SearchParameter nextParameter : searchParameters) {

                String nextParamName = nextParameter.getName();

                String chain = null;
                String nextParamUnchainedName = nextParamName;
                if (nextParamName.contains(".")) {
                    chain = nextParamName.substring(nextParamName.indexOf('.') + 1);
                    nextParamUnchainedName = nextParamName.substring(0, nextParamName.indexOf('.'));
                }

                String nextParamDescription = nextParameter.getDescription();

                /*
                 * If the parameter has no description, default to the one from the
                 * resource
                 */
                if (StringUtils.isBlank(nextParamDescription)) {
                    RuntimeSearchParam paramDef = def.getSearchParam(nextParamUnchainedName);
                    if (paramDef != null) {
                        nextParamDescription = paramDef.getDescription();
                    }
                }

                ConformanceRestResourceSearchParamComponent param = resource.addSearchParam();
                param.setName(nextParamUnchainedName);
                if (StringUtils.isNotBlank(chain)) {
                    param.addChain(chain);
                }
                param.setDocumentation(nextParamDescription);
                if (nextParameter.getParamType() != null) {
                    param.getTypeElement().setValueAsString(nextParameter.getParamType().getCode());
                }
                for (Class<? extends IBaseResource> nextTarget : nextParameter.getDeclaredTypes()) {
                    RuntimeResourceDefinition targetDef = myRestfulServer.getFhirContext()
                            .getResourceDefinition(nextTarget);
                    if (targetDef != null) {
                        ResourceType code;
                        try {
                            code = ResourceType.fromCode(targetDef.getName());
                        } catch (Exception e) {
                            code = null;
                        }
                        if (code != null) {
                            param.addTarget(code.toCode());
                        }
                    }
                }
            }
        }
    }

    @Initialize
    public void initializeOperations() {
        myOperationBindingToName = new IdentityHashMap<OperationMethodBinding, String>();
        myOperationNameToBindings = new HashMap<String, List<OperationMethodBinding>>();

        Map<String, List<BaseMethodBinding<?>>> resourceToMethods = collectMethodBindings();
        for (Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) {
            List<BaseMethodBinding<?>> nextMethodBindings = nextEntry.getValue();
            for (BaseMethodBinding<?> nextMethodBinding : nextMethodBindings) {
                if (nextMethodBinding instanceof OperationMethodBinding) {
                    OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding;
                    if (myOperationBindingToName.containsKey(methodBinding)) {
                        continue;
                    }

                    String name = createOperationName(methodBinding);
                    myOperationBindingToName.put(methodBinding, name);
                    if (myOperationNameToBindings.containsKey(name) == false) {
                        myOperationNameToBindings.put(name, new ArrayList<OperationMethodBinding>());
                    }
                    myOperationNameToBindings.get(name).add(methodBinding);
                }
            }
        }
    }

    @Read(type = OperationDefinition.class)
    public OperationDefinition readOperationDefinition(@IdParam IdType theId) {
        if (theId == null || theId.hasIdPart() == false) {
            throw new ResourceNotFoundException(theId);
        }
        List<OperationMethodBinding> sharedDescriptions = myOperationNameToBindings.get(theId.getIdPart());
        if (sharedDescriptions == null || sharedDescriptions.isEmpty()) {
            throw new ResourceNotFoundException(theId);
        }

        OperationDefinition op = new OperationDefinition();
        op.setStatus(ConformanceResourceStatus.ACTIVE);
        op.setIdempotent(true);

        Set<String> inParams = new HashSet<String>();
        Set<String> outParams = new HashSet<String>();

        for (OperationMethodBinding sharedDescription : sharedDescriptions) {
            if (isNotBlank(sharedDescription.getDescription())) {
                op.setDescription(sharedDescription.getDescription());
            }
            if (!sharedDescription.isIdempotent()) {
                op.setIdempotent(sharedDescription.isIdempotent());
            }
            op.setCode(sharedDescription.getName());
            if (sharedDescription.isCanOperateAtInstanceLevel()) {
                op.setInstance(sharedDescription.isCanOperateAtInstanceLevel());
            }
            if (sharedDescription.isCanOperateAtServerLevel()) {
                op.setSystem(sharedDescription.isCanOperateAtServerLevel());
            }
            if (isNotBlank(sharedDescription.getResourceName())) {
                op.addTypeElement().setValue(sharedDescription.getResourceName());
            }

            for (IParameter nextParamUntyped : sharedDescription.getParameters()) {
                if (nextParamUntyped instanceof OperationParameter) {
                    OperationParameter nextParam = (OperationParameter) nextParamUntyped;
                    OperationDefinitionParameterComponent param = op.addParameter();
                    if (!inParams.add(nextParam.getName())) {
                        continue;
                    }
                    param.setUse(OperationParameterUse.IN);
                    if (nextParam.getParamType() != null) {
                        param.setType(nextParam.getParamType());
                    }
                    param.setMin(nextParam.getMin());
                    param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()));
                    param.setName(nextParam.getName());
                }
            }

            for (ReturnType nextParam : sharedDescription.getReturnParams()) {
                if (!outParams.add(nextParam.getName())) {
                    continue;
                }
                OperationDefinitionParameterComponent param = op.addParameter();
                param.setUse(OperationParameterUse.OUT);
                if (nextParam.getType() != null) {
                    param.setType(nextParam.getType());
                }
                param.setMin(nextParam.getMin());
                param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()));
                param.setName(nextParam.getName());
            }
        }

        return op;
    }

    /**
     * Sets the cache property (default is true). If set to true, the same
     * response will be returned for each invocation.
     * <p>
     * See the class documentation for an important note if you are extending this
     * class
     * </p>
     */
    public void setCache(boolean theCache) {
        myCache = theCache;
    }

    /**
     * Sets the value of the "publisher" that will be placed in the generated
     * conformance statement. As this is a mandatory element, the value should not
     * be null (although this is not enforced). The value defaults to
     * "Not provided" but may be set to null, which will cause this element to be
     * omitted.
     */
    public void setPublisher(String thePublisher) {
        myPublisher = thePublisher;
    }

    private void sortRuntimeSearchParameters(List<RuntimeSearchParam> searchParameters) {
        Collections.sort(searchParameters, new Comparator<RuntimeSearchParam>() {
            @Override
            public int compare(RuntimeSearchParam theO1, RuntimeSearchParam theO2) {
                return theO1.getName().compareTo(theO2.getName());
            }
        });
    }

    private void sortSearchParameters(List<SearchParameter> searchParameters) {
        Collections.sort(searchParameters, new Comparator<SearchParameter>() {
            @Override
            public int compare(SearchParameter theO1, SearchParameter theO2) {
                if (theO1.isRequired() == theO2.isRequired()) {
                    return theO1.getName().compareTo(theO2.getName());
                }
                if (theO1.isRequired()) {
                    return -1;
                }
                return 1;
            }
        });
    }
}