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

com.regnosys.rosetta.translate.IngesterGenerator.xtend Maven / Gradle / Ivy

There is a newer version: 11.25.1
Show newest version
package com.regnosys.rosetta.translate

import com.codahale.metrics.MetricRegistry
import com.google.common.base.Splitter
import com.google.common.collect.LinkedListMultimap
import com.google.common.collect.Multimap
import com.google.common.collect.TreeMultimap
import com.regnosys.rosetta.common.util.UrlUtils
import com.regnosys.rosetta.generator.java.enums.EnumHelper
import com.regnosys.rosetta.generator.java.types.JavaTypeTranslator
import com.regnosys.rosetta.generator.object.ExpandedAttribute
import com.regnosys.rosetta.generator.object.ExpandedType
import com.regnosys.rosetta.rosetta.RosettaAttributeReference
import com.regnosys.rosetta.rosetta.RosettaBuiltinType
import com.regnosys.rosetta.rosetta.RosettaDataReference
import com.regnosys.rosetta.rosetta.RosettaEnumValueReference
import com.regnosys.rosetta.rosetta.RosettaEnumeration
import com.regnosys.rosetta.rosetta.RosettaMapTestExpression
import com.regnosys.rosetta.rosetta.RosettaMergeSynonymValue
import com.regnosys.rosetta.rosetta.RosettaModel
import com.regnosys.rosetta.rosetta.RosettaSynonymSource
import com.regnosys.rosetta.rosetta.RosettaType
import com.regnosys.rosetta.rosetta.RosettaTypeAlias
import com.regnosys.rosetta.rosetta.expression.RosettaBooleanLiteral
import com.regnosys.rosetta.rosetta.expression.RosettaIntLiteral
import com.regnosys.rosetta.rosetta.expression.RosettaLiteral
import com.regnosys.rosetta.rosetta.expression.RosettaNumberLiteral
import com.regnosys.rosetta.rosetta.expression.RosettaStringLiteral
import com.regnosys.rosetta.rosetta.simple.Data
import com.regnosys.rosetta.translate.CustomDeserialise.ParseException
import com.regnosys.rosetta.translate.MappingError.MappingErrorLevel
import com.regnosys.rosetta.translate.datamodel.Cardinality
import com.regnosys.rosetta.translate.datamodel.ModelParser
import com.regnosys.rosetta.translate.datamodel.Schema
import com.regnosys.rosetta.translate.datamodel.SchemaDeserialise
import com.regnosys.rosetta.translate.datamodel.SchemaSerialise
import com.regnosys.rosetta.translate.datamodel.json.JsonSchemaParser
import com.regnosys.rosetta.translate.datamodel.xsd.BeansXsdParser
import com.regnosys.rosetta.translate.synonymmap.AttributeGroup
import com.regnosys.rosetta.translate.synonymmap.AttributeGroupMapping
import com.regnosys.rosetta.translate.synonymmap.CardinalityChecker
import com.regnosys.rosetta.translate.synonymmap.Element
import com.regnosys.rosetta.translate.synonymmap.ModelCombiner
import com.regnosys.rosetta.translate.synonymmap.SynonymAbsentTest
import com.regnosys.rosetta.translate.synonymmap.SynonymBinaryTest
import com.regnosys.rosetta.translate.synonymmap.SynonymCondition
import com.regnosys.rosetta.translate.synonymmap.SynonymConditionFuncTest
import com.regnosys.rosetta.translate.synonymmap.SynonymExistsTest
import com.regnosys.rosetta.translate.synonymmap.SynonymGroup
import com.regnosys.rosetta.translate.synonymmap.SynonymMap
import com.regnosys.rosetta.translate.synonymmap.SynonymMapBuilder
import com.regnosys.rosetta.translate.synonymmap.SynonymPathTest
import com.regnosys.rosetta.translate.synonymmap.SynonymRosettaPathTest
import com.regnosys.rosetta.translate.synonymmap.SynonymTest
import com.regnosys.rosetta.translate.synonymmap.SynonymTest.TestPriority
import com.regnosys.rosetta.translate.synonymmap.SynonymValue
import com.regnosys.rosetta.types.builtin.RBuiltinTypeService
import com.rosetta.model.lib.RosettaModelObject
import com.rosetta.model.lib.meta.FieldWithMeta
import com.rosetta.util.types.JavaClass
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.net.URL
import java.util.Collection
import java.util.Collections
import java.util.HashMap
import java.util.LinkedHashMap
import java.util.LinkedHashSet
import java.util.List
import java.util.Map
import java.util.Set
import java.util.function.Function
import java.util.function.Predicate
import java.util.stream.Collectors
import javax.inject.Inject
import org.apache.commons.text.StringEscapeUtils
import org.apache.log4j.Logger
import org.eclipse.xtend2.lib.StringConcatenationClient

import static extension com.regnosys.rosetta.generator.util.RosettaAttributeExtensions.*
import static extension com.regnosys.rosetta.generator.util.IterableUtil.*
import static extension com.regnosys.rosetta.translate.GeneratorUtil.*
import static extension com.regnosys.rosetta.translate.synonymmap.AttributeGroup.*
import com.rosetta.util.types.generated.GeneratedJavaClass
import com.regnosys.rosetta.generator.java.RosettaJavaPackages
import com.regnosys.rosetta.generator.java.types.JavaTypeUtil
import com.rosetta.util.DottedPath

class IngesterGenerator {

	static val Logger LOGGER = Logger.getLogger(IngesterGenerator);
	public static val MetricRegistry GENERATOR_METRICS = new MetricRegistry();
	
	@Inject
	extension JavaTypeTranslator typeTranslator
	@Inject 
	JsonSchemaParser jsonSchemaParser;
	@Inject
	RBuiltinTypeService builtins

	@org.eclipse.xtend.lib.annotations.Data static class GeneratedIngesters {
		GeneratedNameAndSource generatedFactory
		List generatedHandlers
		List errors
		
		def Map classNameToDef() {
			val classes = newHashMap
			classes.put(generatedFactory.className, generatedFactory.source)
			generatedHandlers.forEach[classes.put(className, source)]
			return classes
		}
	}

	@org.eclipse.xtend.lib.annotations.Data static class ParseHandlersAndMappingErrors {
		List parseHandlers
		List errors
	}

	def GeneratedIngesters generateXml(String envName, String factoryName, List generatorParamsList) {
		val handlersAndErrors = parseHandlersAndMappingErrors(new BeansXsdParser(), generatorParamsList)

		val factory = generateXmlFactory(envName, factoryName, generatorParamsList)
		return new GeneratedIngesters(
			new GeneratedNameAndSource(envName + "." + factoryName, new GenerationResult(false, factory)),
			handlersAndErrors.parseHandlers,
			handlersAndErrors.errors
		);
	}

	def GeneratedIngesters generateJson(String envName, String factoryName, List generatorParamsList) {
		LOGGER.info("JSON model parser " + jsonSchemaParser.class.simpleName)
		val handlersAndErrors = parseHandlersAndMappingErrors(jsonSchemaParser, generatorParamsList)

		val factory = generateJsonFactory(envName, factoryName, generatorParamsList)
		return new GeneratedIngesters(
			new GeneratedNameAndSource(envName + "." + factoryName, new GenerationResult(false, factory)),
			handlersAndErrors.parseHandlers,
			handlersAndErrors.errors
		);
	}

	private def parseHandlersAndMappingErrors(ModelParser modelParser, List params) {
		val parseHandlers = newArrayList
		val errors = newArrayList
		for (param : params) {
			val parsers = generateParsers(
				modelParser,
				param.schema,
				param.rosettaEntities,
				param.sources,
				param.topLevelTags,
				param.childPackageName
			)
			parseHandlers.addAll(parsers.key.entrySet.map [
				new GeneratedNameAndSource(param.childPackageName + '.' + key + "ParseHandler", value)
			])
			errors.addAll(parsers.value)
		}
		new ParseHandlersAndMappingErrors(parseHandlers, errors)
	}

	private def Pair, Collection> generateParsers(ModelParser modelParser, URL modelUrl,
		Collection rosettaEntities, Collection sources,
		Collection topLevelTags, String childPackageName) {

		val schema = getSchema(modelParser, modelUrl, childPackageName)
		generateParsers(sources, rosettaEntities, schema, topLevelTags, childPackageName);
	}

	def Schema getSchema(ModelParser modelParser, URL modelUrl, String childPackageName) {

		val jschemaFileName = UrlUtils.getBaseFileName(modelUrl) + ".schema"
		val schemaURL = new URL(modelUrl, jschemaFileName)

		try {
			val loadStart = System.currentTimeMillis
			val deserial = new SchemaDeserialise
			val schema = deserial.deserialise(schemaURL.openStream) as Schema
			LOGGER.info("Loaded schema from file " + schemaURL + " in " + (System.currentTimeMillis - loadStart))
			return schema
		} catch (IOException | ParseException e) {
			LOGGER.info("Failed to read schema file from " + schemaURL)
		// if loading fails we fall back to generating the schema
		}

		val type = if (UrlUtils.getFileExtension(modelUrl).toLowerCase == "xsd") "XSD" else "JSON"
		LOGGER.info("Parsing " + type + " model for " + childPackageName)
		val schema = modelParser.parseModel(modelUrl)

		// This code is useful if you need to serialise a schema
		if (!schemaURL.toURI.isOpaque) {
			val schemaFile = new File(schemaURL.toURI())
			val serialise = new SchemaSerialise()
			serialise.serialiseObject(schema, new FileOutputStream(schemaFile))
		}
		schema
	}

	def Pair, Collection> generateParsers(
		Collection sources, Collection rosettaEntities, Schema schema,
		Collection topLevelTags, String childPackageName) {
		LOGGER.info("Building synonym map")

		val synonymSourceNames = sources.synonymSourceNames
		val externalSynonyms = sources.externalSynonymSources

		val mappingBuilder = new SynonymMapBuilder(synonymSourceNames, externalSynonyms, typeTranslator)
		return generateParsers(mappingBuilder, sources, rosettaEntities, schema, topLevelTags, childPackageName,
			new HashMap)
	}

	def Pair, Collection> generateParsers(SynonymMapBuilder mappingBuilder,
		Collection sources, Collection rosettaEntities, Schema schema,
		Collection topLevelTags, String childPackageName, Map synHashCache) {
		var syns = rosettaEntities.map[c|mappingBuilder.buildMap(c)].toList

		LOGGER.info("Validating synonyms")
		val combiner = new ModelCombiner
		val combineErrors = newArrayList
		syns = combiner.combineModels(schema, syns, combineErrors, topLevelTags)
		LOGGER.info("Pruning synonyms")
		syns = combiner.prune(syns);

		if (combineErrors.stream().anyMatch(e|e.getLevel() == MappingErrorLevel.ERROR)) {
			// throw new XMLIngestException("Errors combining " + combineErrors)
			// TODO fix the error in the CDM then re-enable this to  prevent new ones from creeping in
		}
		LOGGER.info("Checking cardinalities")
		combineErrors.addAll(new CardinalityChecker().checkCardinalities(syns))
		return new Pair(generateParsers(syns, sources, childPackageName, synHashCache), combineErrors)
	}

	def Map generateParsers(Collection topLevels,
		Collection sources, String childName, Map synHashCache) {
		LOGGER.debug("Creating java files");
		val Map generated = new HashMap
		for (mapping : topLevels) {
			generateParsers(mapping, generated, sources, childName, synHashCache)
		}
		return generated;
	}

