All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ibm.fhir.server.operation.spi.AbstractOperation Maven / Gradle / Ivy

There is a newer version: 4.11.1
Show newest version
/*
 * (C) Copyright IBM Corp. 2016, 2021
 *
 * SPDX-License-Identifier: Apache-2.0
 */

package com.ibm.fhir.server.operation.spi;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import com.ibm.fhir.exception.FHIROperationException;
import com.ibm.fhir.model.resource.OperationDefinition;
import com.ibm.fhir.model.resource.OperationOutcome;
import com.ibm.fhir.model.resource.Parameters;
import com.ibm.fhir.model.resource.Parameters.Parameter;
import com.ibm.fhir.model.resource.Resource;
import com.ibm.fhir.model.type.code.IssueType;
import com.ibm.fhir.model.type.code.OperationParameterUse;
import com.ibm.fhir.model.type.code.ResourceType;
import com.ibm.fhir.model.util.FHIRUtil;
import com.ibm.fhir.model.util.ModelSupport;

public abstract class AbstractOperation implements FHIROperation {
    protected final OperationDefinition definition;

    public AbstractOperation() {
        definition = Objects.requireNonNull(buildOperationDefinition(), "definition");
    }

    @Override
    public OperationDefinition getDefinition() {
        return definition;
    }

    @Override
    public String getName() {
        return (definition.getCode() != null) ? definition.getCode().getValue() : null;
    }

    /**
     * Validate the input parameters, invoke the operation, validate the output parameters, and return the result.
     *
     * @throws FHIROperationException
     *     if input or output parameters fail validation or an exception occurs
     */
    @Override
    public final Parameters invoke(
            FHIROperationContext operationContext,
            Class resourceType,
            String logicalId, String versionId,
            Parameters parameters,
            FHIRResourceHelpers resourceHelper) throws FHIROperationException {
        validateOperationContext(operationContext, resourceType, parameters);
        validateInputParameters(operationContext, resourceType, logicalId, versionId, parameters);
        Parameters result = doInvoke(operationContext, resourceType, logicalId, versionId, parameters, resourceHelper);
        validateOutputParameters(result);
        return result;
    }

    protected abstract OperationDefinition buildOperationDefinition();

    protected int countParameters(Parameters parameters, String name) {
        return getParameters(parameters, name).size();
    }

    /**
     * This is the method that concrete subclasses must implement to perform the operation logic.
     *
     * @return
     *     the Parameters object to return or null if there is no response Parameters object to return
     * @throws FHIROperationException
     */
    protected abstract Parameters doInvoke(
            FHIROperationContext operationContext,
            Class resourceType,
            String logicalId, String versionId,
            Parameters parameters,
            FHIRResourceHelpers resourceHelper) throws FHIROperationException;

    protected Parameters.Parameter getParameter(Parameters parameters, String name) {
        for (Parameters.Parameter parameter : parameters.getParameter()) {
            if (name.equals(parameter.getName().getValue())) {
                return parameter;
            }
        }
        return null;
    }

    protected List getParameterDefinitions(OperationParameterUse use) {
        List parameterDefinitions = new ArrayList();
        OperationDefinition definition = getDefinition();
        for (OperationDefinition.Parameter parameter : definition.getParameter()) {
            if (use.getValue().equals(parameter.getUse().getValue())) {
                parameterDefinitions.add(parameter);
            }
        }
        return parameterDefinitions;
    }

    protected List getParameters(Parameters parameters, String name) {
        List result = new ArrayList();
        if (parameters == null) {
            return result;
        }
        for (Parameters.Parameter parameter : parameters.getParameter()) {
            if (parameter.getName() != null && name.equals(parameter.getName().getValue())) {
                result.add(parameter);
            }
        }
        return result;
    }

    protected List getResourceTypeNames() {
        List resourceTypeNames = new ArrayList();
        OperationDefinition definition = getDefinition();
        for (ResourceType type : definition.getResource()) {
            resourceTypeNames.add(type.getValue());
        }
        return resourceTypeNames;
    }

    protected void validateInputParameters(
            FHIROperationContext operationContext,
            Class resourceType,
            String logicalId,
            String versionId,
            Parameters parameters) throws FHIROperationException {
        validateParameters(parameters, OperationParameterUse.IN);
    }

