mbeckerle commented on code in PR #1176: URL: https://github.com/apache/daffodil/pull/1176#discussion_r1511949267
########## daffodil-runtime1-layers/src/main/resources/org/apache/daffodil/layers/xsd/boundaryMarkLayer.dfdl.xsd: ########## @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<schema + xmlns:xs="http://www.w3.org/2001/XMLSchema" + xmlns="http://www.w3.org/2001/XMLSchema" + xmlns:dfdl="http://www.ogf.org/dfdl/dfdl-1.0/" + xmlns:dfdlx="http://www.ogf.org/dfdl/dfdl-1.0/extensions" + xmlns:bm="urn:org.apache.daffodil.layers.boundaryMark" + targetNamespace="urn:org.apache.daffodil.layers.boundaryMark"> + + <annotation> + <appinfo source="http://www.ogf.org/dfdl/"> + + <dfdl:defineVariable name="boundaryMark" type="xs:string"/> + <dfdl:defineVariable name="layerEncoding" type="xs:string"/> Review Comment: Should it be there or in the javadoc? For DFDL schema users it should be in these files I think. I have not written this yet however. ########## daffodil-runtime1-layers/src/main/scala/org/apache/daffodil/layers/runtime1/FixedLengthLayer.scala: ########## @@ -0,0 +1,144 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.daffodil.layers.runtime1 + +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.io.OutputStream +import java.lang.{ Long => JLong } +import java.nio.ByteBuffer + +import org.apache.daffodil.lib.exceptions.Assert +import org.apache.daffodil.runtime1.layers.api.Layer +import org.apache.daffodil.runtime1.layers.api.LayerRuntime + +import org.apache.commons.io.IOUtils + +/** + * Suitable only for small sections of data, not large data streams or whole files. + * See the maxFixedLength value defined herein for the maximum. + * + * The entire fixed length region of the data will be pulled into a byte buffer in memory. + * + * TODO: Someday, enhance to make this streaming. + * + * One DFDL Variable is a parameter + * - fixedLength - an unsignedInt giving the fixed length of this layer. + * This length is enforced on both parsing and unparsing the layer. + * There are no output/result DFDL variables from this layer. + */ +final class FixedLengthLayer(var fixedLength: JLong) + extends Layer("fixedLength", "urn:org.apache.daffodil.layers.fixedLength") { + + Assert.invariant(fixedLength > 0) + + /** Required for SPI class loading */ + def this() = this(1) + + private def maxFixedLength = Short.MaxValue + + override def wrapLayerInput(jis: InputStream, lr: LayerRuntime): InputStream = { + + if (fixedLength > maxFixedLength) + lr.processingError( + s"fixedLength value of $fixedLength is above the maximum of $maxFixedLength.", + ) + + new FixedLengthInputStream(fixedLength.toInt, jis, lr) + } + + override def wrapLayerOutput(jos: OutputStream, lr: LayerRuntime): OutputStream = { + + if (fixedLength > maxFixedLength) Review Comment: I think this is not worth it. It's better to just construct the object at schema compile time so as to verify everything works, and then do it again, once, on loading. The latter check is really only needed for the chance case that you use the wrong jar/class files that don't match the schema. And even in that case the check just verifies that the number of param args and their types and the return getters and their types haven't changed. You could still have the wrong jar. ########## daffodil-runtime1/src/main/scala/org/apache/daffodil/runtime1/layers/LayerDriver.scala: ########## @@ -0,0 +1,227 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.daffodil.runtime1.layers + +import java.io.FilterInputStream +import java.io.FilterOutputStream +import java.io.InputStream +import java.io.OutputStream + +import org.apache.daffodil.io.DataInputStream.Mark +import org.apache.daffodil.io.DataOutputStream +import org.apache.daffodil.io.DirectOrBufferedDataOutputStream +import org.apache.daffodil.io.FormatInfo +import org.apache.daffodil.io.InputSourceDataInputStream +import org.apache.daffodil.lib.exceptions.Assert +import org.apache.daffodil.lib.schema.annotation.props.gen.BitOrder +import org.apache.daffodil.lib.util.Maybe +import org.apache.daffodil.lib.util.Maybe.Nope +import org.apache.daffodil.lib.util.Maybe.One +import org.apache.daffodil.lib.util.Misc +import org.apache.daffodil.runtime1.layers.api.Layer +import org.apache.daffodil.runtime1.processors.unparsers.UState + +import passera.unsigned.ULong + +/** + * Driving mechanism that incorporates a layer at runtime to transform the data stream. + * + * A layer driver is created at runtime as part of a single parse/unparse call. + * Hence, they can be stateful without causing thread-safety issues. + */ +class LayerDriver( + layerRuntimeData: LayerRuntimeData, + layer: Layer, + layerVarsRuntime: LayerVarsRuntime, +) { + + private def wrapJavaInputStream( + s: InputSourceDataInputStream, + layerRuntimeImpl: LayerRuntimeImpl, + ): InputStream = + new JavaIOInputStream(s, layerRuntimeImpl.finfo) + + private def wrapJavaOutputStream( + s: DataOutputStream, + layerRuntimeImpl: LayerRuntimeImpl, + ): OutputStream = + new JavaIOOutputStream(s, layer, layerRuntimeImpl, layerVarsRuntime) + + /** + * Override these if we ever have transformers that don't have these + * requirements. + */ + val mandatoryLayerAlignmentInBits: Int = 8 + + final def addInputLayer( + s: InputSourceDataInputStream, + layerRuntimeImpl: LayerRuntimeImpl, + ): InputSourceDataInputStream = { + val jis = wrapJavaInputStream(s, layerRuntimeImpl) + val decodedInputStream = + new AssuredCloseInputStream(layer.wrapLayerInput(jis, layerRuntimeImpl), jis) + val newDIS = InputSourceDataInputStream(decodedInputStream) + newDIS.cst.setPriorBitOrder( + BitOrder.MostSignificantBitFirst, + ) // must initialize priorBitOrder + newDIS.setDebugging(s.areDebugging) + newDIS + } + + /** + * Parsing works as a tree traversal, so when the parser unwinds from + * parsing the layer we can invoke this to handle cleanups, and + * finalization issues like assigning the result variables + */ + final def removeInputLayer( + s: InputSourceDataInputStream, + layerRuntimeImpl: LayerRuntimeImpl, + ): Unit = { + layerVarsRuntime.callGettersToPopulateResultVars(layer, layerRuntimeImpl) Review Comment: The LayerFactory has to _get_ variable values. The LayerDriver has to call the getters to assign the output variables. This is ordinary I think. One could do something more java-beans flavored, and have setters for the args and getters for the results, but I think that's clumsier. Never did like java beans approach. So many lines of code to do nothing much. ########## daffodil-runtime1-layers/src/main/resources/org/apache/daffodil/layers/xsd/base64_MIMELayer.dfdl.xsd: ########## @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<schema + xmlns:xs="http://www.w3.org/2001/XMLSchema" + xmlns="http://www.w3.org/2001/XMLSchema" + xmlns:dfdl="http://www.ogf.org/dfdl/dfdl-1.0/" + xmlns:dfdlx="http://www.ogf.org/dfdl/dfdl-1.0/extensions" + xmlns:b64="urn:org.apache.daffodil.layers.base64_MIME" + targetNamespace="urn:org.apache.daffodil.layers.base64_MIME"> + + <!-- + This layer does not define any variables for parameters or results + --> + <annotation> + <appinfo source="http://www.ogf.org/dfdl/"> + <dfdl:defineFormat name="base64_MIME"> + <dfdl:format dfdlx:layerTransform="b64:base64_MIME"/> + </dfdl:defineFormat> + </appinfo> + </annotation> Review Comment: Getting rid of the dfdl:ref. They add no value over just dfdlx:layer with a QName arg. ########## daffodil-runtime1-layers/src/main/scala/org/apache/daffodil/layers/runtime1/FixedLengthLayer.scala: ########## @@ -0,0 +1,144 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.daffodil.layers.runtime1 + +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.InputStream +import java.io.OutputStream +import java.lang.{ Long => JLong } +import java.nio.ByteBuffer + +import org.apache.daffodil.lib.exceptions.Assert +import org.apache.daffodil.runtime1.layers.api.Layer +import org.apache.daffodil.runtime1.layers.api.LayerRuntime + +import org.apache.commons.io.IOUtils + +/** + * Suitable only for small sections of data, not large data streams or whole files. + * See the maxFixedLength value defined herein for the maximum. + * + * The entire fixed length region of the data will be pulled into a byte buffer in memory. + * + * TODO: Someday, enhance to make this streaming. + * + * One DFDL Variable is a parameter + * - fixedLength - an unsignedInt giving the fixed length of this layer. + * This length is enforced on both parsing and unparsing the layer. + * There are no output/result DFDL variables from this layer. + */ +final class FixedLengthLayer(var fixedLength: JLong) Review Comment: Not needed. ########## daffodil-core/src/main/scala/org/apache/daffodil/core/layers/LayerCompiler.scala: ########## @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.daffodil.core.layers + +import org.apache.daffodil.core.dsom.SequenceGroupTermBase +import org.apache.daffodil.lib.exceptions.Assert +import org.apache.daffodil.lib.xml.RefQName +import org.apache.daffodil.runtime1.layers.LayerFactory +import org.apache.daffodil.runtime1.layers.LayerRegistry +import org.apache.daffodil.runtime1.layers.LayerRuntimeData +import org.apache.daffodil.runtime1.layers.api.Layer + +object LayerCompiler { + + /** + * Compiles the layer. + * + * This is mostly checking for errant use of DFDL properties that + * can't be used on layers. + * + * The real compilation - creating runtime data structures based + * on constructor signatures and DFDL variables that are in the + * layer's namespace, that is called here, but it is part of + * the daffodil runtime because that step has to be carried out + * *also* at runtime to verify that the dynamically loaded layer + * classes are compatible with the variables and their definitions. + * + * Constructs a LayerFactory which is the serializable runtime data structure + * used by the LayerParser, and LayerUnparser at runtime. + */ + def compileLayer(sq: SequenceGroupTermBase): LayerFactory = { + val lc = new LayerCompiler(sq) + val res = lc.compile() + res + } +} + +/** + * + * @param sq + */ +private class LayerCompiler private (sq: SequenceGroupTermBase) { + + Assert.usage(sq.isLayered) + + private def srd = sq.sequenceRuntimeData + private def lrd = sq.optionLayerRuntimeData.get + + private def layerQName: RefQName = lrd.layerQName + private def layerName = layerQName.local + private def layerNamespace = layerQName.namespace + + private val layeredSequenceAllowedProps = Seq( + "ref", Review Comment: Removing. ########## daffodil-runtime1/src/main/scala/org/apache/daffodil/runtime1/layers/LayerFactory.scala: ########## @@ -0,0 +1,326 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.daffodil.runtime1.layers + +import java.lang.reflect.Constructor +import java.lang.reflect.Method +import scala.collection.immutable.ListSet +import scala.collection.mutable + +import org.apache.daffodil.lib.exceptions.Assert +import org.apache.daffodil.runtime1.dpath.NodeInfo.PrimType +import org.apache.daffodil.runtime1.infoset.DataValue +import org.apache.daffodil.runtime1.layers.api.Layer +import org.apache.daffodil.runtime1.layers.api.LayerException +import org.apache.daffodil.runtime1.processors.VariableRuntimeData + +object LayerFactory { + + /** cache that maps spiName of layer to the LayerVarsRuntime */ + private lazy val alreadyCheckedLayers = + new mutable.LinkedHashMap[String, LayerVarsRuntime]() + + /** + * Computes the things that can be computed once-only to make calling the constructor + * and passing the parameter variables faster at runtime. This also does all the + * checking that the constructor has an argument for each variable of properly matching type, and + * has a getter for each result variable, again returning the proper type. + * + * This is called at schema compile time to ensure the layer code is properly defined and matches + * the layer variable definitions. + * + * It is called again at runtime after the Layer class is loaded by the SPI + * to ensure that the loaded layer class constructor signature at least matches the layer + * variables defined in the schema. + * @param lrd + * @param protoLayer the layer instance allocated by the SPI loader (zero-arg constructed) + * @return + */ + def computeLayerVarsRuntime(lrd: LayerRuntimeData, protoLayer: Layer): LayerVarsRuntime = { + val optLayerVarsRuntime = alreadyCheckedLayers.get(protoLayer.name()) + optLayerVarsRuntime.getOrElse { + // we know there is a default zero arg constructor + // find another constructor and check that there is an argument + // corresponding to each of the layer variables. + val c = protoLayer.getClass + val allConstructors = c.getConstructors.toSeq + + val constructor: Constructor[_] = + if (allConstructors.length == 1) { + // There is only the default constructor. + // That's ok if there are no variables for the layer, which we check later. + allConstructors.head + } else if (allConstructors.length == 2) { + allConstructors.filter(_.getParameterCount > 0).head + } else { + def tooManyConstructorsMsg: String = { + s"""Layer class $c has multiple non-default constructors. It should have a default (no args) + | constructor and a single additional constructor with arguments for + | each of the layer's parameter variables.""".stripMargin + } + lrd.context.SDE(tooManyConstructorsMsg) + } + + if (lrd.vmap.isEmpty && allConstructors.length == 1) { + // there are no vars, we're done + new LayerVarsRuntime(constructor, Nil, Nil) + } else { + // there is a constructor with args that are supposed to correspond to bound vars + val params = constructor.getParameters.toSeq + val nParams = params.length + val nVars = lrd.vmap.size + + val paramTypes = constructor.getParameterTypes.toSeq + val paramVRDs = params.map { p => + lrd.vmap.getOrElse( + p.getName, + lrd.context.SDE(s"No layer DFDL variable named '$p.getName' was found."), + ) + }.toSeq + + // Now we deal with the result getters and the corresponding vars + // + val allLayerVRDs = ListSet(lrd.vmap.toSeq.map(_._2): _*) + val returnVRDs = allLayerVRDs -- paramVRDs // set subtraction + val allMethods = c.getMethods + val allVarResultGetters = + ListSet(allMethods.filter { m => + val nom = m.getName + nom.startsWith(varResultPrefix) + }.toSeq: _*) + // each returnVRD needs to have a corresponding getter method + val returnVRDNames = returnVRDs.map(_.globalQName.local) + val resultGettersNames = allVarResultGetters.map(_.getName.replace(varResultPrefix, "")) + val nResultGetters = resultGettersNames.size + def javaConstructorArgs = + paramVRDs + .map { + case vrd => { + s"type: ${PrimType.toJavaTypeString(vrd.primType.dfdlType)} name: ${vrd.globalQName.local}" + } + } + .mkString(", ") + def badConstructorMsg: String = { + s"""Layer class $c does not have a constructor with arguments for each of the layer's variables. + | It should have a constructor with these arguments in any order, such as + | ($javaConstructorArgs)""".stripMargin + } + + lrd.context.schemaDefinitionUnless(nParams + nResultGetters == nVars, badConstructorMsg) + + // at this point the number of vars and number of constructor args match + + val typePairs = (paramVRDs.zip(paramTypes)).toSeq + typePairs.foreach { case (vrd, pt) => + val vrdClass = PrimType.toJavaType(vrd.primType.dfdlType) + lrd.context.schemaDefinitionUnless( + vrdClass == pt, + s"""Layer constructor argument ${vrd.globalQName.local} and the corresponding + |Layer DFDL variable have differing types: ${pt.getName} + | and ${vrdClass.getName} respectively.""".stripMargin, + ) + } + + val returnVRDsWithoutGetters = returnVRDNames -- resultGettersNames + val resultGettersWithoutVRDs = resultGettersNames -- returnVRDNames + lrd.context.schemaDefinitionUnless( + returnVRDsWithoutGetters.isEmpty, + s"""The layer variables ${returnVRDsWithoutGetters.mkString( + ",", + )} have no corresponding getters.""", + ) + lrd.context.schemaDefinitionUnless( + resultGettersWithoutVRDs.isEmpty, { + val getterFullNames = returnVRDsWithoutGetters.map { vname => + this.varResultPrefix + vname + } + s"""The getters ${getterFullNames.mkString( + ",", + )} have no corresponding layer variables.""" + }, + ) + // at this point we know each variable that was not a parameter of the constructor + // has a getter with matching name. + val resultVarPairs = resultGettersNames.map { rgn: String => + val getter: Method = + allVarResultGetters.find { g: Method => g.getName.endsWith(rgn) }.getOrElse { + Assert.invariantFailed("no getter for getter name.") + } + val vrd = returnVRDs.find { vrd => vrd.globalQName.local == rgn }.getOrElse { + Assert.invariantFailed("no vrd for getter name.") + } + (vrd, getter) + } + resultVarPairs.foreach { case (vrd, getter) => + val vrdClass = PrimType.toJavaType(vrd.primType.dfdlType) + val gt = getter.getReturnType + lrd.context.schemaDefinitionUnless( + vrdClass == gt, + s"""Layer return variable ${vrd.globalQName.local} and the corresponding + |Layer getter have differing types: ${vrdClass.getName} + | and ${gt.getName} respectively.""".stripMargin, + ) + } + val lrv = new LayerVarsRuntime(constructor, paramVRDs, resultVarPairs.toSeq) + alreadyCheckedLayers.put(lrd.spiName, lrv) + lrv + } + } + } + + private def varResultPrefix = "getDFDLResultVariable_" + + type ParameterVarsInfo = Seq[Seq[(String, Class[_])]] + type ResultVarsInfo = Seq[Method] + + def analyzeClass(obj: Any): (ParameterVarsInfo, ResultVarsInfo) = { + val c = obj.getClass + val cs = c.getConstructors.toSeq + val constructorInfo = cs.map { c => + val parms = c.getParameters.toSeq + parms.map { p => + val pn: String = p.getName + val pt = p.getType + (pn, pt) + } + } + val getterInfo: Array[Method] = c.getMethods.filter { + _.getName.startsWith(varResultPrefix) + } + (constructorInfo, getterInfo) + } + +} + +/** + * Factory for a layer + * + * This is the serialized object which is saved as part of a processor. + * It constructs the layer at runtime when newInstance() is called. + * + * This allows the layer instance itself to be stateful and not serializable. + */ +class LayerFactory(val layerRuntimeData: LayerRuntimeData) extends Serializable { + import LayerFactory._ + + /** + * Call at runtime to create a layer object. This layer object can be stateful + * and non-thread safe. + * + * Called by the LayeredSequenceParser or LayeredSequenceUnparser to allocate the + * layer, and the result is used to carry out the layering mechanism. + * + * @param lri the layer runtime info which includes both static and runtime + * state-based information for the parse or unparse + * @return the Layer properly initialized/constructed for this layer + */ + def newInstance(lri: LayerRuntimeImpl): LayerDriver = { + val optCache = alreadyCheckedLayers.get(lri.layerRuntimeData.spiName) + val layerVarsRuntime: LayerVarsRuntime = optCache.getOrElse { + val optLayerInstance = LayerRegistry.findLayer(lri.layerRuntimeData.spiName) + val spiLayerInstance: Layer = optLayerInstance.getOrElse { + lri.runtimeSchemaDefinitionError( + new LayerException( + lri, + s"Unable to load class for layerTransform '${lri.layerRuntimeData.layerQName.toQNameString}'.", + ), + ) + } + // Since layer implementations are dynamically loaded, we must re-verify that + // the layer implementation matches the DFDL schema's layer variable definitions. + // This prevents using a layer class file that doesn't match the schema, at + // least as far as the number and type of the DFDL variables it consumes and writes goes. + // However, we want to do this exactly once, not every time this method is called, + // So there is a cache of instances that have already been through these checks, + // at runtime. + // We compute this data structure once only at the time the SPI Loads the layer + // class. + // In addition, this process of verifying the DFDL variables used by the layer + // pre-computes some data structures that facilitate fast run-time processing + + val lvr = computeLayerVarsRuntime(lri.layerRuntimeData, spiLayerInstance) Review Comment: This one I have to think about. Perhaps we can do this. Once we have the layer we have the layer namespace, and that will let us get the variables if we have the data processor. That's the issue. Do we have the data processor object (with schema set runtime data) already in hand at this point, or is that 'in process' of being created? ########## daffodil-runtime1/src/main/scala/org/apache/daffodil/runtime1/dpath/NodeInfo.scala: ########## @@ -932,6 +936,33 @@ object NodeInfo extends Enum { DFDLTimeConversion.fromXMLString(s) } } + + def toJavaType(dfdlType: DFDLPrimType): Class[_] = { + dfdlType match { + case DFDLPrimType.String => classOf[JString] + case DFDLPrimType.Int => classOf[JInt] + case DFDLPrimType.Byte => classOf[JByte] + case DFDLPrimType.Short => classOf[JShort] + case DFDLPrimType.Long => classOf[JLong] + case DFDLPrimType.Integer => classOf[JBigInt] + case DFDLPrimType.Decimal => classOf[JBigDecimal] + case DFDLPrimType.UnsignedInt => classOf[JLong] + case DFDLPrimType.UnsignedByte => classOf[JShort] + case DFDLPrimType.UnsignedShort => classOf[JInt] + case DFDLPrimType.UnsignedLong => classOf[JBigInt] + case DFDLPrimType.NonNegativeInteger => classOf[JBigInt] + case DFDLPrimType.Double => classOf[JDouble] + case DFDLPrimType.Float => classOf[JFloat] + case DFDLPrimType.HexBinary => classOf[Array[Byte]] + case DFDLPrimType.AnyURI => classOf[java.net.URI] + case DFDLPrimType.Boolean => classOf[JBoolean] + case DFDLPrimType.DateTime => classOf[ICUCalendar] + case DFDLPrimType.Date => classOf[ICUCalendar] + case DFDLPrimType.Time => classOf[ICUCalendar] Review Comment: I am just taking this from the data interface I built for the Drill implementation. For performance reasons I don't think we want to hand back strings and made people parse them to create their own preferred calendar object instances. I am wondering if we should just narrow this to just the truly necessary types for layers: String and integers. Generalizing this to all the simple types seems silly when we have no examples of a layer needing to compute say, a time. ########## daffodil-runtime1/src/main/scala/org/apache/daffodil/runtime1/layers/LayerFactory.scala: ########## @@ -0,0 +1,326 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.daffodil.runtime1.layers + +import java.lang.reflect.Constructor +import java.lang.reflect.Method +import scala.collection.immutable.ListSet +import scala.collection.mutable + +import org.apache.daffodil.lib.exceptions.Assert +import org.apache.daffodil.runtime1.dpath.NodeInfo.PrimType +import org.apache.daffodil.runtime1.infoset.DataValue +import org.apache.daffodil.runtime1.layers.api.Layer +import org.apache.daffodil.runtime1.layers.api.LayerException +import org.apache.daffodil.runtime1.processors.VariableRuntimeData + +object LayerFactory { + + /** cache that maps spiName of layer to the LayerVarsRuntime */ + private lazy val alreadyCheckedLayers = + new mutable.LinkedHashMap[String, LayerVarsRuntime]() + + /** + * Computes the things that can be computed once-only to make calling the constructor + * and passing the parameter variables faster at runtime. This also does all the + * checking that the constructor has an argument for each variable of properly matching type, and + * has a getter for each result variable, again returning the proper type. + * + * This is called at schema compile time to ensure the layer code is properly defined and matches + * the layer variable definitions. + * + * It is called again at runtime after the Layer class is loaded by the SPI + * to ensure that the loaded layer class constructor signature at least matches the layer + * variables defined in the schema. + * @param lrd + * @param protoLayer the layer instance allocated by the SPI loader (zero-arg constructed) + * @return + */ + def computeLayerVarsRuntime(lrd: LayerRuntimeData, protoLayer: Layer): LayerVarsRuntime = { + val optLayerVarsRuntime = alreadyCheckedLayers.get(protoLayer.name()) + optLayerVarsRuntime.getOrElse { + // we know there is a default zero arg constructor + // find another constructor and check that there is an argument + // corresponding to each of the layer variables. + val c = protoLayer.getClass + val allConstructors = c.getConstructors.toSeq + + val constructor: Constructor[_] = + if (allConstructors.length == 1) { + // There is only the default constructor. + // That's ok if there are no variables for the layer, which we check later. + allConstructors.head + } else if (allConstructors.length == 2) { + allConstructors.filter(_.getParameterCount > 0).head + } else { + def tooManyConstructorsMsg: String = { + s"""Layer class $c has multiple non-default constructors. It should have a default (no args) + | constructor and a single additional constructor with arguments for + | each of the layer's parameter variables.""".stripMargin + } + lrd.context.SDE(tooManyConstructorsMsg) + } + + if (lrd.vmap.isEmpty && allConstructors.length == 1) { + // there are no vars, we're done + new LayerVarsRuntime(constructor, Nil, Nil) + } else { + // there is a constructor with args that are supposed to correspond to bound vars + val params = constructor.getParameters.toSeq + val nParams = params.length + val nVars = lrd.vmap.size + + val paramTypes = constructor.getParameterTypes.toSeq + val paramVRDs = params.map { p => + lrd.vmap.getOrElse( + p.getName, + lrd.context.SDE(s"No layer DFDL variable named '$p.getName' was found."), + ) + }.toSeq + + // Now we deal with the result getters and the corresponding vars + // + val allLayerVRDs = ListSet(lrd.vmap.toSeq.map(_._2): _*) + val returnVRDs = allLayerVRDs -- paramVRDs // set subtraction Review Comment: I am documenting this in the Layer class javadoc. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