	private def void generateParsers(
		SynonymMap mapping,
		Map generated,
		Collection sources,
		String childPackageName,
		Map synHashCache
	) {
		if (generated.containsKey(mapping.rosetta.name)) {
			return;
		}
		if (mapping.rosetta.isClassOrData) {
			generateClassParsers(mapping, generated, sources, childPackageName, synHashCache);
		} else if (mapping.rosetta instanceof RosettaEnumeration) {
			generateEnumParsers(mapping, generated, childPackageName, synHashCache);
		}
	}

	private def validNamespaces(RosettaModel model, String importedNamespace) {

		val validNamespacesSet = if (model.eResource.resourceSet?.resources === null) {
				LOGGER.warn("No resource set found for " + model.eResource.URI.toString)
				newHashSet
			} else
				model.eResource.resourceSet.resources.flatMap[contents].filter(RosettaModel).flatMap[it.elements].filter[
					it instanceof Data || it instanceof RosettaEnumeration
				].map[(RosettaType.cast(it))].map[it.model.name].toSet

		val namespace = importedNamespace.substring(0, indexOrLength(importedNamespace, "."))
		return validNamespacesSet.contains(namespace)
	}

	private def int indexOrLength(String toSearch, char find) {
		val index = toSearch.lastIndexOf(find)
		if (index === -1) {
			toSearch.length
		} else {
			index
		}
	}

	private def void generateClassParsers(SynonymMap mapping, Map generated,
		Collection synonymSources, String envName, Map synHashCache) {
		if (generated.containsKey(mapping.rosetta.fullname)) {
			return;
		}
		val rosettaModel = mapping.rosetta.eContainer as RosettaModel
		if (rosettaModel.eResource === null) {
			LOGGER.warn("Unable to generate translate parsers. No resource found for rosetta model: " + rosettaModel)
			return;
		}

		
		val prevHash = synHashCache.get(mapping.rosetta.fullname)
		val newHash = mapping.hashForGeneration
		if (prevHash === null || newHash != prevHash) {
			val header = '''
				package «envName».«mapping.rosetta.packageName»;
				
				import java.math.BigDecimal;
				import java.time.LocalDate;
				import java.time.LocalTime;
				import java.util.Collections;
				import java.util.ArrayList;
				import java.util.List;
				import java.util.Set;
				import java.util.Optional;
				import java.util.Map;
				import java.util.HashMap;
				import java.util.LinkedHashMap;
				import java.util.concurrent.atomic.AtomicBoolean;
				import java.util.stream.Collectors;
				
				import com.google.common.collect.ImmutableList;
				import com.google.common.collect.ImmutableMultimap;
				import com.google.inject.Injector;
				import com.regnosys.rosetta.translate.HandlerSupplier;
				import com.rosetta.model.lib.meta.Reference;
				import com.regnosys.rosetta.translate.GeneratorPathUtil;
				import com.regnosys.rosetta.translate.HandlerCache;
				import com.regnosys.rosetta.translate.IngestMerger;
				import com.regnosys.rosetta.translate.MappingProcessorSupplier;
				import com.regnosys.rosetta.translate.SynonymValueGroup;
				import com.regnosys.rosetta.translate.ROMParseHandler;
				import com.regnosys.rosetta.common.translation.Mapping;
				import com.regnosys.rosetta.common.translation.MappingContext;
				import com.regnosys.rosetta.common.translation.Path;
				import com.regnosys.rosetta.common.util.PathUtils;
				import com.regnosys.rosetta.translate.basic.*;
				import com.regnosys.rosetta.common.translation.Path.PathElement;
				«FOR importResources : rosettaModel.imports»
					«val importedNamespace = importResources.importedNamespace»
					«IF validNamespaces(rosettaModel, importedNamespace)»
						import «importedNamespace»;
					«ENDIF»
				«ENDFOR»
				import «rosettaModel.name».*;
				
				«FOR traversedImports : mapping.traversedImports»
					import «traversedImports»;
				«ENDFOR»

				import com.rosetta.model.lib.records.*;
				import com.rosetta.model.lib.RosettaModelObjectBuilder;
				import com.rosetta.model.lib.path.RosettaPath;
				import com.rosetta.model.lib.process.BuilderProcessor;
				import com.rosetta.model.metafields.MetaFields;

			'''
			val body = '''	
				public «IF (mapping.rosetta.isAbstract)»abstract «ENDIF»class «mapping.pNameW» «mapping.romExtends(envName)» {
					
					«pathConstants(mapping)»
					
					«conditionalValues(mapping)»
					
					«handlerCaches(mapping, envName)»
					
					«constructor(mapping)»
					
					// regular handlers
					«FOR h : mapping.mappedGroups.filter[group.getSynonymGroups.exists[!synonymValues.isEmpty && conditions.forall[condition.isEmpty]]].sortBy[group].sortBy[group.toHandlerMethodName] SEPARATOR "\n"»
						«handler(h, mapping, envName)»
					«ENDFOR»
					
					// conditional handlers
					«FOR h : mapping.mappedGroups.filter[group.getSynonymGroups.exists[!synonymValues.isEmpty && conditions.exists[!condition.isEmpty]]].sortBy[group] SEPARATOR "\n"»
						«condHandler(h, mapping, envName)»
					«ENDFOR»
					
					// conditional capture handlers
					«FOR ent : mapping.conditionalCaptures.asMap.entrySet.sortBy[e|e.key.toConditionalHandlerMethodName] SEPARATOR "\n"»
						«conditionalCaptureHandler(ent, mapping)»
					«ENDFOR»
					
					// regular handler getter (for nested 'set to' conditional mapping with path predicate)
					«val alreadyGenerated = newHashSet»
					«FOR p : mapping.mappedGroups
							.filter[group.getSynonymGroups.exists[synonymValues.isEmpty]] // empty synonym values
							.filter[group.getSynonymGroups.flatMap[conditions].flatMap[condition].exists[it instanceof SynonymPathTest]] // conditional path predicate
							.map[group.attributePath] // maps to attribute path (e.g. rosetta path)
							.filter[size>1] // filter to paths with multiple path elements, e.g., nested
							 // result is list of paths to each attribute used in a set-to conditional mapping with path predicate
							SEPARATOR "\n"»
						«nestedSetToHandler(p, mapping, alreadyGenerated)»
					«ENDFOR»
					
					// mappers
					«FOR h : mapping.mappedGroups.filter[group.getSynonymGroups.exists[mapperName !== null]].sortBy[group] SEPARATOR "\n"»
						«mappingProcessor(h, mapping)»
					«ENDFOR»
					
					«IF !mapping.rosetta.abstract»
						// builder proxy
						«generateBuilderProxy(mapping)»
					«ENDIF»
				}
			'''
			var parser = '''
				«header»
				
				«body»
			'''

			generated.put(mapping.rosetta.fullname, new GenerationResult(false, parser))
			synHashCache.put(mapping.rosetta.fullname, newHash);
		} else {
			generated.put(mapping.rosetta.fullname, new GenerationResult(true, null))
		}

		if (mapping.superMap !== null) {
			generateParsers(mapping.superMap, generated, synonymSources, envName, synHashCache)
		}
		for (subParser : mapping.childMappings) {
			if (subParser.rosetta !== null) {
				generateParsers(subParser, generated, synonymSources, envName, synHashCache)
			}
		}
	}

	private def getTraversedImports(SynonymMap mapping) {
		return mapping.attributeGroups
					.flatMap[attributePath]
					.filter[rosettaType?.type !== null]
					.filter[!rosettaType?.type.toExpandedType.builtInType]
					.filter[type.model !== null]
					.map[type.model.name + "." + type.name]
					.distinct
					.sort		
	}


	def dispatch boolean isAbstract(RosettaType type) {
		true
	}

	def dispatch boolean isAbstract(Data type) {
		false
	}

	def captureList(List groups) {
		val Multimap conditionals = LinkedListMultimap.create
		for (sg : groups) {
			for (sv : sg.synonymValues) {
				conditionals.putAll(sv, sg.conditions);
			}
		}
		return conditionals;
	}

	private def toPathName(SynonymValue synonymValue) {
		return synonymValue.synonymPath.map[name].join("_").toFirstLower;
	}

	private def romExtends(SynonymMap mapping, String envName) {
		if (mapping.superMap === null) {
			'''extends ROMParseHandler'''
		} else {
			'''extends «mapping.superMap.pFullNameW(envName)»'''
		}
	}

	private def pNameW(SynonymMap mapping) { pNameW(mapping.rosetta) }

	private def pFullNameT(SynonymMap mapping, String packageName) { pFullNameT(mapping.rosetta, packageName) }

	private def pNameW(RosettaType rosetta) '''«rosetta.name»ParseHandler'''
	
	private def pFullNameT(ExpandedType rosetta, String envName) '''«envName».«rosetta.model.name».«rosetta.name»'''

	private def pFullNameW(ExpandedType rosetta, String envName) { '''«pFullNameT(rosetta, envName)»ParseHandler''' }
	
	private def pFullNameW(SynonymMap mapping, String envName) { pFullNameW(mapping.rosetta, envName) }

	private def pFullNameW(RosettaType rosetta, String envName) '''«envName».«rosetta.fullname»ParseHandler'''

	private def pFullNameT(RosettaType rosetta,
		String envName) '''«envName».«rosetta.fullname»ParseHandler<«rosetta.toBuilder»>'''

	private def expandedTypeToBuilder(ExpandedType type) {
		if (type.isType) {
			'''«(type.expandedTypeToJavaType as JavaClass).toBuilderType»'''
		} else {
			'''«type.expandedTypeToJavaType»'''
		}
	}
	@Inject
	RosettaJavaPackages packages;
	@Inject
	private JavaTypeUtil typeUtil;
	private def JavaClass expandedTypeToJavaType(ExpandedType type) {
		if (type.name == METAFIELDS_CLASS_NAME || type == META_AND_TEMPLATE_FIELDS_CLASS_NAME) {
			return new GeneratedJavaClass(packages.basicMetafields(), type.name, Object);
		}
		if (type.isMetaType) {//TODO ExpandedType needs to store the underlying type for meta types if we want them to be anything other than strings
			return typeUtil.STRING;
		}
		if (type.isBuiltInType) {
			return toJavaReferenceType(builtins.getType(type.name, Collections.emptyMap()));
		}
		return new GeneratedJavaClass(modelPackage(type.model), type.name, Object);
	}
	private def DottedPath modelPackage(RosettaModel model) {
		return DottedPath.splitOnDots(model.getName());
	}
	
	private def dispatch StringConcatenationClient toBuilder(Data type) {
		if (type.name.endsWith("WithMeta")) {
			return '''«type.fullname».«type.name»Builder«type.definition.toFirstUpper»'''
		}
		'''«type.fullname».«type.name»Builder'''
	}
	private def dispatch StringConcatenationClient toBuilder(RosettaEnumeration type) '''«type.name»'''
	private def dispatch StringConcatenationClient toBuilder(RosettaBuiltinType type) '''«builtins.getType(type.name, newHashMap)»'''
	private def dispatch StringConcatenationClient toBuilder(RosettaTypeAlias type) {
		type.typeCall.type.toBuilder
	}