    protected void validateOperationContext(FHIROperationContext operationContext, Class resourceType, Parameters parameters) throws FHIROperationException {
        OperationDefinition definition = getDefinition();

        // Check which methods are allowed
        String method = (String) operationContext.getProperty(FHIROperationContext.PROPNAME_METHOD_TYPE);
        if (!"POST".equalsIgnoreCase(method)
                && (!"GET".equalsIgnoreCase(method) || !isGetMethodAllowed(definition, parameters))
                && !isAdditionalMethodAllowed(method)) {
            String msg = "HTTP method '" + method + "' is not supported for operation: '" + getName() + "'";
            throw buildExceptionWithIssue(msg, IssueType.NOT_SUPPORTED);
        }

        switch (operationContext.getType()) {
        case INSTANCE:
            if (definition.getInstance().getValue() == false) {
                String msg = "Operation context INSTANCE is not allowed for operation: '" + getName() + "'";
                throw buildExceptionWithIssue(msg, IssueType.INVALID);
            }
            validateResourceType(operationContext, resourceType);
            break;
        case RESOURCE_TYPE:
            if (definition.getType().getValue() == false) {
                String msg = "Operation context RESOURCE_TYPE is not allowed for operation: '" + getName() + "'";
                throw buildExceptionWithIssue(msg, IssueType.INVALID);
            }
            validateResourceType(operationContext, resourceType);
            break;
        case SYSTEM:
            if (definition.getSystem().getValue() == false) {
                String msg = "Operation context SYSTEM is not allowed for operation: '" + getName() + "'";
                throw buildExceptionWithIssue(msg, IssueType.INVALID);
            }
            break;
        default:
            break;
        }
    }

    /**
     * Determines if the operation disallows the GET method.
     * This is determined by the affectsState value of the OperatorDefinition and whether the
     * OperatorDefinition contains any non-primitive parameters.
     * @param operationDefinition the operation definition
     * @param parameters the parameters
     * @return true or false
     */
    private boolean isGetMethodAllowed(OperationDefinition operationDefinition, Parameters parameters) {
        // Check if affectState is true
        if (operationDefinition.getAffectsState() != null && operationDefinition.getAffectsState().getValue() == Boolean.TRUE) {
            return false;
        }
        // Check for any non-primitive parameters passed in
        if (parameters != null && operationDefinition.getParameter() != null) {
            for (Parameter parameter : parameters.getParameter()) {
                // Determine if parameter is non-primative by checking the operation definition for the parameter type
                for (OperationDefinition.Parameter odParameter : operationDefinition.getParameter()) {
                    if (parameter.getName().getValue() != null && odParameter.getName() != null
                            && parameter.getName().getValue().equals(odParameter.getName().getValue())
                            && (odParameter.getType() == null || !ModelSupport.isPrimitiveType(ModelSupport.getDataType(odParameter.getType().getValue()))
                                    || (odParameter.getPart() != null && !odParameter.getPart().isEmpty()))) {
                        return false;
                    }
                }
            }
        }
        return true;
    }

    /**
     * Determines if any methods (except GET and POST) are allowed for the operation.
     * This can be overridden by an operation to allow additional methods.
     * @return true or false
     */
    protected boolean isAdditionalMethodAllowed(String method) {
        return false;
    }

    /**
     * Determines if the operation disallows abstract resource types, Resource and DomainResource.
     * TODO: Remove this method when Issue #2526 is implemented, at which time, abstract resource types
     * will be disallowed for any operation.
     * @return true or false
     */
    protected boolean isAbstractResourceTypesDisallowed() {
        return false;
    }

    private void validateResourceType(FHIROperationContext operationContext, Class resourceType) throws FHIROperationException {
        String resourceTypeName = resourceType.getSimpleName();
        List resourceTypeNames = getResourceTypeNames();
        if ((isAbstractResourceTypesDisallowed() && !ModelSupport.isConcreteResourceType(resourceTypeName)) ||
                (!resourceTypeNames.contains(resourceTypeName) && !resourceTypeNames.contains("Resource"))) {
            String msg = "Resource type: '" + resourceTypeName + "' is not allowed for operation: '" + getName() + "'";
            throw buildExceptionWithIssue(msg, IssueType.INVALID);
        }
    }

    protected void validateOutputParameters(Parameters result) throws FHIROperationException {
        validateParameters(result, OperationParameterUse.OUT);
    }

