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

org.openehr.build.RMObjectBuilder Maven / Gradle / Ivy

/*
 * component:   "openEHR Java Reference Implementation"
 * description: "Class RMObjectBuilder"
 * keywords:    "builder"
 *
 * author:      "Rong Chen "
 * copyright:   "Copyright (c) 2003-2008 ACODE HB, Sweden"
 * license:     "See notice at bottom of class"
 *
 * file:        "$URL$"
 * revision:    "$LastChangedRevision$"
 * last_change: "$LastChangedDate$"
 */
package org.openehr.build;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.openehr.rm.Attribute;
import org.openehr.rm.FullConstructor;
import org.openehr.rm.RMObject;
import org.openehr.rm.common.archetyped.Archetyped;
import org.openehr.rm.common.changecontrol.Contribution;
import org.openehr.rm.common.changecontrol.OriginalVersion;
import org.openehr.rm.common.generic.Attestation;
import org.openehr.rm.common.generic.AuditDetails;
import org.openehr.rm.common.generic.Participation;
import org.openehr.rm.common.generic.PartyIdentified;
import org.openehr.rm.common.generic.PartyRelated;
import org.openehr.rm.common.generic.PartySelf;
import org.openehr.rm.composition.Composition;
import org.openehr.rm.composition.EventContext;
import org.openehr.rm.composition.content.entry.Action;
import org.openehr.rm.composition.content.entry.Activity;
import org.openehr.rm.composition.content.entry.AdminEntry;
import org.openehr.rm.composition.content.entry.Evaluation;
import org.openehr.rm.composition.content.entry.ISMTransition;
import org.openehr.rm.composition.content.entry.Instruction;
import org.openehr.rm.composition.content.entry.InstructionDetails;
import org.openehr.rm.composition.content.entry.Observation;
import org.openehr.rm.composition.content.navigation.Section;
import org.openehr.rm.datastructure.history.History;
import org.openehr.rm.datastructure.history.IntervalEvent;
import org.openehr.rm.datastructure.history.PointEvent;
import org.openehr.rm.datastructure.itemstructure.ItemList;
import org.openehr.rm.datastructure.itemstructure.ItemSingle;
import org.openehr.rm.datastructure.itemstructure.ItemTable;
import org.openehr.rm.datastructure.itemstructure.ItemTree;
import org.openehr.rm.datastructure.itemstructure.representation.Cluster;
import org.openehr.rm.datastructure.itemstructure.representation.Element;
import org.openehr.rm.datatypes.basic.DvBoolean;
import org.openehr.rm.datatypes.basic.DvIdentifier;
import org.openehr.rm.datatypes.basic.DvState;
import org.openehr.rm.datatypes.encapsulated.DvMultimedia;
import org.openehr.rm.datatypes.encapsulated.DvParsable;
import org.openehr.rm.datatypes.quantity.DvCount;
import org.openehr.rm.datatypes.quantity.DvInterval;
import org.openehr.rm.datatypes.quantity.DvOrdinal;
import org.openehr.rm.datatypes.quantity.DvProportion;
import org.openehr.rm.datatypes.quantity.DvQuantity;
import org.openehr.rm.datatypes.quantity.ProportionKind;
import org.openehr.rm.datatypes.quantity.datetime.DvDate;
import org.openehr.rm.datatypes.quantity.datetime.DvDateTime;
import org.openehr.rm.datatypes.quantity.datetime.DvDuration;
import org.openehr.rm.datatypes.quantity.datetime.DvTime;
import org.openehr.rm.datatypes.text.CodePhrase;
import org.openehr.rm.datatypes.text.DvCodedText;
import org.openehr.rm.datatypes.text.DvParagraph;
import org.openehr.rm.datatypes.text.DvText;
import org.openehr.rm.datatypes.uri.DvEHRURI;
import org.openehr.rm.datatypes.uri.DvURI;
import org.openehr.rm.demographic.*;
import org.openehr.rm.support.identification.*;
import org.openehr.rm.support.identification.UUID;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.util.*;

/**
 * Reference model class instances builder
 *
 * @author Rong Chen
 * @version 1.0
 */
public class RMObjectBuilder {

    /**
     * Factory method to create an instance of the RM builder
     *
     * @param systemValues
     * @return
     */
    public static RMObjectBuilder getInstance(Map systemValues) {
        return new RMObjectBuilder(systemValues);
    }