	private def pathConstants(SynonymMap mapping) {
		val svs = new LinkedHashSet
		svs.addAll(mapping.attributeGroups.flatMap[synonymGroups]);
		svs.addAll(mapping.conditionalCaptures.keySet.map [
			new SynonymGroup(Collections.singletonList(it), Collections.emptyList(), null, null, null, null, false)
		])
		return svs.pathConstants
	}

	private def pathConstants(Iterable sgs) {
		sgs.flatMap[synonymValues].sortBy[toPathName].map [ synonymValue |
			'''private final static ImmutableList «synonymValue.toPathName»Path = ImmutableList.of(«synonymValue.synonymPath.list»);'''
		].distinct.join("\n")
	}

	/*
	 * There are two types of temporary conditional values
	 * one where a primitive typed mapping has a condition meaning that the value may or may not be used
	 * the other where there is a path in a when clause and we need to capture the value of that path
	 */
	private def conditionalValues(SynonymMap mapping) '''
		«val groups = mapping.attributeGroups.sortBy[toAttPathName]»
		«««These Values hold primitive values that might be set on the rosetta object dependign on a when clase
		«««required where there is an input path in the SynonymValues and there is a conditional clause
		«FOR group : groups.filter[synonymGroups.exists[!synonymValues.isEmpty && conditions.exists[!condition.isEmpty]]]»
			protected Map «group.toAttPathName»Values = new LinkedHashMap<>();
		«ENDFOR»
		«««These Values hold primitive values are going to be tested in when clauses to see if a rosetta field should be set
		«FOR e : mapping.conditionalCaptures.asMap.entrySet.sortBy[key.toPathName]»
			«IF e.key.synonymPath.exists[cardinality === Cardinality.MULTIPLE]»
				protected Map «toPathName(e.key)»Values = new HashMap<>();
			«ELSE»
				protected String «toPathName(e.key)»Value;
			«ENDIF»
		«ENDFOR»
	'''

	private def handlerCaches(SynonymMap mapping, String packageName) '''
		// basic
		«basicTypeCaches(mapping)»
		// rosetta
		«rosettaTypeCaches(mapping, packageName)»
	'''

	private def basicTypeCaches(SynonymMap mapping) {
		val attrPathSynGroups = mapping.mappedGroups.filter[!(mappings.rosetta.isClassOrData)].map[group]
		val chopped = attrPathSynGroups.filter[attributePath.size() > 1].groupBy[toFirstMultiple.toAttPathName]
		handlerCache(chopped)
	}

	private def toFirstMultiple(AttributeGroup group) {
		group.attributePath.toFirstMultiple
	}

	/**
	 * @param atts - list of attributes represents a path (with attribute name/type for each path element).
	 * @returns the first attribute that has multiple cardinality
	 */
	private def toFirstMultiple(Iterable atts) {
		val result = newArrayList
		var multiple = false;
		var i = 0;
		while (!multiple && i <= atts.size - 2) {
			val att = atts.get(i++);
			result.add(att)
			multiple = att.multiple;
		}
		result
	}

	private def handlerCache(Map> attributeGroupMap) '''
		«FOR attrPathSynGroups : attributeGroupMap.entrySet.sortBy[value.sortBy[toAttPathName()].last.toAttPathName()]»
			«IF attrPathSynGroups.value.map[removeLast].exists[multipleCardinality]»
				protected HandlerCache<«attrPathSynGroups.value.last.toFirstMultiple.last.type.expandedTypeToBuilder»> «attrPathSynGroups.key»Underlyers =
					new HandlerCache<>(ImmutableMultimap.builder()
							«FOR attrPathSynGroup : attrPathSynGroups.value.sort»
								«FOR sg:attrPathSynGroup.synonymGroups.sortBy[toSVG]»
									.put("«attrPathSynGroup.attributePath.last.attributeName»", new SynonymValueGroup(«toSVG(sg)»))
								«ENDFOR»
							«ENDFOR»
							.build());
			«ELSE»
				protected AnonHandler<«attrPathSynGroups.value.last.attributePath.removeLast.last.type.expandedTypeToBuilder»> «attrPathSynGroups.value.last.removeLast.toAttPathName»UnderlyerB;
			«ENDIF»
		«ENDFOR»
	'''

	private def rosettaTypeCaches(SynonymMap mapping, String packageName) '''
		«FOR mappedGroup : mapping.mappedGroups.filter[mappings.rosetta.isClassOrData].sortBy[group.toAttPathName]»
			«IF mappedGroup.group.multipleCardinality»
				protected HandlerCache<«mappedGroup.group.attributePath.last.type.expandedTypeToBuilder»> «mappedGroup.group.toHandlerFieldName» =
						new HandlerCache<>(ImmutableMultimap.builder()
								«FOR sg:mappedGroup.group.synonymGroups.sortBy[toSVG]»
									.put("«mappedGroup.group.attributePath.last.attributeName»", new SynonymValueGroup(«toSVG(sg)»))
								«ENDFOR»
								.build());
			«ELSE»
				«IF mappedGroup.group.synonymGroups.exists[conditions.isEmpty]»
					protected «mappedGroup.mappings.pFullNameT(packageName)» «mappedGroup.group.toHandlerFieldName»;
				«ENDIF»	
			«ENDIF»
		«ENDFOR»
	'''

	private def toSVG(SynonymGroup group) {
		if (group.synonymValues.empty) {
			val pathExprs = group.conditions.map[condition].flatten.filter(SynonymPathTest).map[getPathWithDots]

			val binExprs = group.conditions.map[condition].flatten.filter(SynonymBinaryTest).map[paths].flatten.map [ p |
				p.synonymPath.map[name].join(".")
			]

			return newArrayList(pathExprs, binExprs).flatten.toSet.join("\"", "\", \"", "\"", [it])
		} else {
			return group.synonymValues.map[synonymPath.toFirstLeaf].join("\"", "\", \"", "\"", [ p |
				p.map[e|e.name].join(".")
			])
		}
	}

	private def CharSequence toHandlerFieldName(AttributeGroup attrPathSynGroup) {
		var handlerSuffix = if(attrPathSynGroup.multipleCardinality) 'Handlers' else 'Handler'
		return attrPathSynGroup.toAttPathName + handlerSuffix
	}

	/**
	 * Simple handlers require one handler method per attribute
	 */
	private def String toHandlerMethodName(AttributeGroup attrPathSynGroup) {
		return attrPathSynGroup.toAttPathName + 'Handler'
	}

	private def CharSequence toCondHandlerMethodName(AttributeGroup attrPathSynGroup) {
		return attrPathSynGroup.toAttPathName + 'ConditionalHandler'
	}

	/**
	 * Conditional handlers require one handler method per synonym path (each attribute can have multiple synonym paths)
	 */
	private def String toConditionalHandlerMethodName(SynonymValue synonymValue) {
		return synonymValue.toPathName + 'ConditionalCaptureHandler'
	}

	private def CharSequence toMappingProcessorMethodName(AttributeGroup attrPathSynGroup, SynonymGroup group) {
		return attrPathSynGroup.toAttPathName + group.synonymValues.flatMap[synonymPath].map[name].join.toFirstUpper +
			'MappingProcessor'
	}

	private def toSynonymLastElementName(SynonymGroup group) {
		group.synonymValues.map[synonymPath].map[it.map[name].last].toSet
	}

	private def toFirstLeaf(List elements) {
		var result = newArrayList
		var isLeaf = false;
		var i = 0;
		while (!isLeaf && i < elements.size()) {
			val e = elements.get(i++)
			result.add(e)
			isLeaf = false // e?.entity?.isLeaf
		}
		result
	}

	private def multipleCardinality(AttributeGroup attrPathSynGroup) {
		return attrPathSynGroup.attributePath.exists[cardinalityIsListValue]
	}

	private def multipleCardinality(List attributePath) {
		return attributePath.exists[cardinalityIsListValue]
	}
	
	private def getterMultipleCardinality(List attributePath) {
		return attributePath.removeLast.exists[cardinalityIsListValue]
	}

	private def toAttPathName(AttributeGroup attrPathSynGroup) {
		attrPathSynGroup.attributePath.toAttPathName
	}

	private def toAttPathName(List attributePath) {
		attributePath.map[s|s.attributeName].join("").toFirstLower
	}

	private def list(List path) '''«FOR string : path.map[s|s.name] SEPARATOR ", "»"«string»"«ENDFOR»'''

	private def  constructor(SynonymMap mapping) '''
		«IF (!mapping.rosetta.abstract)»@SuppressWarnings("unchecked")«ENDIF»
		public «mapping.pNameW()»(Injector injector, MappingContext mappingContext) {
			super(injector, mappingContext);
			«IF (!mapping.rosetta.abstract)»setUnderlying((T)new BuilderProxy());«ENDIF»
			handlers = ImmutableMultimap., HandlerSupplier>builder()
				«IF mapping.superMap!==null»
					.putAll(super.handlers)
				«ENDIF»
				«««There are three types of handler: 
				«««1. normal unconditional handler - the input is used to create the output
				«««2. conditional handler - the input may or may not be used for the output depending on the evalutation of a condition
				«««3. conditionCapture - the input is captured to be used as part of a condition
				
				// unconditional handlers
				«FOR path : mapping.unconditionalHandlerPaths»
					.put(«path.synonymPath»Path, this::«path.attributePath»Handler_«path.index»)
				«ENDFOR»
				// conditional handlers
				«FOR path : mapping.conditionalHandlerPaths»
					.put(«path.synonymPath»Path, this::«path.attributePath»ConditionalHandler)
				«ENDFOR»
				// conditional captures
				«FOR conditional : condCaptures(mapping).distinct.sort»
					«conditional»
				«ENDFOR»
				.build();
		
			mappingProcessors = ImmutableMultimap., MappingProcessorSupplier>builder()
				«IF mapping.superMap!==null»
					.putAll(super.mappingProcessors)
				«ENDIF»
				«FOR attrPathSynGroup : mapping.attributeGroups.filter[synonymGroups.exists[mapperName !== null]].sort»
					«FOR mapperSynGroup : attrPathSynGroup.synonymGroups.filter[mapperName !== null]»
						«FOR synonymValue : mapperSynGroup.synonymValues.distinctBy[toPathName].sortBy[toPathName]»
							.put(«synonymValue.toPathName»Path, this::«attrPathSynGroup.toMappingProcessorMethodName(mapperSynGroup)»)
						«ENDFOR»
					«ENDFOR»
				«ENDFOR»
				.build();
		}
	'''

	private def unconditionalHandlerPaths(SynonymMap mapping) {
		val paths = newArrayList
		for (attrPathSynGroup : mapping.attributeGroups.filter[synonymGroups.exists[conditions.forall[condition.isEmpty]]].sort) {
			attrPathSynGroup.synonymGroups.forEach[sg, i|
				val index = {
					val last = attrPathSynGroup.attributePath.last
					if (last.type.isType || last.type.name == "RosettaReference")
						0
					else
						i	
				}
				if (!sg.synonymValues.isEmpty && sg.conditions.forall[condition.isEmpty]) {
					sg.synonymValues.distinctBy[toPathName].sortBy[toPathName].forEach[sv | {
						paths.add(new SynonymAttributePath(sv.toPathName, attrPathSynGroup.toAttPathName, index))
					}]
				}
				
			]
		}
		return paths.sort
	}

