-
Notifications
You must be signed in to change notification settings - Fork 15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Support for DMN model imports #215
Changes from all commits
d81dc46
2b48fc0
5e8df0e
37263b3
375bf32
2125117
3e7cc43
f86643c
91533a3
db11fce
472b1cc
c163785
5a209d5
fa99e31
3881147
78e6295
c1f9893
ed57bc7
3607bba
f325e51
c8949d9
9375b2d
d7b25da
7e1f6c9
526b96b
9553a49
c0e920b
52bedf3
ab9e039
b42c9a7
b9ea01a
6bc26be
c4956df
832519f
e33641c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
/* | ||
* Copyright © 2022 Camunda Services GmbH ([email protected]) | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.camunda.dmn.model.xml.instance | ||
|
||
import jdk.dynalink.Namespace | ||
import org.camunda.bpm.model.dmn.DmnModelInstance | ||
import org.camunda.bpm.model.xml.ModelBuilder | ||
import org.camunda.bpm.model.xml.impl.ModelImpl | ||
import org.camunda.bpm.model.xml.instance.DomDocument | ||
|
||
import java.io.InputStream | ||
|
||
/** | ||
* Provides DmnModelInstances via an InputStream or through some other locator | ||
*/ | ||
trait DmnModelInstanceProvider { | ||
|
||
/** | ||
* Attempts to find and load a dmn model for the given namespace, optionally | ||
* using the locator if necessary | ||
* | ||
* @param namespace - the namespace of the dmn model searched for | ||
* @param locator - a hint for the provider about where the model might be found | ||
* @return - a DmnModelInstance if one was located for the given namespace, or None if the model could not be found | ||
*/ | ||
def loadModel[T](namespace: String, locator: Option[T]): Option[DmnModelInstance] | ||
|
||
/** | ||
* Instantiates a DmnModelInstance from the given Input Stream | ||
* | ||
* @param is - the input stream from which a dmn model can be read | ||
* @return | ||
*/ | ||
def readModelFromStream(is: InputStream): DmnModelInstance | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
/* | ||
* Copyright © 2022 Camunda Services GmbH ([email protected]) | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.camunda.dmn.model.xml.instance | ||
|
||
import org.camunda.bpm.model.dmn.impl.DmnModelInstanceImpl | ||
import org.camunda.bpm.model.dmn.impl.DmnModelConstants | ||
import org.camunda.bpm.model.xml.impl.ModelImpl | ||
import org.camunda.bpm.model.xml.{ModelBuilder, ModelException} | ||
import org.camunda.bpm.model.xml.instance.DomDocument | ||
import org.camunda.bpm.model.xml.instance.ModelElementInstance | ||
import org.camunda.bpm.model.dmn.instance.{DrgElement, NamedElement, Variable} | ||
import org.camunda.bpm.model.xml.`type`.ModelElementType | ||
import org.slf4j.LoggerFactory | ||
|
||
import java.util | ||
import scala.jdk.CollectionConverters._ | ||
import scala.util.{Failure, Success, Try} | ||
|
||
object ImportAwareDmnModelInstanceImpl { | ||
|
||
val dmnNameSpaces: List[String] = List( | ||
DmnModelConstants.DMN12_NS, | ||
DmnModelConstants.DMN12_NS, | ||
DmnModelConstants.DMN13_NS, | ||
DmnModelConstants.DMN11_ALTERNATIVE_NS, | ||
DmnModelConstants.DMN13_ALTERNATIVE_NS | ||
).map(_.toLowerCase()) | ||
|
||
/** | ||
* Allows an element's name to be qualified by the (import) name of the model that defines the element | ||
* | ||
* @param modelElement | ||
*/ | ||
implicit class ModelQualifiedElementName(modelElement: NamedElement) { | ||
def qualifiedName: String = { | ||
Comment on lines
+47
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❌ We should avoid implicit classes. They can be hard to understand and need to be migrated for Scala 3. |
||
((modelElement.getModelInstance, modelElement) match { | ||
case (m: ImportAwareDmnModelInstanceImpl, e: DrgElement) => m.importedModelName | ||
case (m: ImportAwareDmnModelInstanceImpl, v: Variable) => m.importedModelName | ||
case (_, _) => None | ||
}) match { | ||
case Some(qualifier) => s"$qualifier.${modelElement.getName}" | ||
case None => modelElement.getName | ||
} | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* A dmn model instance that is able to resolve elements existing in the DRG that were imported from other dmn models. | ||
*/ | ||
|
||
class ImportAwareDmnModelInstanceImpl (model: ModelImpl, | ||
modelBuilder: ModelBuilder, | ||
document: DomDocument, | ||
private val dmnModelInstanceProvider: DmnModelInstanceProvider) | ||
extends DmnModelInstanceImpl(model, modelBuilder, document) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤔 This is an interesting idea. By extending the However, I would prefer composition over inheritance. The |
||
import ImportAwareDmnModelInstanceImpl._ | ||
|
||
private var importedModelName: Option[String] = None | ||
|
||
def withImportedModelName(modelName: Option[String]): ImportAwareDmnModelInstanceImpl = { | ||
val copy = clone() | ||
copy.importedModelName = modelName; | ||
copy | ||
} | ||
|
||
/** | ||
* The collection of imported models to be loaded once when required | ||
*/ | ||
private val importedModels = loadImports | ||
|
||
private def loadImports = getDefinitions.getImports.asScala | ||
.filter(id => dmnNameSpaces.contains(id.getImportType.toLowerCase())) | ||
.flatMap(id => | ||
Try(dmnModelInstanceProvider.loadModel(id.getNamespace, Option(id.getLocationUri)) | ||
.map { | ||
case iam: ImportAwareDmnModelInstanceImpl => | ||
iam.withImportedModelName(Option(id.getAttributeValue("name"))) | ||
case other => other | ||
}) match { | ||
case Success(m) => m | ||
case Failure(exception) => | ||
val errorMsg = s"Unable to load imported model at location ${id.getLocationUri} " + | ||
s"for name ${id.getAttributeValue("name")}" | ||
LoggerFactory.getLogger(getClass).error(errorMsg, exception) | ||
throw new ModelException(errorMsg, exception) | ||
}).toSeq | ||
|
||
/** | ||
* Gets a model element from the model or an imported model whose id matches the given id | ||
* | ||
* @param id | ||
* @tparam T | ||
* @return | ||
*/ | ||
override def getModelElementById[T <: ModelElementInstance](id: String): T = { | ||
var element = super.getModelElementById[T](id) | ||
if (element == null) { | ||
importedModels.map(_.getModelElementById[T](id)).find(e => e != null) match { | ||
case Some(value) => element = value | ||
case None => //nothing to do | ||
} | ||
} | ||
element | ||
} | ||
|
||
/** | ||
* Gets all elements of the model and its imported models | ||
* | ||
* @param referencingClass - the type of element to be retrieved | ||
* @tparam T | ||
* @return | ||
*/ | ||
override def getModelElementsByType[T <: ModelElementInstance](referencingClass: Class[T]): util.Collection[T] = { | ||
(super.getModelElementsByType[T](referencingClass).asScala ++ | ||
importedModels.flatMap(_.getModelElementsByType[T](referencingClass).asScala)).asJavaCollection | ||
} | ||
|
||
override def getModelElementsByType(`type`: ModelElementType): util.Collection[ModelElementInstance] = { | ||
(super.getModelElementsByType(`type`).asScala ++ | ||
importedModels.flatMap(_.getModelElementsByType(`type`).asScala)).asJavaCollection | ||
} | ||
override def clone(): ImportAwareDmnModelInstanceImpl = { | ||
val superClone = super.clone() | ||
new ImportAwareDmnModelInstanceImpl(model, modelBuilder, superClone.getDocument, dmnModelInstanceProvider) | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
/* | ||
* Copyright © 2022 Camunda Services GmbH ([email protected]) | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.camunda.dmn.model.xml.instance | ||
|
||
import org.camunda.bpm.model.dmn.impl.{DmnModelInstanceImpl, DmnParser} | ||
import org.camunda.bpm.model.dmn.{Dmn, DmnModelInstance} | ||
import org.camunda.bpm.model.xml.impl.ModelImpl | ||
import org.camunda.bpm.model.xml.instance.DomDocument | ||
|
||
import java.io.InputStream | ||
|
||
/** | ||
* A DmnModelInstanceProvider that maintains a list of all models that it has already been used to load, | ||
* which can then be retrieved by namespace | ||
*/ | ||
class StatefulDmnModelInstanceProvider extends DmnParser with DmnModelInstanceProvider { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔧 We should favor composition over inheritance. For example, by injecting the provider in the parser. |
||
|
||
val loadedModels: scala.collection.mutable.Map[String, DmnModelInstance] = scala.collection.mutable.Map.empty | ||
|
||
/** | ||
* Retrieves the dmn model | ||
* @param namespace - the namespace of the dmn model searched for | ||
* @param locator - a hint for the provider about where the model might be found. In this case, returns none if locator is not an InputStream | ||
* @return - a DmnModelInstance if one was located for the given namespace, or None if the model could not be found | ||
*/ | ||
override def loadModel[T](namespace: String, locator: Option[T] = None): Option[DmnModelInstance] = { | ||
if (!loadedModels.contains(namespace)) { | ||
None | ||
} else { | ||
loadedModels.get(namespace) | ||
} | ||
} | ||
|
||
override def readModelFromStream(is: InputStream): DmnModelInstance = { | ||
Option(parseModelFromStream(is)) | ||
.map(modelInstance => { | ||
loadedModels.put( | ||
modelInstance.getDefinitions.getNamespace, | ||
modelInstance) | ||
modelInstance | ||
}).orNull | ||
} | ||
|
||
override def createModelInstance(document: DomDocument): DmnModelInstanceImpl = { | ||
new ImportAwareDmnModelInstanceImpl( | ||
Dmn.INSTANCE.getDmnModel.asInstanceOf[ModelImpl], | ||
Dmn.INSTANCE.getDmnModelBuilder, | ||
document, | ||
this) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
/* | ||
* Copyright © 2022 Camunda Services GmbH ([email protected]) | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.camunda.dmn.model.xml.instance | ||
|
||
import org.camunda.bpm.model.dmn.{Dmn, DmnModelInstance} | ||
import org.camunda.bpm.model.xml.impl.ModelImpl | ||
|
||
import java.io.InputStream | ||
import java.net.URI | ||
import scala.reflect.ClassTag.Nothing | ||
|
||
object URILocatorDmnModelInstanceProvider { | ||
/** | ||
* Attempts to open an input stream on a resource at the given uri. If the uri is absolute, then an attempt is made | ||
* to open the stream at that location. If the URI is relative, then the resource is assumed to be a classpath | ||
* resource, and an attempt will be made to locate it on the classpath. | ||
* | ||
* @param uri - A java.net.URI suitable for locating a resource | ||
*/ | ||
implicit class URItoStream(val uri: URI) { | ||
def inputStream: InputStream = if (uri.isAbsolute) { | ||
uri.toURL.openStream() | ||
} else { | ||
ClassLoader.getSystemResourceAsStream(s"${uri.getPath}") | ||
} | ||
} | ||
} | ||
|
||
class URILocatorDmnModelInstanceProvider extends StatefulDmnModelInstanceProvider { | ||
import org.camunda.dmn.model.xml.instance.URILocatorDmnModelInstanceProvider._ | ||
|
||
/** | ||
* Loads a model from a URI given by the locator, or returns a model already loaded | ||
* | ||
* @param namespace - the namespace of the dmn model searched for | ||
* @param locator - An absolute URI or a path to a location on the classpath where the model can be found | ||
* @tparam T | ||
* @return - a DmnModelInstance if one was located for the given namespace, or None if the model could not be found | ||
*/ | ||
override def loadModel[T](namespace: String, locator: Option[T]): Option[DmnModelInstance] = { | ||
super.loadModel(namespace, locator) | ||
.orElse(locator.map { | ||
case s: String => readModelFromStream(URI.create(s).inputStream) | ||
case uri: URI => readModelFromStream(uri.inputStream) | ||
case _ => null | ||
}) | ||
} | ||
Comment on lines
+53
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❌ It feels insecure to load an arbitrary URL from the DMN XML. |
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔧 This variable is never used. We can delete it.