    /**
     * Create a RMObjectBuilder
     *
     * @param systemValues
     */
    public RMObjectBuilder(Map systemValues) {

        this();

        if (systemValues == null) {
            throw new IllegalArgumentException("systemValues null");
        }
        this.systemValues = systemValues;
    }

    // for testing purpose
    RMObjectBuilder() {
        try {
            loadTypeMap();
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("failed to load class, " + e
                    + " when starting rmObjectBuilder..");
        }
    }

    // load all reference types that are possible to instantiate
    // using FullConstructor annotation
    private Map loadTypeMap() throws ClassNotFoundException {
        Class[] classes = {

                // common classes
                PartySelf.class,
                Archetyped.class,
                Attestation.class,
                AuditDetails.class,
                Participation.class,
                PartyIdentified.class,
                PartyRelated.class,
                PartySelf.class,
                OriginalVersion.class,
                Contribution.class,

                // support classes
                TerminologyID.class,
                ArchetypeID.class,
                HierObjectID.class,
                AccessGroupRef.class,
                GenericID.class,
                InternetID.class,
                ISO_OID.class,
                LocatableRef.class,
                ObjectVersionID.class,
                ObjectRef.class,
                PartyRef.class,
                TemplateID.class,
                TerminologyID.class,
                org.openehr.rm.support.identification.UUID.class,
                //VersionTreeID.class,

                // datatypes classes
                DvBoolean.class,
                DvState.class,
                DvIdentifier.class,
                DvText.class,
                DvCodedText.class,
                DvParagraph.class,
                CodePhrase.class,
                DvCount.class,
                DvOrdinal.class,
                DvQuantity.class,
                DvInterval.class,
                DvProportion.class,
                DvDate.class,
                DvDateTime.class,
                DvTime.class,
                DvDuration.class,
                DvParsable.class,
                DvURI.class,
                DvEHRURI.class,
                DvMultimedia.class,

                // datastructure classes
                Element.class,
                Cluster.class,
                ItemSingle.class,
                ItemList.class,
                ItemTable.class,
                ItemTree.class,
                //ItemStructure.class,
                History.class,
                IntervalEvent.class,
                PointEvent.class,

                // ehr classes
                Action.class, Activity.class, Evaluation.class, ISMTransition.class,
                Instruction.class, InstructionDetails.class, Observation.class, AdminEntry.class,
                Section.class, Composition.class,
                EventContext.class, ISMTransition.class,

                // demographic classes
                Address.class, PartyIdentity.class, Agent.class, Group.class,
                Organisation.class, Person.class, Contact.class,
                PartyRelationship.class, Role.class, Capability.class};

        typeMap = new LinkedHashMap<>();
        upperCaseMap = new LinkedHashMap<>();
        for (Class klass : classes) {
            String name = klass.getSimpleName();
            typeMap.put(name, klass);
            upperCaseMap.put(name.toUpperCase(), klass);
        }

        return typeMap;
    }

    /*
     * Return a map with name as the key and index of position as the value for
     * required parameters of the full constructor in the RMObject
     *
     * @param rmClass @return
     */
    private Map attributeType(Class rmClass) {

        Map map = new HashMap();
        Constructor constructor = fullConstructor(rmClass);
        if (constructor == null) {
            throw new IllegalArgumentException("no annotated constructor of "
                    + rmClass + ">");
        }
        Annotation[][] annotations = constructor.getParameterAnnotations();
        Class[] types = constructor.getParameterTypes();

        if (annotations.length != types.length) {
            throw new IllegalArgumentException("less annotations");
        }
        for (int i = 0; i < types.length; i++) {
            if (annotations[i].length == 0) {
                throw new IllegalArgumentException(
                        "missing annotations of attribute " + i);
            }
            Attribute attribute = (Attribute) annotations[i][0];
            map.put(attribute.name(), types[i]);
        }
        return map;
    }

    /**
     * Return a map with name as the key and index of position as the value for
     * all parameters of the full constructor in the RMObject
     *
     * @param rmClass
     * @return
     */
    private Map attributeIndex(Class rmClass) {
        Map map = new HashMap();
        Constructor constructor = fullConstructor(rmClass);
        Annotation[][] annotations = constructor.getParameterAnnotations();

        for (int i = 0; i < annotations.length; i++) {
            if (annotations[i].length == 0) {
                throw new IllegalArgumentException(
                        "missing annotation at position " + i);
            }
            Attribute attribute = (Attribute) annotations[i][0];
            map.put(attribute.name(), i);
        }
        return map;
    }

    /**
     * Return a map with name as the key and index of position as the value for
     * all parameters of the full constructor in the RMObject
     *
     * @param rmClass
     * @return
     */
    private Map attributeMap(Class rmClass) {
        Map map = new HashMap();
        Constructor constructor = fullConstructor(rmClass);
        Annotation[][] annotations = constructor.getParameterAnnotations();

        for (int i = 0; i < annotations.length; i++) {
            if (annotations[i].length == 0) {
                throw new IllegalArgumentException(
                        "missing annotation at position " + i);
            }
            Attribute attribute = (Attribute) annotations[i][0];
            map.put(attribute.name(), attribute);
        }
        return map;
    }

    private Constructor fullConstructor(Class klass) {
        Constructor constructor = constructorMap.get(klass);
        if (constructor == null) {
            Constructor[] array = klass.getConstructors();
            int i = 0;
            boolean found = false;
            while (i < array.length && !found) {
                Constructor constructorAux = array[i++];
                if (constructorAux.isAnnotationPresent(FullConstructor.class)) {
                    constructor = constructorAux;
                    constructorMap.put(klass, constructor);
                    found = true;
                }
            }
        }
        return constructor;
    }

    /**
     * Return system value for given id
     *
     * @param id
     * @return null if not there
     */
    public Object getSystemValue(SystemValue id) {
        return systemValues.get(id);
    }