	private def conditionalHandlerPaths(SynonymMap mapping) {
		val paths = newArrayList
		for (attrPathSynGroup : mapping.attributeGroups.filter[synonymGroups.exists[conditions.exists[!condition.isEmpty]]].sort) {
			for (synonymValue : attrPathSynGroup.synonymGroups
				.filter[!synonymValues.isEmpty && conditions.exists[!condition.isEmpty]]
				.flatMap[sg|sg.synonymValues]
				.distinctBy[toPathName]
				.sortBy[toPathName]) {
				paths.add(new SynonymAttributePath(synonymValue.toPathName, attrPathSynGroup.toAttPathName, 0))
			}
		}
		return paths.sort
	}

	private def condCaptures(SynonymMap mapping) {
		mapping.conditionalCaptures.keySet.map[toPathName()].map [ name |
			'''.put(«name»Path, this::«name»ConditionalCaptureHandler)'''
		]
	}

	private def handler(AttributeGroupMapping mappedGroup, SynonymMap mapping, String packageName) {
		val last = mappedGroup.group.attributePath.last

		if (last.type.isType || last.type.name == "RosettaReference")
			romHandler(mappedGroup, mapping, packageName)
		else {
			val handlers = newArrayList
			mappedGroup.group.synonymGroups.forEach[sg, i|
			if (last.type.enumeration)
				handlers+=basicHandler(mappedGroup, mapping, last.type.pFullNameT(packageName), i)
			else
				handlers+=basicHandler(mappedGroup, mapping, last.type.name.toFirstUpper, i)
			]
			return handlers.join("\n");
		}
	}

	private def boolean isClassOrData(RosettaType t) {
		t instanceof Data
	}

	private def nestedSetToHandler(List attributePath, SynonymMap mapping, Set alreadyGenerated) {
		val multiple = attributePath.take(attributePath.size-1).exists[cardinalityIsListValue]
		val underlyerAttrName = attributePath.removeLast.toAttPathName + "UnderlyerB"
		if (!multiple && alreadyGenerated.add(underlyerAttrName)) {
			val underlyerType = firstMultipleBuilder(attributePath, mapping)
			'''
				protected AnonHandler<«underlyerType»> get«underlyerAttrName.toFirstUpper»() {
					// nested (set-to conditional mapping with path predicate) anon handler
					return «underlyerAttrName» != null ?
						«underlyerAttrName» :
						useOrNew(«underlyerAttrName»,
							() -> {
								«underlyerType» chainBuilder;
								«builderChain(attributePath, mapping)»
								return new AnonHandler<>(injector, mappingContext, rosettaPath, chainBuilder);
							});
				}

			'''
		}
	}
	
	private def condHandler(AttributeGroupMapping mappedGroup, SynonymMap mapping, String envName) {
		val t = mappedGroup.group.attributePath.last.type

		if (t.isType)
			romCondHandler(mappedGroup, mapping, envName)
		else if (t.enumeration)
			enumCondHandler(mappedGroup, mapping, t, envName)
		else
			basicCondHandler(mappedGroup, mapping, t.name.toFirstUpper)
	}

	private def builderChain(List attributePath, SynonymMap mapping) '''
		Path rosettaPath = getRosettaPath();
		«var attList = attributePath.toFirstMultiple»
		«var lastBuilderName = "getUnderlying()"»
		«var nextBuilderName =""»
		«FOR att : attList»
			«IF (att.cardinalityIsListValue)»
				int «att.name»Index = getMappingListIndex(«lastBuilderName».get«att.name.toFirstUpper»(), «mergeSynonyms(attributePath, mapping).asList»);
				«att.type.expandedTypeToBuilder» «nextBuilderName=att.type.name.toFirstLower+"Builder"» = «lastBuilderName».getOrCreate«att.name.toFirstUpper»(«att.name»Index);
				rosettaPath = rosettaPath.addElement(new PathElement("«att.name»", «att.name»Index));
			«ELSE»
				«att.type.expandedTypeToBuilder» «nextBuilderName = att.type.name.toFirstLower+"Builder"» = «lastBuilderName».getOrCreate«att.name.toFirstUpper»();
				rosettaPath = rosettaPath.addElement(new PathElement("«att.name»"));
			«ENDIF»
			«{lastBuilderName = nextBuilderName;""}»
		«ENDFOR»
		chainBuilder = («firstMultipleBuilder(attributePath, mapping)»)«lastBuilderName»;
	'''
	
	/**
	 * Find merge synonyms for the given model path.
	 */
	def mergeSynonyms(List attributePath, SynonymMap mapping) {
		mapping.mappedGroups
			.filter[it.group.attributePath === attributePath]
			.flatMap[it.mappings.mergeSynonyms.values]
			.toList
	}
	
	/**
	 * Write merge synonym values out as an list of MergeSynonymValue
	 */
	def asList(List mergeSyns) {
		'''java.util.Arrays.asList(«mergeSyns.map['''new com.regnosys.rosetta.translate.MergeSynonymValue("«it.name»", "«it.excludePath»")'''].join(",")»)'''
	}

	def postAnon(AttributeGroup group, SynonymMap mapping) {
		val afterMultiple = postFirstMultiple(group);
		var lastBuilderName = "foundbuilder.getUnderlying()"
		var String nextBuilderName = null
		'''
			«FOR att : afterMultiple.removeLast»
				«IF (att.cardinalityIsListValue)»
					int «att.name»Index = sizeOf(«lastBuilderName».get«att.name.toFirstUpper»());
					«att.type.expandedTypeToBuilder» «nextBuilderName=att.type.name.toFirstLower+"Builder"» = «lastBuilderName».getOrCreate«att.name.toFirstUpper»(«att.name»Index);
					rosettaPath = rosettaPath.addElement(new PathElement("«att.name»", «att.name»Index));
				«ELSE»
					«att.type.expandedTypeToBuilder» «nextBuilderName = att.type.name.toFirstLower+"Builder"» = «lastBuilderName».getOrCreate«att.name.toFirstUpper»();
					rosettaPath = rosettaPath.addElement(new PathElement("«att.name»"));
				«ENDIF»
				«{lastBuilderName = nextBuilderName;""}»
			«ENDFOR»
			«penultimateBuilder(group.attributePath, mapping)» builder = «lastBuilderName»;
		'''
	}

	protected def List postFirstMultiple(AttributeGroup group) {
		group.attributePath.subList(group.toFirstMultiple.size, group.attributePath.size)
	}

	protected def List postFirstMultiple(List attributePath) {
		attributePath.subList(attributePath.toFirstMultiple.size, attributePath.size)
	}

	private def conditionalCaptureHandler(Map.Entry> sv, SynonymMap mapping) '''
		private StringParseHandler «sv.key.toConditionalHandlerMethodName»(Path localPath, Path parentPath) {
			«IF sv.key.synonymPath.exists[e | e.entity !== null && e.cardinality === Cardinality.MULTIPLE]»
				«IF sv.value.forall[c|c.condition.forall[c2|c2 instanceof SynonymExistsTest || c2 instanceof SynonymAbsentTest]]»
					«sv.key.toPathName»Values.put(localPath, "Exists");
					StringParseHandler res = new StringParseHandler(true, true);
					res.setParentSetter(a -> {});
					res.setParentSupplier(() -> Optional.empty());
					return res;
				«ELSE»
					StringParseHandler handler = new StringParseHandler(true, true);
					useConditionalCache(handler, localPath, «sv.key.toPathName»Values);
					return handler;
				«ENDIF»
			«ELSE»
				«IF sv.value.forall[c|c.condition.forall[c2|c2 instanceof SynonymExistsTest || c2 instanceof SynonymAbsentTest]]»
					«sv.key.toPathName»Value = "Exists";
					StringParseHandler res = new StringParseHandler(true, true);
					res.setParentSetter(a -> {});
					res.setParentSupplier(() -> Optional.empty());
					return res;
				«ELSE»
					StringParseHandler handler = new StringParseHandler(true, true);
					handler.setParentSetter(val -> «sv.key.toPathName»Value = val);
					handler.setParentSupplier(() -> Optional.ofNullable(«sv.key.toPathName»Value));
					return handler;
				«ENDIF»
			«ENDIF»
		}
	'''

	private def basicHandler(AttributeGroupMapping mappedGroup, SynonymMap mapping, CharSequence type, int index) {
		val attrPathSynGroup = mappedGroup.group
		val lastAttribute = attrPathSynGroup.attributePath.last
		'''
			private «type»ParseHandler «attrPathSynGroup.toHandlerMethodName»_«index»(Path localPath, Path parentPath) {
				Path xmlPath = parentPath.addElement(localPath.getLastElement());
				«generateAnonHandler(attrPathSynGroup, mapping, lastAttribute, "localPath", "xmlPath")»
				«type»ParseHandler handler = new «basicHandlerConstructor(type, attrPathSynGroup.synonymGroups.get(index))»
				handler.addXmlPath(xmlPath);
				«IF lastAttribute.cardinalityIsSingleValue»
					rosettaPath = rosettaPath.addElement(new PathElement("«lastAttribute.name»"));
					handler.setParentSetter(val -> builder.set«lastAttribute.attributeName.toFirstUpper»(val));
					handler.setParentSupplier(() -> «supplier(mappedGroup.group.attributePath)»
					);
				«ELSE»
					int index = sizeOf(builder.get«lastAttribute.name.toFirstUpper»());
					rosettaPath = rosettaPath.addElement(new PathElement("«lastAttribute.name»", index));
					handler.setParentSetter(val -> builder.add«lastAttribute.name.toFirstUpper»(val));
					handler.setParentSupplier(() -> Optional.empty());
				«ENDIF»
				handler.setRosettaPath(rosettaPath);
				return handler;
			}
		'''
	}
	
	def basicHandlerConstructor(CharSequence type, SynonymGroup sg) {
		basicHandlerConstructor(
			type,
			sg.synonymValues.map[mapsTo].max(0)>1,
			sg.removeHtml,
			sg.formatString,
			sg.patternMatcher,
			sg.patternReplace
		)
	}
	
	def basicHandlerConstructor(CharSequence type, List synonymGroups) {		
		basicHandlerConstructor(
			type,
			synonymGroups.flatMap[synonymValues].map[mapsTo].max(0)>1,
			synonymGroups.map[removeHtml].findFirst[true],
			synonymGroups.map[formatString].findFirst[true],
			synonymGroups.map[patternMatcher].findFirst[true],
			synonymGroups.map[patternReplace].findFirst[true]
		)
	}
	
	def basicHandlerConstructor(CharSequence type, boolean hasMapsTo, boolean removeHtml, String formatString, String patternMatcher, String patternReplace) {	
		switch (type) {
			case "Date",
			case "DateTime",
			case "Time",
			case "ZonedDateTime":
				'''«type»ParseHandler(«hasMapsTo», false, «removeHtml»«IF formatString!==null», "«formatString»"«ENDIF»);'''
				
			default :
				'''«type»ParseHandler(«hasMapsTo», false, «removeHtml»«IF patternMatcher!==null», "«patternMatcher»", "«patternReplace»" «ENDIF»);'''
		}
	}
	
	def enumHandlerConstructor(ExpandedType type, List synonymGroups, String envName) {		
		enumHandlerConstructor(
			type,
			synonymGroups.flatMap[synonymValues].map[mapsTo].max(0)>1,
			synonymGroups.map[removeHtml].findFirst[true],
			synonymGroups.map[formatString].findFirst[true],
			synonymGroups.map[patternMatcher].findFirst[true],
			synonymGroups.map[patternReplace].findFirst[true],
			envName
		)
	}
	