    protected void validateParameters(Parameters parameters, OperationParameterUse use) throws FHIROperationException {
        String direction = OperationParameterUse.IN.equals(use) ? "input" : "output";

        // Retrieve the set of parameters from the OperationDefinition matching the specified use (in/out).
        List opDefParameters = getParameterDefinitions(use);

        // Verify that each defined parameter is specified appropriately in the Parameters object.
        for (OperationDefinition.Parameter parameterDefinition : opDefParameters) {
            String name = parameterDefinition.getName().getValue();
            int min = parameterDefinition.getMin().getValue();
            String max = parameterDefinition.getMax().getValue();
            int count = countParameters(parameters, name);
            if (count < min) {
                String msg = "Missing required " + direction + " parameter: '" + name + "'";
                throw buildExceptionWithIssue(msg, IssueType.REQUIRED);
            }
            if (!"*".equals(max)) {
                int maxValue = Integer.parseInt(max);
                if (count > maxValue) {
                    String msg = "Number of occurrences of " + direction + " parameter: '" + name + "' greater than allowed maximum: " + maxValue;
                    throw buildExceptionWithIssue(msg, IssueType.INVALID);
                }
            }
            if (count > 0) {
                List inputParameters = getParameters(parameters, name);
                for (Parameters.Parameter inputParameter : inputParameters) {
                    // Check to see if it's a parameter
                    if (inputParameter.getPart() == null || inputParameter.getPart().isEmpty()) {
                        String parameterValueTypeName = (inputParameter.getResource() != null) ?
                                inputParameter.getResource().getClass().getName() :
                                    inputParameter.getValue().getClass().getName();
                        String parameterDefinitionTypeName = parameterDefinition.getType().getValue();
                        parameterDefinitionTypeName = parameterDefinitionTypeName.substring(0, 1).toUpperCase() + parameterDefinitionTypeName.substring(1);
                        try {
                            Class parameterValueType, parameterDefinitionType;
                            parameterValueType = Class.forName(parameterValueTypeName);

                            if (ModelSupport.isResourceType(parameterDefinitionTypeName)) {
                                parameterDefinitionType = Class.forName("com.ibm.fhir.model.resource." + parameterDefinitionTypeName);
                            } else {
                                parameterDefinitionType = Class.forName("com.ibm.fhir.model.type." + parameterDefinitionTypeName);
                            }

                            if (!parameterDefinitionType.isAssignableFrom(parameterValueType)) {
                                String msg = "Invalid type: '" + parameterValueTypeName + "' for " + direction + " parameter: '" + name + "'";
                                throw buildExceptionWithIssue(msg, IssueType.INVALID);
                            }
                        } catch (FHIROperationException e) {
                            throw e;
                        } catch (Exception e) {
                            throw new FHIROperationException("An unexpected error occurred during type checking", e);
                        }
                    }
                }
            }
        }

        if (parameters == null) {
            return;
        }

        // Verify that each parameter contained in the Parameters object is defined in the OperationDefinition.
        for (Parameters.Parameter p : parameters.getParameter()) {
            String name = p.getName().getValue();

            OperationDefinition.Parameter opDefParameter = findOpDefParameter(opDefParameters, name);
            if (opDefParameter == null) {
                // Avoid throwing the exception for an OUT parameter called "return".
                if (!(OperationParameterUse.OUT.equals(use) && "return".equals(name))) {
                    String msg = "Unexpected " + direction + " parameter found: " + name;
                    throw buildExceptionWithIssue(msg, IssueType.INVALID);
                }
            }
        }
    }

    /**
     * Find the operation definition parameter with the specified name.
     *
     * @param parameters
     *     the list of parameters from the OperationDefinition
     * @param name
     *     the name of the parameter to find
     * @return
     *     the operation definition parameter with the specified name or null if not found
     */
    protected OperationDefinition.Parameter findOpDefParameter(List parameters, String name) {
        for (OperationDefinition.Parameter p : parameters) {
            if (p.getName().getValue().equals(name)) {
                return p;
            }
        }
        return null;
    }

    protected FHIROperationException buildExceptionWithIssue(String msg, IssueType issueType) throws FHIROperationException {
        return buildExceptionWithIssue(msg, issueType, null);
    }

    protected FHIROperationException buildExceptionWithIssue(String msg, IssueType issueType, Throwable cause) throws FHIROperationException {
        OperationOutcome.Issue ooi = FHIRUtil.buildOperationOutcomeIssue(msg, issueType);
        return new FHIROperationException(msg, cause).withIssue(ooi);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy