Java tutorial
/* * Copyright (C) 2015 Systematic A/S * * 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. */ package com.systematic.healthcare.fhir.generator; import ca.uhn.fhir.model.api.IDatatype; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.annotation.Child; import ca.uhn.fhir.model.api.annotation.Description; import ca.uhn.fhir.model.api.annotation.Extension; import ca.uhn.fhir.model.api.annotation.ResourceDef; import ca.uhn.fhir.model.dstu2.composite.BoundCodeableConceptDt; import ca.uhn.fhir.model.dstu2.composite.ElementDefinitionDt; import ca.uhn.fhir.model.dstu2.composite.ResourceReferenceDt; import ca.uhn.fhir.model.dstu2.resource.StructureDefinition; import ca.uhn.fhir.model.primitive.BoundCodeDt; import ca.uhn.fhir.model.primitive.UriDt; import ca.uhn.fhir.util.ElementUtil; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.collect.FluentIterable; import org.apache.commons.lang3.StringUtils; import org.jboss.forge.roaster.Roaster; import org.jboss.forge.roaster.model.source.*; import javax.annotation.Nullable; import java.io.IOException; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.util.*; public class Generator { private static final String DSTU2_PACKAGE = "ca.uhn.fhir.model.dstu2"; private static final String DSTU2_RESOURCE_PACKAGE = DSTU2_PACKAGE + ".resource"; private static final String DSTU2_COMPOSITE_PACKAGE = DSTU2_PACKAGE + ".composite"; private static final String DSTU2_PRIMITIVE_PACKAGE = "ca.uhn.fhir.model.primitive"; public static final String HL7_FHIR_REFERENCE_URL_START = "http://hl7.org/fhir"; public static JavaClassSource generate(StructureDefinitionProvider resolver) throws Exception { return new Generator().convertDefinitionToJavaFile(resolver); } private JavaClassSource convertDefinitionToJavaFile(StructureDefinitionProvider resolver) throws Exception { StructureDefinition def = resolver.getDefinition(); final JavaClassSource javaClass = Roaster.create(JavaClassSource.class); Class<? extends IResource> superClass = (Class<? extends IResource>) Class .forName(DSTU2_RESOURCE_PACKAGE + "." + def.getConstrainedType()); javaClass.setPackage(resolver.getOutPackage()).setName(convertNameToValidJavaIdentifier(def.getName())) .extendSuperType(superClass); addClassResourceDefAnnotation(def, javaClass); Map<String, ResourceParser.FieldInfo> fieldInfo = new ResourceParser().parseResource(superClass); StructureDefinition.Differential dif = def.getDifferential(); List<ElementDefinitionDt> elements = dif.getElement(); for (ElementDefinitionDt element : elements) { if (element.getPath().indexOf('.') != -1) { //Element name is (path minus the constrained name). E.g. Path for field Subject on Observations is Observation.subject, which we resolve to subject String elementName = element.getPath().substring(def.getConstrainedType().length() + 1); if (elementName.endsWith("[x]")) { elementName = elementName.substring(0, elementName.indexOf("[x]")); } if (elementName.equals("extension")) { addExtensionField(javaClass, element, resolver); } else { addField(javaClass, fieldInfo, element, elementName); } } } addSliceFields(resolver, javaClass); List<FieldSource<JavaClassSource>> allFields = new ArrayList<>(); allFields.addAll(existingFieldsChanged); allFields.addAll(extensionFieldsAdded); addIsEmptyMethod(javaClass, allFields); addSettersAndGettersForFields(javaClass, existingFieldsChanged, false, superClass, fieldInfo); addSettersAndGettersForFields(javaClass, extensionFieldsAdded, true, superClass, fieldInfo); return javaClass; } private void addSettersAndGettersForFields(JavaClassSource javaClass, List<FieldSource<JavaClassSource>> fieldsAdded, boolean isExtension, Class<?> superClass, Map<String, ResourceParser.FieldInfo> fieldInfo) { for (FieldSource<JavaClassSource> field : fieldsAdded) { String fieldName = StringUtils.capitalize(field.getName().substring(2)); // Remove my ResourceParser.FieldInfo existingField = fieldInfo.get(fieldName.toLowerCase()); String genericType = null; if (field.getType().getTypeArguments().size() == 1) { genericType = field.getType().getTypeArguments().get(0).getName(); } else if (field.getType().getTypeArguments().size() > 1) { genericType = IDatatype.class.getName(); } String type = field.getType().getName(); String bodyGetSimple = "return " + field.getName() + ";"; if (field.getType().isType(List.class)) { bodyGetSimple = "if (" + field.getName() + " == null) {\n" + " " + field.getName() + " = new java.util.ArrayList<>();\n" + "}\n" + bodyGetSimple; type = type + "<" + genericType + ">"; } AnnotationSource<JavaClassSource> childAnnotation = field.getAnnotation(Child.class); String min = childAnnotation.getStringValue("min"); String max = childAnnotation.getStringValue("max"); boolean deprecate = "0".equals(min) && "0".equals(max); if (existingField != null) { for (Method method : existingField.getMethods()) { String simpleType = genericType != null ? genericType : field.getType().getName(); if (method.getName().startsWith("get") && method.getName().endsWith("FirstRep")) { String body = "if (get" + existingField.getOrigFieldName() + "().isEmpty()) {\n" + " return add" + existingField.getOrigFieldName() + "();\n" + "}\n" + "return get" + existingField.getOrigFieldName() + "().get(0);"; addGetMethod(javaClass, method.getName(), simpleType, body, deprecate); } else if (method.getName().startsWith("get") && method.getName().endsWith("Element")) { String body = "if (" + field.getName() + " == null) {\n" + " " + field.getName() + " = new " + type + "();\n" + " \n" + "return " + field.getName() + ";"; addGetMethod(javaClass, method.getName(), type, body, deprecate); } else if (method.getName().startsWith("get")) { addGetMethod(javaClass, method.getName(), type, bodyGetSimple, deprecate); } else if (method.getName().startsWith("set")) { String bodySet = field.getName() + " = theValue;\nreturn this;"; MethodSource<JavaClassSource> methodSet = javaClass.addMethod().setName("set" + fieldName) .setPublic().setReturnType(javaClass.getName()).setBody(bodySet); methodSet.addParameter(type, "theValue"); methodSet.addAnnotation(Override.class); if (deprecate) { methodSet.addAnnotation(Deprecated.class); } } else if (method.getName().startsWith("add") && method.getParameterTypes().length == 0) { String body = simpleType + " newType = new " + simpleType + "();\n" + " get" + existingField.getOrigFieldName() + "().add(newType);\n" + "return newType;"; addGetMethod(javaClass, method.getName(), simpleType, body, deprecate); } } continue; } //Extensions and slicing } } private void addGetMethod(JavaClassSource javaClass, String methodName, String type, String body, boolean deprecate) { MethodSource<JavaClassSource> method = javaClass.addMethod().setName(methodName).setPublic() .setReturnType(type).setBody(body); if (deprecate) { method.addAnnotation(Deprecated.class); } } private void addIsEmptyMethod(JavaClassSource javaClass, List<FieldSource<JavaClassSource>> fieldsAdded) { String types = Joiner.on(',') .join(FluentIterable.from(fieldsAdded).transform(new FieldSourceGetNameFunction())); String body = "return super.isEmpty() && ElementUtil.isEmpty(" + types + ");"; MethodSource<JavaClassSource> method = javaClass.addMethod().setName("isEmpty").setPublic() .setReturnType("boolean").setBody(body); method.addAnnotation(Override.class); javaClass.addImport(ElementUtil.class); } private void addSliceFields(StructureDefinitionProvider resolver, JavaClassSource javaClass) { for (Map.Entry<String, CompositeValue> i : slicePathToValues.entrySet()) { final JavaEnumSource enumClass = Roaster.create(JavaEnumSource.class); String enumName = slicedPathToEnumType.get(i.getKey()); enumClass.setPackage(resolver.getOutPackage()) .setName(convertNameToValidJavaIdentifier(enumName) + "Type"); for (CompositeValueField value : i.getValue().getFields()) { enumClass.addEnumConstant().setName(convertNameToValidJavaIdentifier(value.name).toUpperCase()); // TODO: fix slicing //System.out.println(value.fixedCode + " " + value.name + " " + value.type + " " + value.url); } javaClass.addNestedType(enumClass); } } private String convertNameToValidJavaIdentifier(String enumName) { StringBuilder b = new StringBuilder(); for (String part : enumName.split("[ ]")) { b.append(StringUtils.capitalize(part)); } return b.toString().replaceAll("[ \\.\\?]", ""); } private Set<String> sliced = new HashSet<>(); private Map<String, String> slicedPathToEnumType = new HashMap<>(); private Map<String, CompositeValue> slicePathToValues = new HashMap<>(); private CompositeValue lastSlicedValue = null; private CompositeValueField lastSlicedValueField = null; private List<FieldSource<JavaClassSource>> existingFieldsChanged = new ArrayList<>(); private List<FieldSource<JavaClassSource>> extensionFieldsAdded = new ArrayList<>(); private void addField(JavaClassSource javaClass, Map<String, ResourceParser.FieldInfo> fieldInfo, ElementDefinitionDt element, String elementName) { if (!element.getSlicing().getDiscriminator().isEmpty()) { sliced.add(element.getPath()); slicedPathToEnumType.put(element.getPath(), element.getShort()); lastSlicedValue = new CompositeValue(element.getPath(), element.getSlicing().getDescription()); slicePathToValues.put(element.getPath(), lastSlicedValue); } else { // The ElementDefinition is part of the last slice. if (lastSlicedValue != null && element.getPath().startsWith(lastSlicedValue.path)) { if (element.getPath().equals(lastSlicedValue.path)) { // This defines the name of slice. lastSlicedValueField = new CompositeValueField(element.getName()); lastSlicedValue.fields.add(lastSlicedValueField); } else if (element.getPath().equals(lastSlicedValue.path + ".code.coding.system")) { lastSlicedValueField.url = ((UriDt) element.getFixed()).getValue(); } else if (element.getPath().equals(lastSlicedValue.path + ".code.coding.code")) { lastSlicedValueField.fixedCode = String.valueOf(element.getFixed()); } else if (element.getPath().equals(lastSlicedValue.path + ".value[x]")) { lastSlicedValueField.type = String.valueOf(element.getFixed()); } return; } else if (elementName.indexOf('.') != -1) { //Dont know why we need theese sub elements. (.code and .code.system etc..) return; } } ResourceParser.FieldInfo inheritedField = fieldInfo.get(elementName.toLowerCase()); FieldSource<JavaClassSource> field = javaClass.addField() .setName("my" + StringUtils.capitalize(elementName)).setPrivate(); existingFieldsChanged.add(field); if (Collection.class.isAssignableFrom(inheritedField.getType())) { List<Class<?>> cl = FluentIterable.from(element.getType()) .transform(new TypeClassFunction(inheritedField)).toList(); if (cl.size() == 0) { setFieldTypeGeneric(javaClass, inheritedField, field); } else { String args = Joiner.on(',') .join(FluentIterable.from(cl).transform(new ClassToSimpleNameFunction())); field.setType(inheritedField.getType().getCanonicalName() + "<" + args + ">"); } } else { if (element.getBinding() != null && isBindingStrengthNotExample(element.getBinding()) && element.getBinding().getValueSet() instanceof ResourceReferenceDt) { ResourceReferenceDt ref = (ResourceReferenceDt) element.getBinding().getValueSet(); if (ref.getReference().getValue().startsWith(HL7_FHIR_REFERENCE_URL_START)) { if (BoundCodeableConceptDt.class.isAssignableFrom(inheritedField.getType())) { setFieldTypeGeneric(javaClass, inheritedField, field); } else if (BoundCodeDt.class.isAssignableFrom(inheritedField.getType())) { setFieldTypeGeneric(javaClass, inheritedField, field); } else { field.setType(inheritedField.getType()); } } else { field.setType(inheritedField.getType()); } } else { field.setType(inheritedField.getType()); } } List<Class<?>> fieldType = FluentIterable.from(element.getType()) .transform(new TypeClassFunction(inheritedField)).toList(); AnnotationSource<JavaClassSource> childAnnotation = addFieldChildAnnotation(element, elementName, field, false); childAnnotation.setClassArrayValue("type", fieldType.toArray(new Class[fieldType.size()])); addFieldDescriptionAnnotation(element, field); } private void setFieldTypeGeneric(JavaClassSource javaClass, ResourceParser.FieldInfo originalField, FieldSource<JavaClassSource> field) { Class<?> typeClass = (Class<?>) ((ParameterizedType) originalField.getGenericType()) .getActualTypeArguments()[0]; field.setType(originalField.getType().getCanonicalName() + "<" + typeClass.getSimpleName() + ">"); javaClass.addImport(typeClass); } private boolean isBindingStrengthNotExample(ElementDefinitionDt.Binding binding) { return binding.getStrength() == null || !binding.getStrength().equals("example"); } private void addClassResourceDefAnnotation(StructureDefinition def, JavaClassSource javaClass) { javaClass.addAnnotation(ResourceDef.class).setStringValue("name", def.getConstrainedType()) .setStringValue("id", def.getId().getIdPart()); } private void addExtensionField(JavaClassSource javaClass, ElementDefinitionDt element, StructureDefinitionProvider resolver) throws Exception { if (element.getType().size() > 1) { throw new IllegalStateException("WTF"); } else { if (element.getName() == null) { return; } FieldSource<JavaClassSource> field = javaClass.addField() .setName("my" + StringUtils.capitalize(element.getName())).setPrivate(); extensionFieldsAdded.add(field); Class<?> extensionType = getExtensionType(element, resolver); if (extensionType != null) { field.setType(extensionType); } else { field.setType(Roaster.parse("public class " + StringUtils.capitalize(element.getName()) + " {}")); String errMsg = "Replace " + StringUtils.capitalize(element.getName()) + ".class with correct extension name"; addTodoAndDeprecationAnnotation(field, errMsg); } addFieldExtensionAnnotation(element, field); addFieldChildAnnotation(element, element.getName(), field, true); addFieldDescriptionAnnotation(element, field); } } private void addFieldExtensionAnnotation(ElementDefinitionDt element, FieldSource<JavaClassSource> field) { field.addAnnotation(Extension.class).setLiteralValue("definedLocally", "false") .setLiteralValue("isModifier", "false") .setStringValue("url", element.getTypeFirstRep().getProfileFirstRep().getValue()); } private void addTodoAndDeprecationAnnotation(FieldSource<JavaClassSource> field, String errMsg) { field.getJavaDoc().addTagValue("TODO:", errMsg); field.getJavaDoc().addTagValue("@deprecated", errMsg); } private Class<?> getExtensionType(ElementDefinitionDt element, StructureDefinitionProvider resolver) throws IOException { StructureDefinition def = resolver.provideReferenceDefinition(element); for (ElementDefinitionDt el : def.getDifferential().getElement()) { if (el.getPath().equals("Extension.value[x]")) { return getDSTU2ClassType(el.getTypeFirstRep()); } } throw new IllegalArgumentException("Could not find name for extension: " + element); } private AnnotationSource<JavaClassSource> addFieldChildAnnotation(ElementDefinitionDt element, String name, FieldSource<JavaClassSource> field, boolean isExtension) { AnnotationSource<JavaClassSource> childAnnotation = field.addAnnotation(Child.class); childAnnotation.setStringValue("name", name); childAnnotation.setLiteralValue("min", element.getMin().toString()); childAnnotation.setLiteralValue("max", "*".equals(element.getMax()) ? "Child.MAX_UNLIMITED" : element.getMax()); childAnnotation.setLiteralValue("order", isExtension ? "Child.ORDER_UNKNOWN" : "Child.REPLACE_PARENT"); childAnnotation.setLiteralValue("summary", element.getIsSummary() != null ? element.getIsSummary().toString() : "false"); childAnnotation.setLiteralValue("modifier", element.getIsModifier() != null ? element.getIsModifier().toString() : "false"); return childAnnotation; } private void addFieldDescriptionAnnotation(ElementDefinitionDt element, FieldSource<JavaClassSource> field) { AnnotationSource<JavaClassSource> descriptionAnnotation = field.addAnnotation(Description.class); if (element.getShort() != null) { descriptionAnnotation.setStringValue("shortDefinition", element.getShort()); } if (element.getDefinition() != null) { descriptionAnnotation.setStringValue("formalDefinition", element.getDefinition()); } } private static class TypeClassFunction implements Function<ElementDefinitionDt.Type, Class<?>> { private ResourceParser.FieldInfo field; public TypeClassFunction(ResourceParser.FieldInfo field) { this.field = field; } @Nullable @Override public Class<?> apply(@Nullable ElementDefinitionDt.Type input) { return getClassFromType(input, field); } } private static Class<?> getClassFromType(@Nullable ElementDefinitionDt.Type input, ResourceParser.FieldInfo originalField) { switch (input.getCode()) { case "BackboneElement": if (originalField.getGenericType() instanceof ParameterizedType) { return (Class<?>) ((ParameterizedType) originalField.getGenericType()).getActualTypeArguments()[0]; } else { return originalField.getType(); } case "Reference": // TODO: for Reference Fields the type should be The referenced type. //return originalField.getType(); return ResourceReferenceDt.class; default: return getDSTU2ClassType(input); } } private static Class<?> getDSTU2ClassType(@Nullable ElementDefinitionDt.Type input) { try { try { return Class .forName(DSTU2_PRIMITIVE_PACKAGE + "." + StringUtils.capitalize(input.getCode()) + "Dt"); } catch (ClassNotFoundException ee) { return Class.forName(DSTU2_COMPOSITE_PACKAGE + "." + input.getCode() + "Dt"); } } catch (ClassNotFoundException e) { throw new IllegalStateException("Cannot locate class", e); } } private static class ClassToSimpleNameFunction implements Function<Class<?>, String> { @Nullable @Override public String apply(@Nullable Class<?> input) { return input.getSimpleName(); } } private static class CompositeValue { public String description; public String path; public List<CompositeValueField> fields = new ArrayList<>(); public CompositeValue(String path, String description) { this.path = path; this.description = description; } public List<CompositeValueField> getFields() { return fields; } } private static class CompositeValueField { public String url; public String name; public String type; public String fixedCode; public CompositeValueField(String name) { this.name = name; } } private static class FieldSourceGetNameFunction implements Function<FieldSource<JavaClassSource>, String> { @Nullable @Override public String apply(@Nullable FieldSource<JavaClassSource> input) { return input.getName(); } } }