	def enumHandlerConstructor(ExpandedType type, boolean hasMapsTo, boolean removeHtml, String formatString, String patternMatcher, String patternReplace, String envName)
		'''«type.pFullNameW(envName)»(«hasMapsTo», false, «removeHtml»«IF patternMatcher!==null», "«patternMatcher»", "«patternReplace»" «ENDIF»);'''

	
	private def > T max(Iterable iterable, T defaultVal)	{
		if (iterable.isEmpty) defaultVal
		else iterable.max
	}

	private def enumCondHandler(AttributeGroupMapping mappedGroup, SynonymMap mapping, ExpandedType type, String envName) '''
		// enum condition handler
		«val attrPathSynGroup = mappedGroup.group»
		private «type.pFullNameW(envName)» «attrPathSynGroup.toCondHandlerMethodName»(Path localPath, Path parentPath) {
			Path xmlPath = parentPath.addElement(localPath.getLastElement());
			«type.pFullNameW(envName)» handler = new «enumHandlerConstructor(type, attrPathSynGroup.synonymGroups, envName)»
			handler.addXmlPath(xmlPath);
			Path rosettaPath = getRosettaPath();
			«FOR att : mappedGroup.group.attributePath»
				rosettaPath = rosettaPath.addElement(new PathElement("«att.name»"));
			«ENDFOR»
			useConditionalCache(handler, localPath, «mappedGroup.group.toAttPathName»Values);
			handler.setRosettaPath(rosettaPath);
			return handler;
		}
	'''
	
	private def basicCondHandler(AttributeGroupMapping mappedGroup, SynonymMap mapping, String type) '''
		// basic type condition handler
		«val attrPathSynGroup = mappedGroup.group»
		private «type»ParseHandler «attrPathSynGroup.toCondHandlerMethodName»(Path localPath, Path parentPath) {
			Path xmlPath = parentPath.addElement(localPath.getLastElement());
			«type»ParseHandler handler = new «basicHandlerConstructor(type, attrPathSynGroup.synonymGroups)»
			handler.addXmlPath(xmlPath);
			Path rosettaPath = getRosettaPath();
			«FOR att : mappedGroup.group.attributePath»
				rosettaPath = rosettaPath.addElement(new PathElement("«att.name»"));
			«ENDFOR»
			useConditionalCache(handler, localPath, «mappedGroup.group.toAttPathName»Values);
			handler.setRosettaPath(rosettaPath);
			return handler;
		}
	'''

	private def generateAnonHandler(AttributeGroup group, SynonymMap mapping, ExpandedAttribute last, String localPathVar, String xmlPathVar) '''
		// anon handler
		«IF group.attributePath.size>1» 
			«IF group.attributePath.take(group.attributePath.size-1).exists[cardinalityIsListValue]»
				AnonHandler<«firstMultipleBuilder(group.attributePath, mapping)»> foundbuilder = findOrNew(«group.toFirstMultiple.toAttPathName»Underlyers, "«last.name»", «localPathVar»,
			«ELSE»
				AnonHandler<«firstMultipleBuilder(group.attributePath, mapping)»> foundbuilder = «group.removeLast.toAttPathName»UnderlyerB = useOrNew(«group.removeLast.toAttPathName»UnderlyerB,
			«ENDIF»
			() -> {
				«firstMultipleBuilder(group.attributePath, mapping)» chainBuilder;
				«builderChain(group.attributePath, mapping)»
				AnonHandler<«firstMultipleBuilder(group.attributePath, mapping)»> result = new AnonHandler<>(injector, mappingContext, rosettaPath, chainBuilder);
				result.addXmlPath(«xmlPathVar»);
				return result;
			});
			Path rosettaPath = foundbuilder.getRosettaPath();
			«postAnon(group, mapping)»
		«ELSE»
			Path rosettaPath = getRosettaPath();
			«mapping.rosetta.pNameW» foundbuilder = «mapping.rosetta.pNameW».this;
			«postAnon(group, mapping)»
		«ENDIF»
	'''

	private def mappingProcessor(AttributeGroupMapping mappedGroup, SynonymMap mapping) {
		val attrPathSynGroup = mappedGroup.group
		val mapperSynGroups = attrPathSynGroup.synonymGroups.filter[mapperName !== null].distinct

		'''
			«FOR mapperSynGroup : mapperSynGroups»
				«val mapperClassName = mapperSynGroup.mapperName + "MappingProcessor"»
				private Map.Entry «attrPathSynGroup.toMappingProcessorMethodName(mapperSynGroup)»(Path parentPath, MappingContext mappingContext) {
					«builderChainForMapper(attrPathSynGroup.attributePath.removeLastReference, mapping)»
					RosettaPath modelPath = PathUtils.toRosettaPath(rosettaPath);
					List synonymPaths = java.util.stream.Stream.of(«mapperSynGroup.toSynonymLastElementName.join("\"", "\", \"", "\"", [it])»)
						.map(Path::parse)
						.map(parentPath::append)
						.collect(Collectors.toList());
					return new java.util.AbstractMap.SimpleEntry<>(modelPath, new «mapperClassName»(modelPath, synonymPaths, mappingContext));
				}
				
			«ENDFOR»
		'''
	}
	
	private def builderChainForMapper(List attributePath, SynonymMap mapping) '''
		Path rosettaPath = getRosettaPath();
		«var attList = attributePath.removeLast»
		«var lastBuilderName = "getUnderlying()"»
		«var nextBuilderName =""»
		«FOR att : attList»
			«IF (att.cardinalityIsListValue)»
				int «att.name»Index = Math.max(0, getMappingListIndex(«lastBuilderName».get«att.name.toFirstUpper»(), «mergeSynonyms(attributePath, mapping).asList») - 1);
				«att.type.expandedTypeToBuilder» «nextBuilderName=att.type.name.toFirstLower+"Builder"» = «lastBuilderName».getOrCreate«att.name.toFirstUpper»(«att.name»Index);
				rosettaPath = rosettaPath.addElement(new PathElement("«att.name»", «att.name»Index));
			«ELSE»
				«att.type.expandedTypeToBuilder» «nextBuilderName = att.type.name.toFirstLower+"Builder"» = «lastBuilderName».getOrCreate«att.name.toFirstUpper»();
				rosettaPath = rosettaPath.addElement(new PathElement("«att.name»"));
			«ENDIF»
			«{lastBuilderName = nextBuilderName;""}»
		«ENDFOR»
		rosettaPath = rosettaPath.addElement(new PathElement("«attributePath.last.name»"));
	'''

	def getRemoveLastReference(List attributes) {
		if (attributes.isParentMeta) {
			return attributes.removeLast
		}
		return attributes
	}

	def isMeta(ExpandedAttribute attribute) {
		val clazz = attribute?.type?.toJavaClass
		if (clazz === null) {
			return false
		} 
		return FieldWithMeta.isAssignableFrom(clazz)
	}

	def isParentMeta(List attributes) {
		attributes.parentAttribute?.isMeta
	}

	def getParentAttribute(List attributes) {
		attributes.size > 1 ? attributes.get(attributes.size - 2) : null
	}


	def toJavaClass(ExpandedType expandedType) {
		val packageName = expandedType.model !== null ? expandedType.model.name + "." : ""
		val className = packageName + expandedType.name
		Thread.currentThread.contextClassLoader.loadClass(className)
	}

	private def supplier(List atts) {
		if (atts.exists[cardinalityIsListValue]) {
			'''Optional.empty()''' // if there is multi-cardinality on the path then a new value will be created every time - the supplier of the existing value should be null
		} else {
			'''
				Optional.of(getUnderlying())
						«FOR name : atts.map[AttributeGroup.attributeName(it)]»
							.map(bu -> bu.get«name.toFirstUpper»())
						«ENDFOR»
			'''
		}
	}

	private def romHandler(AttributeGroupMapping mappedGroup, SynonymMap mapping, String packageName) {
		val last = mappedGroup.group.attributePath.last
		if (mappedGroup.group.attributePath.exists[cardinalityIsListValue]) {
			romHandlerList(mappedGroup, mapping, last, packageName)
		} else {
			romHandlerSingle(mappedGroup, mapping, last, packageName)
		}
	}

	private def romCondHandler(AttributeGroupMapping mappedGroup, SynonymMap mapping, String packageName) {
		val last = mappedGroup.group.attributePath.last
		if (mappedGroup.group.attributePath.exists[cardinalityIsListValue]) {
			romCondHandlerList(mappedGroup, mapping, last, packageName)
		} else {
			romCondHandlerSingle(mappedGroup, mapping, packageName)
		}
	}

	private def romHandlerSingle(AttributeGroupMapping mappedGroup, SynonymMap mapping, ExpandedAttribute last,
		String packageName) '''
		private «mappedGroup.mappings.pFullNameT(packageName)» «mappedGroup.group.toHandlerMethodName»_0(Path localPath, Path parentPath) {
			Path xmlPath = parentPath.addElement(localPath.getLastElement());
			«mappedGroup.group.toAttPathName»Handler = useOrNew(«mappedGroup.group.toHandlerFieldName», 
					«romSupllier(mappedGroup, mapping, last, packageName)»
			return «mappedGroup.group.toHandlerFieldName»;
		}
	'''

	private def romCondHandlerSingle(AttributeGroupMapping mappedGroup, SynonymMap mapping, String packageName) '''
		private «mappedGroup.mappings.pFullNameT(packageName)» «mappedGroup.group.toCondHandlerMethodName»(Path localPath, Path parentPath) {
			Path xmlPath = parentPath.addElement(localPath.getLastElement());
			Path rosettaPath = getRosettaPath();
			«FOR att : mappedGroup.group.attributePath»
				rosettaPath = rosettaPath.addElement(new PathElement("«att.name»"));
			«ENDFOR»
			«mappedGroup.mappings.pFullNameT(packageName)» handler = new «mappedGroup.mappings.pFullNameT(packageName)»(injector, mappingContext);
			handler.setRosettaPath(rosettaPath);
			handler.setUnderlying(«mappedGroup.group.toAttPathName»Values.computeIfAbsent(localPath, a -> handler.getUnderlying()));
			return handler;
		}
	'''

	private def romHandlerList(AttributeGroupMapping mappedGroup, SynonymMap mapping, ExpandedAttribute last,
		String packageName) '''
		private «mappedGroup.mappings.pFullNameT(packageName)» «mappedGroup.group.toHandlerMethodName»_0(Path localPath, Path parentPath) {
			Path xmlPath = parentPath.addElement(localPath.getLastElement());
			«mappedGroup.mappings.pFullNameT(packageName)» handler = findOrNew(«mappedGroup.group.toHandlerFieldName», "«last.name»", localPath,
					«romSupllier(mappedGroup, mapping, last, packageName)»
			return handler;
		}
	'''

