blob: 782dc3fb8a89dcdb51df5e13cd692823c29e3492 [file] [log] [blame]
/*
* 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.dsom
import scala.xml.Node
import org.apache.daffodil.exceptions.Assert
import org.apache.daffodil.xml.XMLUtils
import org.apache.daffodil.xml.NS
import org.apache.daffodil.equality._
import org.apache.daffodil.schema.annotation.props.PropertyLookupResult
import org.apache.daffodil.schema.annotation.props.NotFound
import org.apache.daffodil.schema.annotation.props.Found
import org.apache.daffodil.schema.annotation.props.FindPropertyMixin
import org.apache.daffodil.api.WarnID
import scala.collection.mutable
/**
* Only objects from which we generate processors (parsers/unparsers)
* can lookup scoped property values.
*
* This avoids the possibility of a property being resolved incorrectly by
* not looking at the complete chain of schema components contributing to the
* property resolution.
*
* The only objects that should resolve properties are instances of Term
* ElementRef, Root, LocalElementDecl, Sequence, Choice, SequenceRef, ChoiceRef,
* EnumerationFactory, SimpleTypeDefFactory,
*
* All "real" terms are able to resolve properties. Most other objects just contribute
* properties to the mix, but they are not points where properties are
* used to generate processors.
*
* EnumerationFactory and SimpleTypeDefFactory are the oddballs out. In addition to
* being used to generate processors, these classes our also used to generate abstract
* TypeCalculators, which are not necessarily attached to any particular element, nor
* used to generate any processor (for instance, there may be a globalSimpleType whose
* only purpose is to define a TypeCalculator for use in DPath expressions)
*/
object ResolvesProperties {
/**
* List of properties that when looked up should only be found on the
* immediate element--properties on a reference or defaults should not be
* taken into account.
*/
val localOnlyProperties = Seq(
"choiceBranchKey",
"hiddenGroupRef",
"inputTypeCalc",
"outputTypeCalc",
"repType",
"repValueRanges",
"repValues")
}
/**
* Mixin for non-terms that need to lookup local properties
*/
trait ResolvesLocalProperties
extends FindPropertyMixin { self: AnnotatedSchemaComponent =>
/**
* Does lookup of only local properties
*/
protected override def lookupProperty(pname: String): PropertyLookupResult = {
Assert.usage(
ResolvesProperties.localOnlyProperties.contains(pname),
"Property '%s' is not a valid local-only property.".format(pname))
val fa = formatAnnotation
val opt = fa.justThisOneProperties.get(pname)
val optFound = opt.map { case (value, location) => Found(value, location, pname, false) }
val res = optFound.getOrElse { NotFound(Seq(fa.annotatedSC), Nil, pname) }
res
}
}
/**
* Mixin for Term, which can lookup all properties using DFDL scoping rules.
*/
trait ResolvesScopedProperties
extends FindPropertyMixin { self: Term =>
private def findNonDefaultProperty(pname: String): PropertyLookupResult = {
val sources =
if (ResolvesProperties.localOnlyProperties.contains(pname)) {
Seq(nonDefaultFormatChain)
} else {
nonDefaultPropertySources
}
val result = findPropertyInSources(pname, sources)
result match {
case f: Found => f
case NotFound(nd, d, _) =>
Assert.invariant(d.isEmpty)
}
result
}
private def findDefaultProperty(pname: String): PropertyLookupResult = {
val result = findPropertyInSources(pname, defaultPropertySources)
val fixup = result match {
case Found(value, loc, pname, _) =>
// found as a default property.
// supply constructor's last arg is boolean indicating it's a default property
Found(value, loc, pname, true)
case NotFound(nd, d, pn) =>
Assert.invariant(d.isEmpty)
NotFound(Seq(), nd, pn) // we want the places we searched shown as default locations searched
}
fixup
}
private def findPropertyInSources(
pname: String,
sources: Seq[ChainPropProvider]): PropertyLookupResult = {
//
// Important - use of stream here insures we don't lookup
// properties down the chain once we have them here.
//
val str = sources.toStream.map { _.chainFindProperty(pname) }
val optFound = str.collectFirst { case found: Found => found }
val result = optFound match {
case Some(f: Found) => f
case None => {
val seq = str.toSeq
// merge all the NotFound stuff.
val nonDefaults = seq.flatMap {
case NotFound(nd, d, _) => nd
case _: Found => Assert.invariantFailed()
}
val defaults = seq.flatMap {
case NotFound(nd, d, _) => d
case _: Found => Assert.invariantFailed()
}
Assert.invariant(defaults.isEmpty)
val nf = NotFound(nonDefaults, defaults, pname)
nf
}
}
result
}
/**
* Does lookup of property using DFDL scoping rules, checking first non-default
* properties, then default property locations.
*/
protected override def lookupProperty(pname: String): PropertyLookupResult = {
// first try in regular properties
val regularResult = findNonDefaultProperty(pname)
regularResult match {
case f: Found => f
case nf @ NotFound(nonDefaultLocsTried1, defaultLocsTried1, _) => {
if (ResolvesProperties.localOnlyProperties.contains(pname)) {
nf
} else {
Assert.invariant(defaultLocsTried1.isEmpty)
val defaultResult = findDefaultProperty(pname)
defaultResult match {
case f: Found => f
case NotFound(nonDefaultLocsTried2, defaultLocsTried2, _) => {
Assert.invariant(nonDefaultLocsTried2.isEmpty)
// did not find it at all. Return a NotFound with all the places we
// looked non-default and default.
val nonDefaultPlaces = nonDefaultLocsTried1
val defaultPlaces = defaultLocsTried2
NotFound(nonDefaultPlaces, defaultPlaces, pname)
}
}
}
}
}
}
}
/** Convenience class for implemening AnnotatedSchemaComponent trait */
abstract class AnnotatedSchemaComponentImpl(
final override val xml: Node,
final override val optLexicalParent: Option[SchemaComponent])
extends AnnotatedSchemaComponent {
def this(xml: Node, lexicalParent: SchemaComponent) =
this(xml, Option(lexicalParent))
}
/**
* Identifies the property environment of a term.
*
* If two terms have the same propEnv and same def/decl, then some things can be shared
* about their implementation.
*
* If they have different propEnv, then all bets are off.
*
* Can be used as a key for whether to create new instance in a factory, or whether one
* can be re-used because the property environment is the same.
*
* The trick is that the regular objects which carry properties have location information
* unique to where they originate (for diagnostic messaging purposes).
*
* What we want is pure value based - depends on the properties and their values only.
*
* The components are of type Seq[Set[String, String]] because the machinery below has
* alredy converted dfdl:ref="q:name" format references into references to objects having
* property resolvers. It is easiest just to extract the string-to-string information rather than
* trying to reuse the named format objects directly.
*
* These Seq[Set[String, String]] are conceptually equivalent to just property sets,
* but keeping them ordered like this preserves some information for us which
* might be useful in debugging/maintenance of the code. the first set in
* the sequence are the local properties excluding dfdl:ref, the next set is those from
* the dfdl:ref, and the next from the next dfdl:ref, and so on. For purposes of
* a hash key we don't care that they are flattened. that happens elsewhere. It will
* be much easier to see why two PropEnv objects are not equal looking at this than an
* entirely flattened set of properties, most of which aren't relevant.
*
* optNext is the optional next schema component. So for an element reference, it is the
* global element declaration. For a group reference, it is the group definition. It is the
* next place we would get properties from (and defaults from) for property resolution.
*/
case class PropEnv(
localProps: Seq[Set[(String, String)]],
defaultPropSource: Seq[Set[(String, String)]],
optNext: Option[scala.xml.Node])
case class ShareKey(
xml: scala.xml.Node,
env: PropEnv) {
/**
* It is critical here that we perform reference equality on the XML. This is
* because it is possible that the same piece of XML with the same properties
* could appear in two completely different contexts. If we perform object
* equality, then we might end up sharing parts of the grammar that shouldn't
* be shared just because they look the same. By performing reference
* equality on the XML, we ensure that we only share parts of grammar that
* are actually the same.
*/
override def equals(that: Any): Boolean = {
that match {
case that: ShareKey => (this.xml eq that.xml) && (this.env == that.env)
case _ => false
}
}
}
/**
* Shared object factory/cache.
*
* Creates objects for new keys, shares previously computed ones (and avoids
* recomputing them) for existing keys.
*/
final class SharedFactory[SharedType] {
private val vals = new mutable.HashMap[ShareKey, SharedType]
/**
* The passing of the value argument by name is critical here, as
* we want to avoid evaluating that at all when the key is one
* we have already seen.
*/
final def getShared(key: ShareKey, valueArg: => SharedType): SharedType = {
lazy val value = valueArg // once only
val opt = vals.get(key)
opt match {
case Some(y) => y
case None => {
val shared = value
vals.put(key, shared)
shared
}
}
}
}
/**
* Shared characteristics of any annotated schema component.
*
* Not all components can carry DFDL annotations.
*/
trait AnnotatedSchemaComponent
extends SchemaComponent
with AnnotatedMixin
with OverlapCheckMixin {
protected override def initialize(): Unit = {
super.initialize()
}
/**
* If two terms have the same propEnv, they have identical properties
* in scope.
*/
private lazy val propEnv: PropEnv = {
val localPropsSets = this.nonDefaultFormatChain.propertyPairsSets
val defaultPropObj = this.defaultFormatChain.propertyPairsSets
val next = refersToForPropertyCombining
PropEnv(localPropsSets, defaultPropObj, next.map { _.xml })
}
/**
* The thing that provides the actual definition. If this is a local
* element decl or local sequence/choice, then this is the thing,
* if this is some sort of a ref, then the referenced definition is the thing.
*/
private lazy val actualDef: AnnotatedSchemaComponent = {
this match {
case gr: GroupRef => gr.groupDef
case aer: AbstractElementRef => aer.referencedElement
case eb: ElementBase => eb
case grl: GroupDefLike => grl
}
}
protected final lazy val shareKey = ShareKey(actualDef.xml, propEnv)
/**
* For property combining only. E.g., doesn't refer from an element
* to its complex type because we don't combine properties with that
* in DFDL v1.0. (I consider that a language design bug in DFDL v1.0, but
* that is the way it's defined.)
*/
final protected def refersToForPropertyCombining: Option[AnnotatedSchemaComponent] = optReferredToComponent
def optReferredToComponent: Option[AnnotatedSchemaComponent] // override in ref objects
lazy val nonDefaultPropertySources: Seq[ChainPropProvider] = LV('nonDefaultPropertySources) {
this match {
case sd: SchemaDocument => Nil
case _ => {
val refTo = refersToForPropertyCombining
val chainFromReferredTo = refTo.map { c =>
val ndps = c.nonDefaultPropertySources
ndps
}.toSeq.flatten
val myNDFC = nonDefaultFormatChain
val completeNonDefaultFormatChain =
myNDFC +: chainFromReferredTo
val seq = completeNonDefaultFormatChain.distinct
checkNonOverlap(seq)
seq
}
}
}.value
final protected lazy val defaultPropertySources: Seq[ChainPropProvider] = LV('defaultPropertySources) {
val refTo = refersToForPropertyCombining
val chainFromReferredTo = refTo.toSeq.map { _.defaultPropertySources }.distinct.flatten
val completeDefaultFormatChain =
defaultFormatChain +: chainFromReferredTo
val seq = completeDefaultFormatChain.distinct
seq
}.value
final protected lazy val nonDefaultFormatChain: ChainPropProvider = {
val fa = formatAnnotation
val fc = fa.formatChain
fc
}
final protected lazy val defaultFormatChain: ChainPropProvider = {
val res = schemaDocument.formatAnnotation.formatChain
res
}
}
/**
* Every component that can be annotated.
* Review Note:
* It's no longer clear that this separation is strictly speaking needed.
* It's possible that this could be collapsed back into AnnotatedSchemaComponent
* or made smaller anyway.
*
*/
trait AnnotatedMixin { self: AnnotatedSchemaComponent =>
/**
* Anything annotated must be able to construct the
* appropriate DFDLAnnotation object from the xml.
*/
protected def annotationFactory(node: Node): Option[DFDLAnnotation]
private lazy val annotationNode = {
val ann = xml \ "annotation"
ann
}
private def isDfdlNamespace(ns: String): Boolean = ns.contains("ogf") && ns.contains("dfdl")
lazy val dfdlAppInfos = {
val ais = (annotationNode \ "appinfo")
val dais = ais.filter { ai =>
{
ai.attribute("source") match {
case None => {
// if a child node in the dfdl namespace exists we will provide a warning about using the source property
ai.child.flatMap(n => Option(n.namespace)).find(isDfdlNamespace).foreach { _ =>
SDW(WarnID.AppinfoNoSource,
"""xs:appinfo without source attribute. Is source="http://www.ogf.org/dfdl/" missing?"""
)
}
false
}
case Some(n) => {
val sourceNS = NS(n.text)
//
// Keep in mind. As the DFDL standard evolves, and new versions
// come out, this code may change to tolerate different source
// attributes that call out distinct versions of the standard.
//
val officialAppinfoSourceAttributeNS = XMLUtils.dfdlAppinfoSource
//
// Note: use of the strongly typed =:= operator below.
//
// I got sick of mysterious behavior where turns out we are
// comparing two things of different types.
//
// This fixes a bug where we were comparing a string to a NS
// and getting false, where the types should have been the same.
//
val hasRightSource = (sourceNS =:= officialAppinfoSourceAttributeNS)
val isAcceptable = isDfdlNamespace(sourceNS.toString)
schemaDefinitionWarningWhen(WarnID.AppinfoDFDLSourceWrong, !hasRightSource && isAcceptable,
"The xs:appinfo source attribute value '%s' should be '%s'.", sourceNS, officialAppinfoSourceAttributeNS)
(hasRightSource || isAcceptable)
}
}
}
}
dais
}
/**
* The DFDL annotations on the component, as objects
* that are subtypes of DFDLAnnotation.
*/
final lazy val annotationObjs = {
val objs = dfdlAppInfos.flatMap { dai =>
{
val children = dai.child
val res = children.filter { _.isInstanceOf[scala.xml.Elem] }.flatMap { child =>
{
annotationFactory(child)
}
}
res
}
}
objs
}
/**
* Here we establish an invariant which is that every annotatable schema component has, definitely, has an
* annotation object. It may have no properties on it, but it will be there. Hence, we can
* delegate various property-related attribute calculations to it.
*
* To realize this, every concrete class must implement (or inherit) an implementation of
* emptyFormatFactory, which constructs an empty format annotation,
* and isMyFormatAnnotation which tests if an annotation is the corresponding kind.
*
* Given that, formatAnnotation then either finds the right annotation, or constructs one, but our invariant
* is imposed. There *is* a formatAnnotation.
*/
protected def emptyFormatFactory: DFDLFormatAnnotation
final lazy val formatAnnotationExpectedName = emptyFormatFactory.xml.asInstanceOf[scala.xml.Elem].label
protected def isMyFormatAnnotation(a: DFDLAnnotation): Boolean
final lazy val formatAnnotation = {
val format = annotationObjs.collect { case fa: DFDLFormatAnnotation if isMyFormatAnnotation(fa) => fa }
val res = format match {
case Seq() => emptyFormatFactory // does make things with the right namespace scopes attached!
case Seq(fa) => fa
case _ => schemaDefinitionError("Only one format annotation is allowed at each annotation point.")
}
res
}
}