/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.nifi.processors.ccda; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.TimeUnit; import org.apache.commons.jexl3.JexlBuilder; import org.apache.commons.jexl3.JexlContext; import org.apache.commons.jexl3.JexlEngine; import org.apache.commons.jexl3.JexlExpression; import org.apache.commons.jexl3.MapContext; import org.apache.commons.lang3.StringUtils; import org.apache.nifi.annotation.behavior.InputRequirement; import org.apache.nifi.annotation.behavior.InputRequirement.Requirement; import org.apache.nifi.annotation.behavior.SideEffectFree; import org.apache.nifi.annotation.behavior.SupportsBatching; import org.apache.nifi.annotation.documentation.CapabilityDescription; import org.apache.nifi.annotation.documentation.Tags; import org.apache.nifi.annotation.lifecycle.OnScheduled; import org.apache.nifi.components.PropertyDescriptor; import org.apache.nifi.flowfile.FlowFile; import org.apache.nifi.processor.AbstractProcessor; import org.apache.nifi.processor.ProcessContext; import org.apache.nifi.processor.ProcessSession; import org.apache.nifi.processor.ProcessorInitializationContext; import org.apache.nifi.processor.Relationship; import org.apache.nifi.processor.exception.ProcessException; import org.apache.nifi.processor.util.StandardValidators; import org.apache.nifi.util.StopWatch; import org.eclipse.emf.common.util.Diagnostic; import org.openhealthtools.mdht.uml.cda.CDAPackage; import org.openhealthtools.mdht.uml.cda.ClinicalDocument; import org.openhealthtools.mdht.uml.cda.ccd.CCDPackage; import org.openhealthtools.mdht.uml.cda.consol.ConsolPackage; import org.openhealthtools.mdht.uml.cda.hitsp.HITSPPackage; import org.openhealthtools.mdht.uml.cda.ihe.IHEPackage; import org.openhealthtools.mdht.uml.cda.util.CDAUtil; import org.openhealthtools.mdht.uml.cda.util.CDAUtil.ValidationHandler; @SideEffectFree @SupportsBatching @InputRequirement(Requirement.INPUT_REQUIRED) @Tags({"CCDA", "healthcare", "extract", "attributes"}) @CapabilityDescription("Extracts information from an Consolidated CDA formatted FlowFile and provides individual attributes " + "as FlowFile attributes. The attributes are named as <Parent> <dot> <Key>. " + "If the Parent is repeating, the naming will be <Parent> <underscore> <Parent Index> <dot> <Key>. " + "For example, section.act_07.observation.name=Essential hypertension") public class ExtractCCDAAttributes extends AbstractProcessor { private static final char FIELD_SEPARATOR = '@'; private static final char KEY_VALUE_SEPARATOR = '#'; private Map<String, Map<String, String>> processMap = new LinkedHashMap<String, Map<String, String>>(); // stores mapping data for Parser private JexlEngine jexl = null; // JEXL Engine to execute code for mapping private JexlContext jexlCtx = null; // JEXL Context to hold element being processed private List<PropertyDescriptor> properties; private Set<Relationship> relationships; /** * SKIP_VALIDATION - Indicates whether to validate the CDA document after loading. * if true and the document is not valid, then ProcessException will be thrown */ public static final PropertyDescriptor SKIP_VALIDATION = new PropertyDescriptor.Builder().name("skip-validation") .displayName("Skip Validation").description("Whether or not to validate CDA message values").required(true) .allowableValues("true", "false").defaultValue("true").addValidator(StandardValidators.BOOLEAN_VALIDATOR) .build(); /** * REL_SUCCESS - Value to be returned in case the processor succeeds */ public static final Relationship REL_SUCCESS = new Relationship.Builder() .name("success") .description("A FlowFile is routed to this relationship if it is properly parsed as CDA and its contents extracted as attributes.") .build(); /** * REL_FAILURE - Value to be returned in case the processor fails */ public static final Relationship REL_FAILURE = new Relationship.Builder() .name("failure") .description("A FlowFile is routed to this relationship if it cannot be parsed as CDA or its contents extracted as attributes.") .build(); @Override protected List<PropertyDescriptor> getSupportedPropertyDescriptors() { return properties; } @Override public Set<Relationship> getRelationships() { return relationships; } @Override protected void init(final ProcessorInitializationContext context) { final Set<Relationship> _relationships = new HashSet<>(); _relationships.add(REL_SUCCESS); _relationships.add(REL_FAILURE); this.relationships = Collections.unmodifiableSet(_relationships); final List<PropertyDescriptor> _properties = new ArrayList<>(); _properties.add(SKIP_VALIDATION); this.properties = Collections.unmodifiableList(_properties); } @OnScheduled public void onScheduled(final ProcessContext context) throws IOException { getLogger().debug("Loading packages"); final StopWatch stopWatch = new StopWatch(true); // Load required MDHT packages System.setProperty( "org.eclipse.emf.ecore.EPackage.Registry.INSTANCE", "org.eclipse.emf.ecore.impl.EPackageRegistryImpl" ); CDAPackage.eINSTANCE.eClass(); HITSPPackage.eINSTANCE.eClass(); CCDPackage.eINSTANCE.eClass(); ConsolPackage.eINSTANCE.eClass(); IHEPackage.eINSTANCE.eClass(); stopWatch.stop(); getLogger().debug("Loaded packages in {}", new Object[] {stopWatch.getDuration(TimeUnit.MILLISECONDS)}); // Initialize JEXL jexl = new JexlBuilder().cache(1024).debug(false).silent(true).strict(false).create(); jexlCtx = new MapContext(); getLogger().debug("Loading mappings"); loadMappings(); // Load CDA mappings for parser } @Override public void onTrigger(final ProcessContext context, final ProcessSession session) { Map<String, String> attributes = new TreeMap<String, String>(); // stores CDA attributes getLogger().info("Processing CCDA"); FlowFile flowFile = session.get(); if ( flowFile == null ) { return; } if(processMap.isEmpty()) { getLogger().error("Process Mapping is not loaded"); session.transfer(flowFile, REL_FAILURE); return; } final Boolean skipValidation = context.getProperty(SKIP_VALIDATION).asBoolean(); final StopWatch stopWatch = new StopWatch(true); ClinicalDocument cd = null; try { cd = loadDocument(session.read(flowFile), skipValidation); // Load and optionally validate CDA document } catch (ProcessException e) { session.transfer(flowFile, REL_FAILURE); return; } getLogger().debug("Loaded document for {} in {}", new Object[] {flowFile, stopWatch.getElapsed(TimeUnit.MILLISECONDS)}); getLogger().debug("Processing elements"); processElement(null, cd, attributes); // Process CDA element using mapping data flowFile = session.putAllAttributes(flowFile, attributes); stopWatch.stop(); getLogger().debug("Successfully processed {} in {}", new Object[] {flowFile, stopWatch.getDuration(TimeUnit.MILLISECONDS)}); if(getLogger().isDebugEnabled()){ for (Entry<String, String> entry : attributes.entrySet()) { getLogger().debug("Attribute: {}={}", new Object[] {entry.getKey(), entry.getValue()}); } } session.transfer(flowFile, REL_SUCCESS); } /** * Process elements children based on the parser mapping. * Any String values are added to attributes * For List, the processList method is called to iterate and process * For an Object this method is called recursively * While adding to the attributes the key is prefixed by parent * @param parent parent key for this element, used as a prefix for attribute key * @param element element to be processed * @param attributes map of attributes to populate * @return map of processed data, value can contain String or Map of Strings */ protected Map<String, Object> processElement(String parent, Object element, Map<String, String> attributes) { final StopWatch stopWatch = new StopWatch(true); Map<String, Object> map = new LinkedHashMap<String, Object>(); String name = element.getClass().getName(); Map<String, String> jexlMap = processMap.get(name); // get JEXL mappings for this element if (jexlMap == null) { getLogger().warn("Missing mapping for element " + name); return null; } for (Entry<String, String> entry : jexlMap.entrySet()) { // evaluate JEXL for each child element jexlCtx.set("element", element); JexlExpression jexlExpr = jexl.createExpression(entry.getValue()); Object value = jexlExpr.evaluate(jexlCtx); String key = entry.getKey(); String prefix = parent != null ? parent + "." + key : key; addElement(map, prefix, key, value, attributes); } stopWatch.stop(); getLogger().debug("Processed {} in {}", new Object[] {name, stopWatch.getDuration(TimeUnit.MILLISECONDS)}); return map; } /** * Adds element to the attribute list based on the type * @param map object map * @param prefix parent key as prefix * @param key element key * @param value element value */ protected Map<String, String> addElement(Map<String, Object> map, String prefix, String key, Object value, Map<String, String> attributes) { // if the value is a String, add it to final attribute list // else process it further until we have a String representation if (value instanceof String) { if(value != null && !((String) value).isEmpty()) { map.put(key, value); attributes.put(prefix, (String) value); } } else if (value instanceof List) { if(value != null && !((List) value).isEmpty()) { map.put(key, processList(prefix, (List) value, attributes)); } } else if (value != null) { // process element further map.put(key, processElement(prefix, value, attributes)); } return attributes; } /** * Iterate through the list and calls processElement to process each element * @param key key used while calling processElement * @param value value is the individual Object being processed * @param attributes map of attributes to populate * @return list of elements */ protected List<Object> processList(String key, List value, Map<String, String> attributes) { List<Object> items = new ArrayList<Object>(); String keyFormat = value.size() > 1 ? "%s_%02d" : "%s"; for (Object item : value) { // iterate over all elements and process each element items.add(processElement(String.format(keyFormat, key, items.size() + 1), item, attributes)); } return items; } protected ClinicalDocument loadDocument(InputStream inputStream, Boolean skipValidation) { ClinicalDocument cd = null; try { cd = CDAUtil.load(inputStream); // load CDA document if (!skipValidation && !CDAUtil.validate(cd, new CDAValidationHandler())) { //optional validation getLogger().error("Failed to validate CDA document"); throw new ProcessException("Failed to validate CDA document"); } } catch (Exception e) { getLogger().error("Failed to load CDA document", e); throw new ProcessException("Failed to load CDA document", e); } return cd; } protected void loadMappings() { ClassLoader classloader = Thread.currentThread().getContextClassLoader(); Properties mappings = new Properties(); try (InputStream is = classloader.getResourceAsStream("mapping.properties")){ mappings.load(is); // each child element is key#value and multiple elements are separated by @ for (String property : mappings.stringPropertyNames()) { String[] variables = StringUtils.split(mappings.getProperty(property), FIELD_SEPARATOR); Map<String, String> map = new LinkedHashMap<String, String>(); for (String variable : variables) { String[] keyvalue = StringUtils.split(variable, KEY_VALUE_SEPARATOR); map.put(keyvalue[0], keyvalue[1]); } processMap.put(property, map); } } catch (IOException e) { getLogger().error("Failed to load mappings", e); throw new ProcessException("Failed to load mappings", e); } } protected class CDAValidationHandler implements ValidationHandler { @Override public void handleError(Diagnostic diagnostic) { getLogger().error(new StringBuilder("ERROR: ").append(diagnostic.getMessage()).toString()); } @Override public void handleWarning(Diagnostic diagnostic) { getLogger().warn(new StringBuilder("WARNING: ").append(diagnostic.getMessage()).toString()); } @Override public void handleInfo(Diagnostic diagnostic) { getLogger().info(new StringBuilder("INFO: ").append(diagnostic.getMessage()).toString()); } } }