	private def romCondHandlerList(AttributeGroupMapping mappedGroup, SynonymMap mapping, ExpandedAttribute last,
		String packageName) '''
		private «mappedGroup.mappings.pFullNameT(packageName)» «mappedGroup.group.toCondHandlerMethodName»(Path localPath, Path parentPath) {
			Path xmlPath = parentPath.addElement(localPath.getLastElement());
			«mappedGroup.mappings.pFullNameT(packageName)» handler = findOrNew(«mappedGroup.group.toHandlerFieldName», "«last.name»", localPath,
				() -> {
					Path rosettaPath = getRosettaPath();
					«mappedGroup.mappings.pFullNameT(packageName)» result = new «mappedGroup.mappings.pFullNameW(packageName)»<>(injector, mappingContext);
					result.setUnderlying(«mappedGroup.group.toAttPathName»Values.computeIfAbsent(localPath, a -> «mappedGroup.mappings.rosetta.name».builder()));
					«var cardUsed=false»
					// Model index is guessed at the number of conditional options, e.g., if there's 1 option, then the index would be 0
					int index = «mappedGroup.group.toAttPathName»Values.keySet().size() - 1;
					«FOR att : mappedGroup.group.attributePath»
						«IF att.cardinalityIsListValue && !cardUsed»
							rosettaPath = rosettaPath.addElement(new PathElement("«{cardUsed=true;att.name}»", index));
						«ELSE»
							rosettaPath = rosettaPath.addElement(new PathElement("«att.name»"));
						«ENDIF»
					«ENDFOR»
					result.addXmlPath(xmlPath);
					result.setRosettaPath(rosettaPath);
					return result;
				});
			return handler;
		}
	'''

	private def CharSequence romSupllier(AttributeGroupMapping mappedGroup, SynonymMap mapping, ExpandedAttribute last,
		String packageName) '''
		()-> {«firstMultipleBuilder(mappedGroup.group.attributePath, mapping)» chainBuilder;
				«builderChain(mappedGroup.group.attributePath, mapping)»
				«penultimateBuilder(mappedGroup.group.attributePath, mapping)» penultimateBuilder;
				«var lastBuilderName = "chainBuilder"»
				«var nextBuilderName =""»
				«««FOR att:mappedGroup.group.postFirstMultiple»
				«FOR att : mappedGroup.group.attributePath.subList(mappedGroup.group.toFirstMultiple.size, mappedGroup.group.attributePath.size-1)»
					«IF (att.cardinalityIsListValue)»
						int «att.name»Index = sizeOf(«lastBuilderName».get«att.name.toFirstUpper»());
						«att.type.expandedTypeToBuilder» «nextBuilderName=att.type.name.toFirstLower+"Builder"» = «lastBuilderName».getOrCreate«att.name.toFirstUpper»(«att.name»Index);
						rosettaPath = rosettaPath.addElement(new PathElement("«att.name»", «att.name»Index));
					«ELSE»
						«att.type.expandedTypeToBuilder» «nextBuilderName = att.type.name.toFirstLower+"Builder"» = «lastBuilderName».getOrCreate«att.name.toFirstUpper»();
						rosettaPath = rosettaPath.addElement(new PathElement("«att.name»"));
					«ENDIF»
					//«lastBuilderName = nextBuilderName»
				«ENDFOR»
				penultimateBuilder = «lastBuilderName»;
				«mappedGroup.mappings.pFullNameT(packageName)» result = new «mappedGroup.mappings.pFullNameT(packageName)»(injector, mappingContext);
				«IF last.cardinalityIsListValue»
					rosettaPath = rosettaPath.addElement(new PathElement("«last.name»", sizeOf(penultimateBuilder.get«last.name.toFirstUpper»())));
					penultimateBuilder.add«last.name.toFirstUpper»(result.getUnderlying());
				«ELSE»
					rosettaPath = rosettaPath.addElement(new PathElement("«last.name»"));
					penultimateBuilder.set«last.name.toFirstUpper»(result.getUnderlying());
				«ENDIF»
				result.addXmlPath(xmlPath);
				result.setRosettaPath(rosettaPath);
				return result;
			});
	'''

	private def generateBuilderProxy(SynonymMap mapping) {
		val conditionedPaths = mapping.attributeGroups.filter[synonymGroups.exists[!conditions.isEmpty]].toList
		if (mapping.superMap !== null) {
			conditionedPaths.addAll(mapping.superMap.attributeGroups.filter[synonymGroups.exists[!conditions.isEmpty]])
		}
		return generateBuilderProxy(mapping, conditionedPaths);
	}

	private def generateBuilderProxy(SynonymMap mapping, List conditionedPaths) '''
		private class BuilderProxy extends «mapping.rosetta.toBuilder»Impl {
			
			AtomicBoolean evaluatedConditions = new AtomicBoolean();
						
			@Override
			public «mapping.rosetta.fullname» build() {
				evaluateConditions();
				if (super.hasData()) {
					return super.build();
				}
				return null;
			}
			
			@Override
			public boolean hasData() {
				evaluateConditions();
				return super.hasData();
			}
			
			@Override
			public void process(RosettaPath path, BuilderProcessor processor) {
				evaluateConditions();
				super.process(path, processor);
			}
			
			«buildEvaluateConditions(mapping, conditionedPaths)»
		}
	'''

	protected def CharSequence buildEvaluateConditions(SynonymMap mapping, Iterable conditionedPaths) {
		var counterPerAttribute = new HashMap
		// more than once makes no diff here
		for (path : conditionedPaths) {
			counterPerAttribute.put(path.toAttPathName.toFirstUpper, 0);
		}
		val methods = new LinkedHashMap

		val methodOrder = TreeMultimap.create
		// useful for breakpoints
//		if (mapping.rosetta.name=="xyz") {
//			println()
//		}
		for (path : conditionedPaths.sortBy[attsToString]) {
			for (group : path.synonymGroups.filter[!conditions.isEmpty]) {
				for (cond : group.conditions) {
					// assigning counter per path name
					val perPathCounter = counterPerAttribute.get(path.toAttPathName.toFirstUpper)
					val methodName = 'evaluate' + path.toAttPathName.toFirstUpper + perPathCounter
					counterPerAttribute.put(path.toAttPathName.toFirstUpper, perPathCounter + 1);
					
					val priority = if(cond.condition.isEmpty) TestPriority.Last else cond.condition.minBy[priority].priority
					methodOrder.put(priority, methodName)

					if (cond.condition.forall[paths.forall[synonymPath.forall[cardinality === null || cardinality == Cardinality.SINGLE]]]) {
						methods.put(methodName, evalSingleCardinality(cond, mapping, path.attributePath, group.toPathList, perPathCounter))
					} 
					else if (!cond.condition.filter(SynonymPathTest).empty && !cond.condition.filter(SynonymBinaryTest).empty) {
						methods.put(methodName, evalPathAndTest(cond, path.attributePath, group.toPathList, perPathCounter));
					} 
					else {
						methods.put(methodName, evalMultiCardinality(mapping, cond, path))
					}
				}
			}
		}

		'''
			«FOR e : methods.entrySet»
				private void «e.key»() {
					«e.value»
				}
			«ENDFOR»
						
			private void evaluateConditions() {
				if (evaluatedConditions.compareAndSet(false, true)) {
					«FOR e : methodOrder.entries»
						«e.value»();
					«ENDFOR»
				}
			}
		'''
	}

	protected def CharSequence evalMultiCardinality(SynonymMap mapping, SynonymCondition cond, AttributeGroup group) '''
		«IF cond.condition.exists(SynonymRosettaPathTest)»
			Path rosettaPath = getRosettaPath()«FOR att:group.attributePath.dropMeta».addElement(new PathElement("«att.getName»"))«ENDFOR»;
			if («FOR test:cond.condition.filter(SynonymRosettaPathTest) SEPARATOR " && "»GeneratorPathUtil.matches(rosettaPath, "«test.path.toDotSep»")«ENDFOR») {
				«evalPathFiltering(cond, group, mapping)»
			}
		«ELSE»
			«evalPathFiltering(cond, group, mapping)»
		«ENDIF»
	'''

	def List dropMeta(List attributes) {
		val result = newArrayList
		for (var i = 0; i < attributes.length; i++) {
			if (attributes.size > 0 && attributes.get(i).attributeName.equals("value") && attributes.get(i-1).isMeta) {
				// skip the value artificial field
			} else if (attributes.get(i).attributeName.equals("meta")) {
				i++ // skip meta fields
			} else {
				result.add(attributes.get(i));
			}
		}
		result
	}

	protected def synonymPathHasMultipleCard(SynonymExistsTest synonymExistsTest) {
		synonymExistsTest.path.synonymPath.exists[cardinality === Cardinality.MULTIPLE]
	}

	protected def CharSequence evalPathFiltering(SynonymCondition cond, AttributeGroup group, SynonymMap mapping) '''	
		// evaluate path filtering
		Set paths = null;
		«var initialSet=false»
		«FOR test : cond.condition.filter(SynonymBinaryTest)»
			// SynonymBinaryTest
			// This next line hard codes a lot of stuff that probably needs to depend on the input
			«IF !initialSet»paths = («{initialSet=true;""}»«ELSE» paths.retainAll(«ENDIF»«toPathName(test.leftPath)»Values.entrySet()
				.stream()
				.filter(e -> areEqual(e.getValue(), «test.rightLiteral.handleWhenRight»))
				.map(e -> e.getKey())
				.collect(Collectors.toSet()));
		«ENDFOR»
		
		«FOR test : cond.condition.filter(SynonymExistsTest).filter[synonymPathHasMultipleCard]»
			// SynonymExistsTest
			«IF !initialSet»paths = («{initialSet=true;""}»«ELSE» paths.retainAll(«ENDIF»«toPathName(test.path)»Values.entrySet()
				.stream()
				.filter(e -> areEqual(e.getValue(), "Exists"))
				.map(e -> e.getKey())
				.collect(Collectors.toSet()));
		«ENDFOR»
		«FOR test : cond.condition.filter(SynonymAbsentTest)»
			// SynonymAbsentTest
			«IF !initialSet»paths = («{initialSet=true;""}»«ELSE» paths.removeAll(«ENDIF»«toPathName(test.path)»Values.entrySet()
				.stream()
				.filter(e -> areEqual(e.getValue(), "Exists"))
				.map(e -> e.getKey())
				.collect(Collectors.toSet()));
		«ENDFOR»
		«IF cond.condition.exists[c|!(c instanceof SynonymAbsentTest)]»
			«IF cond.setToValue === null && cond.condition.exists(SynonymExistsTest) && IngesterGeneratorUtils.isConditionalMappingPredicatedOnDifferentPath(group.synonymGroups.flatMap[synonymValues].toList, cond.condition.filter(SynonymExistsTest).map[path].toList)»
				// (a) «cond.condition.map[class.simpleName].join(",")»
				if (!paths.isEmpty()) {
				    int index = 0;
					«group.attributePath.toAttPathName»Values.values().forEach(v -> «setterChain(group.attributePath)»(v));
				}
			«ELSE»
				for (Path p : paths) {
					// in evalPathFiltering
					«IF group.attributePath.multipleCardinality»
						int index = p.cardinality();
					«ENDIF»
					«IF cond.setToValue !== null && cond.condition.filter[priority === TestPriority.Last].empty»
						«generateAnonHandler(group, mapping, group.attributePath.last, "p", "p")»
						builder.«setter(group.attributePath)»(«evaluate(cond.setToValue)»);
					«ELSEIF cond.setToValue !== null»
						«setterChain(group.attributePath)»(«evaluate(cond.setToValue)»);
					«ELSEIF group.attributePath.last.builtInType || !group.attributePath.multipleCardinality»
						«group.attributePath.toAttPathName»Values.entrySet()
							.stream()
							.filter(e -> e.getKey().fullStartMatches(p))
							.map(e -> e.getValue())
							.forEach(v -> «mergeAttribute(group.attributePath)»);
					«ELSE»	
						«group.attributePath.toAttPathName»Handlers.getAllHandlers()
							.stream()
							.filter(h -> h.getXmlPathsToHere().stream().anyMatch(k -> k.fullStartMatches(p)))
							.forEach(v -> «setterChain(group.attributePath)»(v.getUnderlying()));
					«ENDIF»
				}
			«ENDIF»
		«ELSE»
			// (b) «cond.condition.map[class.simpleName].join(",")»
			«IF IngesterGeneratorUtils.isConditionalMappingPredicatedOnDifferentPath(group.synonymGroups.flatMap[synonymValues].toList, cond.condition.filter(SynonymAbsentTest).map[path].toList)»
				«IF group.attributePath.getterMultipleCardinality»
					// getOrCreateFoo(MAX_VALUE) creates new list item at next available index
					int index = Integer.MAX_VALUE;
				«ENDIF»
				if (paths.isEmpty()) {
					«group.attributePath.toAttPathName»Values.values().forEach(v -> «setterChain(group.attributePath)»(v));
				}
			«ELSE»
				«IF group.attributePath.getterMultipleCardinality»
					// getOrCreateFoo(MAX_VALUE) creates new list item at next available index
					int index = Integer.MAX_VALUE;
				«ENDIF»
				for (Map.Entry entry : «group.attributePath.toAttPathName»Values.entrySet()) {
					if (!paths.stream().anyMatch(p -> entry.getKey().fullStartMatches(p))) {
						«setterChain(group.attributePath)»(entry.getValue());
					}
				}
			«ENDIF»
		«ENDIF»
	'''