    /**
     * Construct an instance of RM class of given name and values. 

If the * input is a string, and the required attribute is some other types * (integer, double etc), it will be converted into right type. if there is * any error during conversion, AttributeFormatException will be thrown. * * @param rmClassName * @param valueMap * @return created instance * @throws RMObjectBuildingException */ public RMObject construct(String rmClassName, Map valueMap) throws RMObjectBuildingException { Class rmClass = retrieveRMType(rmClassName); // replace underscore separated names with camel case Map filteredMap = new HashMap(); for (Map.Entry entry : valueMap.entrySet()) { filteredMap.put(toCamelCase(entry.getKey()), entry.getValue()); } Constructor constructor = fullConstructor(rmClass); Map typeMap = attributeType(rmClass); Map indexMap = attributeIndex(rmClass); Map attributeMap = attributeMap(rmClass); Object[] valueArray = new Object[indexMap.size()]; for (Map.Entry entry : typeMap.entrySet()) { String name = entry.getKey(); Object value = filteredMap.get(name); if (!typeMap.containsKey(name) || !attributeMap.containsKey(name)) { throw new RMObjectBuildingException("unknown attribute " + name); } Class type = entry.getValue(); Integer index = indexMap.get(name); Attribute attribute = attributeMap.get(name); if (index == null || type == null) { throw new RMObjectBuildingException("unknown attribute \"" + name + "\""); } // system supplied value if (attribute.system()) { SystemValue sysvalue = SystemValue.fromId(name); if (sysvalue == null) { throw new RMObjectBuildingException("unknonw system value" + "\"" + name + "\""); } value = systemValues.get(sysvalue); if (value == null) { throw new AttributeMissingException("missing value for " + "system attribute \"" + name + "\" in class: " + rmClass + ", with valueMap: " + valueMap); } } // check required attributes if (value == null && attribute.required()) { log.info(attribute); throw new AttributeMissingException("missing value for " + "required attribute \"" + name + "\" of type " + type + " while constructing " + rmClass + " with valueMap: " + valueMap); } // enum else if (type.isEnum() && !value.getClass().isEnum()) { // OG added if (type.equals(ProportionKind.class)) value = ProportionKind.fromValue(Integer.parseInt(value.toString())); else value = Enum.valueOf(type, value.toString()); } // in case of null, create a default value else if (value == null) { value = defaultValue(type); } // in case of string value, convert to right type if necessary else if (value instanceof String) { String str = (String) value; try { // for DvCount if (type.equals(int.class)) { value = Integer.parseInt(str); // for DvQuantity } else if (type.equals(double.class)) { value = Double.parseDouble(str); // for DvProportion.precision } else if (type.equals(Integer.class)) { value = Integer.valueOf(str); // for DvBoolean.value } else if (type.equals(boolean.class)) { value = new Boolean(str).booleanValue(); } } catch (NumberFormatException e) { throw new AttributeFormatException("wrong format of " + "attribute " + name + ", expect " + type); } // deal with mismatch between array and list } else if (type.isAssignableFrom(List.class) && value.getClass().isArray()) { Object[] array = (Object[]) value; List list = new ArrayList(); for (Object o : array) { list.add(o); } value = list; // deal with mismatch between array and set } else if (type.isAssignableFrom(Set.class) && value.getClass().isArray()) { Object[] array = (Object[]) value; Set set = new HashSet(); for (Object o : array) { set.add(o); } value = set; } // check type else if (!type.isPrimitive()) { try { type.cast(value); } catch (ClassCastException e) { throw new RMObjectBuildingException("Failed to construct: " + rmClassName + ", value for attribute '" + name + "' has wrong type, expected \"" + type + "\", but got \"" + value.getClass() + "\""); } } valueArray[index] = value; } Object ret = null; try { // OG added hack if (rmClassName.equalsIgnoreCase("DVCOUNT")) { log.debug("Fixing DVCOUNT..."); for (int i = 0; i < valueArray.length; i++) { Object value = valueArray[i]; if (value != null && value.getClass().equals(Float.class)) valueArray[i] = Double.parseDouble(value.toString()); else if (value != null && value.getClass().equals(Long.class)) valueArray[i] = Integer.parseInt(value.toString()); } } ret = constructor.newInstance(valueArray); } catch (Exception e) { if (log.isDebugEnabled()) { e.printStackTrace(); } log.debug("failed in constructor.newInstance()", e); if (stringParsingTypes.contains(rmClassName)) { throw new AttributeFormatException("wrong format for type " + rmClassName); } throw new RMObjectBuildingException( "failed to create new instance of " + rmClassName + " with valueMap: " + toString(valueMap) + ", cause: " + e.getMessage()); } return (RMObject) ret; } private String toString(Map map) { StringBuffer buf = new StringBuffer(); for (Map.Entry entry : map.entrySet()) { buf.append(entry.getKey()); buf.append("="); Object value = entry.getValue(); if (value != null) { buf.append(value.getClass().getName()); buf.append(":"); buf.append(value.toString()); } else { buf.append("null"); } buf.append(", "); } return buf.toString(); } /** * Retrieves RM type using given name try both the CamelCase and * Underscore-separated ways * * @param rmClassName * @return * @throws Exception */ public Class retrieveRMType(String rmClassName) throws RMObjectBuildingException { int index = rmClassName.indexOf("<"); if (index > 0) { rmClassName = rmClassName.substring(0, index); } Class rmClass = typeMap.get(rmClassName); if (rmClass == null) { rmClass = upperCaseMap.get(rmClassName.replace("_", "")); } if (rmClass == null) { throw new RMObjectBuildingException("RM type unknown: \"" + rmClassName + "\""); } return rmClass; } /** * Retrieves list of attribute names of given class * * @param rmClassName * @return * @throws RMObjectBuildingException */ public Map retrieveAttribute(String rmClassName) throws RMObjectBuildingException { Class rmClass = retrieveRMType(rmClassName); Map map = attributeType(rmClass); return map; } private String toCamelCase(String underscoreSeparated) { StringTokenizer tokens = new StringTokenizer(underscoreSeparated, "_"); StringBuffer buf = new StringBuffer(); while (tokens.hasMoreTokens()) { String word = tokens.nextToken(); if (buf.length() == 0) { buf.append(word); } else { buf.append(word.substring(0, 1).toUpperCase()); buf.append(word.substring(1)); } } return buf.toString(); } private String toUnderscoreSeparated(String camelCase) { String[] array = StringUtils.splitByCharacterTypeCamelCase(camelCase); StringBuffer buf = new StringBuffer(); for (int i = 0; i < array.length; i++) { String s = array[i]; buf.append(s.substring(0, 1).toLowerCase()); buf.append(s.substring(1)); if (i != array.length - 1) { buf.append("_"); } } return buf.toString(); } /** * Finds the matching RM class that can be used to create RM object for * given value map * * @param valueMap * @return null if no match RM class is found */ public String findMatchingRMClass(Map valueMap) { List simpleTypes = Arrays.asList(SKIPPED_TYPES_IN_MATCHING); for (Class rmClass : typeMap.values()) { log.debug("matching rmClass: " + rmClass.getName()); if (simpleTypes.contains(rmClass.getSimpleName())) { continue; // skip simple value types } // replace underscore separated names with camel case Map filteredMap = new HashMap(); for (Map.Entry entry : valueMap.entrySet()) { filteredMap.put(toCamelCase(entry.getKey()), entry.getValue()); } Constructor constructor = fullConstructor(rmClass); if (constructor == null) { throw new RuntimeException("annotated constructor missing for " + rmClass); } Annotation[][] annotations = constructor.getParameterAnnotations(); if (annotations == null || annotations.length == 0) { throw new RuntimeException("attribute annotations missing for " + rmClass); } Class[] types = constructor.getParameterTypes(); boolean matched = true; Set attributes = new HashSet(); for (int i = 0; i < types.length; i++) { if (annotations[i].length == 0) { throw new RuntimeException( "attribute annotation missing for" + rmClass); } Attribute attribute = (Attribute) annotations[i][0]; attributes.add(attribute.name()); log.debug("checking attribute: " + attribute.name()); String attrName = attribute.name(); Object attrValue = filteredMap.get(attrName); if (attribute.required() && attrValue == null) { log.debug("missing required attribute.."); matched = false; break; } else if (attrValue != null) { if (((attrValue instanceof Boolean) && types[i] != boolean.class) || ((attrValue instanceof Integer) && types[i] != Integer.class) || ((attrValue instanceof Double) && types[i] != double.class)) { log.debug("wrong primitive value type for attribute.."); matched = false; break; } else if (!types[i].isPrimitive() && !types[i].isInstance(attrValue)) { log.debug("wrong value type for attribute.."); matched = false; break; } } } for (String attr : filteredMap.keySet()) { if (!attributes.contains(attr)) { log.debug("unknown attribute: " + attr); matched = false; } } // matching found if (matched) { String className = rmClass.getSimpleName(); log.debug(">>> MATCHING FOUND: " + className); return className; } } return null; } // todo: isn't there any support from java api on this? private Object defaultValue(Class type) { if (type == boolean.class) { return Boolean.FALSE; } else if (type == double.class) { return new Double(0); } else if (type == float.class) { return new Float(0); } else if (type == int.class) { return Integer.valueOf(0); } else if (type == short.class) { return Short.valueOf((short) 0); } else if (type == long.class) { return Long.valueOf(0); } else if (type == char.class) { return Character.valueOf((char) 0); } else if (type == byte.class) { return Byte.valueOf((byte) 0); } return null; } /* * Skipped types during matching: 1. Simple value types in DADL 2. Cluster * due to clash with ItemList */ private static final String[] SKIPPED_TYPES_IN_MATCHING = {"DvDateTime", "DvDate", "DvTime", "DvDuration", "Cluster", // due to clash with DvText "TerminologyID", "ArchetypeID", "TemplateID", "ISO_OID", "HierObjectID", "DvBoolean", "InternetID", "UUID", "ObjectVersionID", "DvURI", "DvEHRURI" }; /* logger */ private static final Logger log = Logger.getLogger(RMObjectBuilder.class); /* fields */ private Map systemValues; // loaded rm type map private Map typeMap; private Map upperCaseMap; private static final Set stringParsingTypes; // These should be rm_type_names not Java class names. private final Map constructorMap = Collections.synchronizedMap(new HashMap()); static { // so far only types from quantity.datetime stringParsingTypes = new HashSet(); //String[] types = { "DvDate", "DvDateTime", "DvTime", "DvDuration", "DvPartialDate", "DvPartialTime" }; String[] types = {"DV_DATE", "DV_DATE_TIME", "DV_TIME", "DV_DURATION", "DV_PARTIAL_DATE", "DV_PARTIAL_TIME"}; stringParsingTypes.addAll(Arrays.asList(types)); } } /* * ***** BEGIN LICENSE BLOCK ***** Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (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.mozilla.org/MPL/ * * Software distributed under the License is distributed on an 'AS IS' basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for * the specific language governing rights and limitations under the License. * * The Original Code is RMObjectBuilder.java * * The Initial Developer of the Original Code is Rong Chen. Portions created by * the Initial Developer are Copyright (C) 2003-2008 the Initial Developer. All * Rights Reserved. * * Contributor(s): Daniel Karlsson * * Software distributed under the License is distributed on an 'AS IS' basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for * the specific language governing rights and limitations under the License. * * ***** END LICENSE BLOCK ***** */





© 2015 - 2025 Weber Informatics LLC | Privacy Policy