	protected def CharSequence mergeAttribute(List attributePath) '''
		«IF attributePath.last.dataType»«attributeGetters(attributePath)»merge(v, new IngestMerger())«ELSE»«setterChain(attributePath)»(v)«ENDIF»
	'''

	protected def CharSequence evalSingleCardinality(SynonymCondition cond, SynonymMap mapping, List attributePath, String pathList, int attributeConditionIndex) '''
		// evaluate xml path that has single cardinality
		if («cond.handleWhen(pathList, attributePath)») {
			«conditionTrue(cond, attributePath, pathList, attributeConditionIndex)»
		}
	'''

	/**
	 * Code generate the conditional mapping predicate for xml path that is single cardinality
	 */
	private def handleWhen(SynonymCondition cond, String pathList, List attributePath) {
		if (cond.setToValue!==null && cond.condition.isEmpty) {
			// `set to` with no predicate test, e.g. a default condition
			'''!«supplier(attributePath)».isPresent()'''
		}
		else if (!cond.condition.isEmpty) {
			// code generate conditional mapping predicate
			'''«FOR test:cond.condition SEPARATOR " && "»«handleTest(test, pathList, attributePath)»«ENDFOR»'''
		}
		else {
			// no `set to` and no predicate, this should never happen
			'''/* no `set to` and `set when` condition */ true'''
		}
	}

	protected def CharSequence evalPathAndTest(SynonymCondition cond, List attributePath, String pathList, int attributeConditionIndex) '''
		«IF pathList !== ""»
		// evaluate path and test
		«conditionTrue(cond, attributePath, pathList, attributeConditionIndex)»
		«ENDIF»
	'''

	/**
	 * If the conditional mapping predicate resolves to true, this method code generates the logic.
	 */
	private def conditionTrue(SynonymCondition cond, List attributePath, String pathList, int attributeConditionIndex) '''
		«IF cond.setToValue!==null»
			// `set to` conditional mapping
			«IF cond.condition.exists(SynonymPathTest)»
				// (c) «cond.condition.map[class.simpleName].join(",")»
				«val underlying = attributePath.toFirstMultiple»
				«IF underlying.size>0»
					// nested set to (synonym path predicate)
					«IF attributePath.postFirstMultiple.getterMultipleCardinality»
						int index=Integer.MAX_VALUE;
					«ENDIF»
					evaluateConditionalPaths(
						«IF attributePath.multipleCardinality»«underlying.map[name].join("").toFirstLower»Underlyers,
						«ELSE»get«underlying.map[name].join("").toFirstUpper»UnderlyerB(),
						getXmlPathsToHere(),«ENDIF»
						handler -> handler.getUnderlying().«attributePath.postFirstMultiple.parentGetters»set«attributePath.last.name.toFirstUpper»(«evaluate(cond.setToValue)»),
						ImmutableList.of("«cond.condition.filter(SynonymPathTest).map[getPathWithDots].join("\",\"")»")«IF cond.condition.exists(SynonymBinaryTest)»,
						«toPathMatchBinary(cond.condition.filter(SynonymBinaryTest))»«ENDIF»);
				«ELSE»
					// set to (synonym path predicate)
					Set paths = getXmlPathsToHere();
					if (paths.stream().anyMatch(p -> «FOR test:cond.condition.filter(SynonymPathTest) SEPARATOR " || "»p.endsWith(Path.parse("«test.getPathWithDots»").getPathNames())«ENDFOR»)) {	
						set«attributePath.last.name.toFirstUpper»(«evaluate(cond.setToValue)»);
						return;
					}
				«ENDIF»
			«ELSE»
				// (d) «cond.condition.map[class.simpleName].join(",")»
				«IF attributePath.multipleCardinality»
					// set to (rosetta path predicate) that has multiple cardinality
					«IF attributePath.getterMultipleCardinality»
						int relativeIndex = «attributeConditionIndex»;
						// TODO This line needs further explanation
						int index = (relativeIndex == 0 || relativeIndex == 1) ? 0 : 1;
					«ENDIF»
					«setterChain(attributePath)»(«evaluate(cond.setToValue)»);
				«ELSE»
					// set to (rosetta path predicate) that has single cardinality
					«setterChain(attributePath)»(«evaluate(cond.setToValue)»);
				«ENDIF»
			«ENDIF»
		«ELSEIF pathList != ""»
			// `set when` conditional mapping
			«IF cond.condition.exists(SynonymPathTest)»
				// (e) «cond.condition.map[class.simpleName].join(",")»
				// set rosetta path that has single cardinality
				Set paths = getXmlPathsToHere();
				if (paths.stream().anyMatch(p -> «FOR test:cond.condition.filter(SynonymPathTest) SEPARATOR " || "»p.endsWith(Path.parse("«test.getPathWithDots»").getPathNames())«ENDFOR»)) {	
					«conditionSetterSingleCardinality(attributePath, pathList)»
				}
			«ELSE»
				// (f) «cond.condition.map[class.simpleName].join(",")»
				«IF attributePath.multipleCardinality»
					// set rosetta path that has multiple cardinality
					«IF attributePath.getterMultipleCardinality»
						// getOrCreateFoo(MAX_VALUE) creates new list item at next available index
						int index = Integer.MAX_VALUE;
					«ENDIF»
					for («attributePath.last.type.expandedTypeToBuilder» b : GeneratorPathUtil.getValuesForPathEnding(«attributePath.toAttPathName»Values, ImmutableList.of("«pathList»"))) {
						«setterChain(attributePath)»(b);
					}
				«ELSE»
					«conditionSetterSingleCardinality(attributePath, pathList)»
				«ENDIF»
			«ENDIF»
		«ENDIF»
		
	'''
	def conditionSetterSingleCardinality(List attributePath, String pathList) '''
		// set rosetta path that has single cardinality
		Optional<«attributePath.last.type.expandedTypeToBuilder»> val = GeneratorPathUtil.getValueForPath(«attributePath.toAttPathName»Values, ImmutableList.of("«pathList»"));
		if (val.isPresent()) {
			«IF attributePath.last.type.type»
				«attributePath.last.type.expandedTypeToBuilder» builder = val.get();
				new IngestMerger().run(builder, «getterChain(attributePath)»);
				«setterChain(attributePath)»(builder);
			«ELSE»
				«setterChain(attributePath)»(val.get());
			«ENDIF»
			return;
		}
		'''

	def toPathMatchBinary(Iterable tests) '''
		ps -> «FOR test : tests SEPARATOR " && "»
				GeneratorPathUtil.getPathMatchingValues(ps, «test.paths.last.toPathName»Values).stream().anyMatch(s -> «test.operator.toJava»(s,«toJava(test.rightPath, test.rightLiteral)»))
		«ENDFOR»
	'''

	private def toPathList(SynonymGroup group) {
		group.synonymValues.map[synonymPath.map[name].join(".")].join('''", "''')
	}

	private def setterChain(List path) '''«parentGetters(path)»«setter(path)»'''
	
	private def getterChain(List path) '''«parentGetters(path)»«getter(path.last)»'''

	private def parentGetters(List path) {
		path.removeLast.join("", ".", ".", [a|getter(a)]);
	}
	
	private def attributeGetters(List path) {
		path.join("", ".", ".", [a|getter(a)]);
	}

	private def String getter(ExpandedAttribute att) {
		if (att.isMultiple) {
			'''getOrCreate«att.name.toFirstUpper»(index)'''
		} else {
			'''getOrCreate«att.name.toFirstUpper»()'''
		}
	}

	private def setter(
		List path) '''«IF path.last.cardinalityIsSingleValue»set«ELSE»add«ENDIF»«path.last.name.toFirstUpper»'''

	private def dispatch evaluate(RosettaMapTestExpression expression) {
		throw new UnsupportedOperationException("Don't know how to evaluate expression " + expression.class)
	}

	private def dispatch evaluate(RosettaLiteral expression) {
		return expression.stringValue
	}

	private def dispatch evaluate(RosettaEnumValueReference ref) '''
	«ref.enumeration.fullname.trim».«EnumHelper.formatEnumName(ref.value.name)»'''

	private def dispatch handleTest(SynonymTest test, String pathList, List attributePath) {
		throw new UnsupportedOperationException("Don't know how to generate test for " + test.class)
	}

	private def dispatch handleTest(SynonymExistsTest test, String pathList, List attributePath) '''«test.path.toPathName»Value != null'''

	private def dispatch handleTest(SynonymAbsentTest test, String pathList, List attributePath) '''«test.path.toPathName»Value == null'''

	private def dispatch handleTest(extension SynonymBinaryTest test, String pathList, List attributePath) 
		'''«operator.toJava»(«toJava(leftPath, leftLiteral)»,«toJava(rightPath, rightLiteral)»)'''

	private def dispatch handleTest(SynonymPathTest test, String pathList, List attributePath) { return "true" }

	private def dispatch handleTest(SynonymRosettaPathTest test, String pathList, List attributePath) 
		'''GeneratorPathUtil.endsWith(getRosettaPath().«attributePath.map[name].map['addElement("'+it+'")'].join('.')».getParent(), "«test.path.toDotSepDropFirst»")'''

	private def dispatch handleTest(SynonymConditionFuncTest test, String pathList, List attributePath) '''
		injector.getInstance(«test.funcClassName».class)
				.evaluate(getSynonymPathForConditionFunc(getXmlPathsToHere(), "«pathList»"), 
						formatPathWithNoIndexesAndSeparators(getRosettaPath()), 
						«IF test.predicatePathEndsWith !== null»getValuesFromConditionPath(«test.predicatePathEndsWith.map['"'+it+'"'].join(', ')»)«ELSE»null«ENDIF»)'''

	private def toJava(SynonymValue path, RosettaMapTestExpression literal) {
		if(path !== null) path.toPathName + "Value" else literal.handleWhenRight

	}

	private def toJava(String op) {
		switch (op) {
			case "=": "areEqual"
			case "<>": "!areEqual"
			default: throw new UnsupportedOperationException("test operator " + op + " not currently supported")
		}
	}

	private def dispatch CharSequence toDotSep(
		RosettaAttributeReference call) '''«call.receiver.toDotSep».«call.attribute.name»'''

	private def dispatch CharSequence toDotSep(RosettaDataReference call) '''«call.data.name»'''
	
	private def dispatch CharSequence toDotSepDropFirst(
		RosettaAttributeReference call) '''«call.receiver.toDotSepDropFirst»«call.attribute.name».'''

	private def dispatch CharSequence toDotSepDropFirst(RosettaDataReference call) ''''''

	private dispatch def handleWhenRight(RosettaMapTestExpression right) {
		throw new UnsupportedOperationException("Unsupported expression type " + right.class.simpleName)
	}

	private dispatch def handleWhenRight(RosettaStringLiteral right) '''"«right.value»"'''

	private dispatch def handleWhenRight(RosettaBooleanLiteral right) '''Boolean.valueOf(«right.value»)'''

	private dispatch def handleWhenRight(RosettaIntLiteral right) '''Integer.valueOf(«right.value»)'''
	
	private dispatch def handleWhenRight(RosettaNumberLiteral right) '''BigDecimal.valueOf(«right.value»)'''

	private def generateEnumParsers(SynonymMap mapping, Map generated, String childName,
		Map synHashCache) {
		if (generated.containsKey(mapping.rosetta.fullname)) {
			return;
		}

		val prevHash = synHashCache.get(mapping.rosetta.fullname)
		val newHash = mapping.hashForGeneration
		if (prevHash === null || newHash != prevHash) {
			var parser = '''
				package «childName».«mapping.rosetta.packageName»;
				
				import java.util.Map;
				
				«basicEnumParserImports(mapping)»
				
				public class «mapping.pNameW» extends EnumParseHandler<«mapping.rosettaName»> {
					
					private final Map lookup;
					
					«enumConstructor(mapping)»
					
					@Override
					protected «mapping.rosettaName» convert(String value) {
						«mapping.rosettaName» enumValue = lookup.get(value);
						if (enumValue!=null) return enumValue;
						throw new RuntimeException("Could not find matching «mapping.rosetta.name» for [" + value + "]");
					}
				}
			'''

			generated.put(mapping.rosetta.fullname, new GenerationResult(false, parser))
			synHashCache.put(mapping.rosetta.fullname, newHash)
		} else {
			generated.put(mapping.rosetta.fullname, new GenerationResult(true, null))
		}
	}

	private def enumConstructor(SynonymMap mapping) '''
		public «mapping.rosetta.name»ParseHandler(boolean allowsMultiple, boolean condition, boolean removeHtml) {
			this(allowsMultiple, condition, removeHtml, null, null);
		}
		
		public «mapping.rosetta.name»ParseHandler(boolean allowsMultiple, boolean condition, boolean removeHtml, String patternMatch, String patternReplace) {
			super(allowsMultiple, condition, removeHtml, patternMatch, patternReplace);
			ImmutableMap.Builder builder = ImmutableMap.builder();
					«FOR attrPathSynGroup : mapping.attributeGroups.sort»
						«FOR synonymValue : attrPathSynGroup.getSynonymGroups.flatMap[sp|sp.synonymValues].sortBy[synonymPath.map[name].join]»
							builder.put("«synonymValue.synonymPath.map[name].join»", «mapping.rosettaName».«EnumHelper.formatEnumName(attrPathSynGroup.getAttributePath.last.name)»);
						«ENDFOR»
					«ENDFOR»
			lookup = builder.build();
		}
	'''

	private def  basicEnumParserImports(SynonymMap mapping) {
		val rosettaModel = mapping.rosetta.eContainer as RosettaModel

		val imports = newArrayList
		imports.addAll("import com.google.common.collect.ImmutableMap",
			"import com.regnosys.rosetta.translate.basic.EnumParseHandler",
			"import com.regnosys.rosetta.common.translation.Path")
		imports.add("import " + rosettaModel.name + "." + mapping.rosetta.name)
		return imports.stream.sorted.distinct.collect(Collectors.joining(";\n", "", ";"))
	}

	static class UniqueFilter implements Predicate {
		Set seen = newHashSet
		Function propExtract

		new(Function propExt) {
			this.propExtract = propExt
		}

		override test(T t) {
			return seen.add(propExtract.apply(t))
		}
	}

	def String generateXmlFactory(String packageName, String factoryName, List generatorParamsList) '''
		package «packageName»;
		
		import java.net.MalformedURLException;
		import java.net.URL;
		import java.nio.file.Files;
		import java.nio.file.Path;
		import java.nio.file.Paths;
		import java.util.Map;
		import java.util.HashMap;
		
		import javax.xml.validation.Schema;
		import javax.xml.validation.SchemaFactory;

		import com.regnosys.rosetta.common.translation.MappingContext;
		import com.regnosys.rosetta.common.util.UrlUtils;
		import org.xml.sax.SAXException;
		import com.google.inject.Injector;
		
		import com.rosetta.model.lib.RosettaModelObject;
		import com.regnosys.rosetta.translate.*;
		«IF !generatorParamsList.map[topLevelTags].flatten.empty»
			import java.util.Arrays;
		«ENDIF»
		import java.util.function.Function;
		
		public class «factoryName» implements XmlHandlerFactory {
			
			private final Function, HandlerSupplier>> factory;
			
			private final Map, Schema> schemas = new HashMap<>();
			
			private static final SchemaFactory SCHEMA_FACTORY = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema");
			«FOR xsdUrl : generatorParamsList.map[schema]»
				private static Schema «xsdUrl.urlToStaticVar» = newSchema("«IF xsdUrl.protocol == 'jar'»«Splitter.on('!/').split(xsdUrl.path).last»«ELSE»«StringEscapeUtils.escapeJava(UrlUtils.toPath(xsdUrl).toString)»«ENDIF»");
			«ENDFOR»
			
			private final Injector injector;
			
			public «factoryName»(Injector injector) {
				this.injector = injector;
				this.factory = (mappingContext) -> {
					Map, HandlerSupplier> handlerMap = new HashMap<>();
						«FOR params : generatorParamsList»
							«FOR rmo : params.rosettaEntities»
								«IF params.topLevelTags.empty»
									handlerMap.put(«rmo.fullname».class, (a, b) -> new «rmo.pFullNameT(params.getChildPackageName)»(injector, mappingContext));
								«ELSE»
									handlerMap.put(«rmo.fullname».class, (a, b) -> new RootHandler<>("«rmo.name»", new «rmo.pFullNameT(params.getChildPackageName)»(injector, mappingContext), Arrays.asList(«params.topLevelTags.map['"' + it + '"'].join(', ')»)));
								«ENDIF»
							«ENDFOR»
						«ENDFOR»
					return handlerMap;
				};
				«FOR params : generatorParamsList»
					«FOR rmo : params.rosettaEntities»
						schemas.put(«rmo.fullname».class, «params.schema.urlToStaticVar»);
					«ENDFOR»
				«ENDFOR»
			}

			@Override
			public Map, HandlerSupplier> getHandlerSuppliers(MappingContext mappingContext) {
				return factory.apply(mappingContext);
			}
			
			@Override 
			public Map, Schema> getSchemas() {
				return schemas;
			}
			
			private static Path getXsdPath(String xsdFilePath) {
				Path path = Paths.get(xsdFilePath);
				if (Files.exists(path)) {
					return path;
				}

				URL resource = «factoryName».class.getClassLoader().getResource(xsdFilePath);
				if (resource != null) {
					return UrlUtils.toPath(resource);
				}

				throw new IngestException("Error reading xsd file - " + xsdFilePath + " could not be found");
			}

			private static Schema newSchema(String schemaUrl) {
				try {
					return SCHEMA_FACTORY.newSchema(getXsdPath(schemaUrl).toUri().toURL());
				} catch (SAXException | MalformedURLException e) {
					throw new IngestException("Failed to create Schema URL for " + schemaUrl, e);
				}
			}
		
			@Override
			public Injector getInjector() {
				return injector;
			}
		}
	'''

	def String generateJsonFactory(String packageName, String factoryName, List generatorParamsList) '''
		package «packageName»;
		
		import java.net.MalformedURLException;
		import java.net.URL;
		import java.util.Map;
		import java.util.HashMap;
		
		import com.google.inject.Injector;
		
		import com.regnosys.rosetta.common.translation.MappingContext;
		import com.rosetta.model.lib.RosettaModelObject;
		import com.regnosys.rosetta.translate.*;
		import com.regnosys.rosetta.translate.basic.*;
		«IF !generatorParamsList.map[topLevelTags].flatten.empty»
			import java.util.Arrays;
		«ENDIF»
		import java.util.function.Function;
		
		public class «factoryName» implements JsonHandlerFactory {
			
			private final Function, HandlerSupplier>> factory;
			
			private final Injector injector;
			
			public «factoryName»(Injector injector) {
				this.injector = injector;
				this.factory = (mappingContext) -> {
					Map, HandlerSupplier> handlerMap = new HashMap<>();
						«FOR params : generatorParamsList»
							«FOR rmo : params.rosettaEntities»
								«IF params.topLevelTags.empty»
									handlerMap.put(«rmo.fullname».class, (a, b) -> new «rmo.pFullNameT(params.getChildPackageName)»(injector, mappingContext));
								«ELSE»
									handlerMap.put(«rmo.fullname».class, (a, b) -> new RootHandler<>("«rmo.name»", new «rmo.pFullNameT(params.getChildPackageName)»(injector, mappingContext), Arrays.asList(«params.topLevelTags.map['"' + it + '"'].join(', ')»)));
								«ENDIF»
							«ENDFOR»
						«ENDFOR»
					return handlerMap;
				};
			}
		
			@Override
			public Map, HandlerSupplier> getHandlerSuppliers(MappingContext mappingContext) {
				return factory.apply(mappingContext);
			}
		
			@Override
			public Injector getInjector() {
				return injector;
			}
		}
	'''

	private def urlToStaticVar(URL u) {
		UrlUtils.getFileName(u).replaceAll('[-,.]', '_').toUpperCase
	}

	private def removeLast(SynonymValue synonymValue) {
		new SynonymValue(synonymValue.synonymPath.removeLast, synonymValue.mapsTo, synonymValue.isMeta)
	}

	private def removeLast(AttributeGroup group) {
		new AttributeGroup(group.synonymGroups.map[removeLast], group.attributePath.removeLast)
	}

	private def removeLast(SynonymGroup group) {
		new SynonymGroup(group.synonymValues.map[removeLast], group.conditions, group.mapperName, group.formatString, group.patternMatcher, group.patternReplace, group.removeHtml)
	}

	private def  removeLast(List list) {
		list.subList(0, list.size - 1)
	}

	private def penultimateBuilder(List attributes, SynonymMap mapping) {
		if (attributes.size >= 2) {
			return attributes.get(attributes.size - 2).type.expandedTypeToBuilder
		} else {
			return mapping.rosetta.toBuilder
		}
	}

	/**
	 * @param atts - list of attributes represents a path (with attribute name/type for each path element).
	 * @returns the first attribute that has multiple cardinality
	 */
	private def firstMultipleBuilder(List attributes, SynonymMap mapping) {
		var resAt = attributes.toFirstMultiple;
		if (resAt.size >= 1) {
			return resAt.last.type.expandedTypeToBuilder
		} else {
			return mapping.rosetta.toBuilder
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy