org.smooks.edi.edisax.model.EdifactModel Maven / Gradle / Ivy
/*-
* ========================LICENSE_START=================================
* smooks-edi-sax
* %%
* Copyright (C) 2020 Smooks
* %%
* Licensed under the terms of the Apache License Version 2.0, or
* the GNU Lesser General Public License version 3.0 or later.
*
* SPDX-License-Identifier: Apache-2.0 OR LGPL-3.0-or-later
*
* ======================================================================
*
* 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.
*
* ======================================================================
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* =========================LICENSE_END==================================
*/
package org.smooks.edi.edisax.model;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.smooks.assertion.AssertArgument;
import org.smooks.edi.edisax.EDIConfigurationException;
import org.smooks.edi.edisax.EDIParseException;
import org.smooks.edi.edisax.model.internal.Component;
import org.smooks.edi.edisax.model.internal.Field;
import org.smooks.edi.edisax.model.internal.Segment;
import org.smooks.edi.edisax.model.internal.SegmentGroup;
import org.smooks.edi.edisax.model.internal.Delimiters;
import org.smooks.edi.edisax.model.internal.Description;
import org.smooks.edi.edisax.model.internal.Edimap;
import org.smooks.edi.edisax.model.internal.Import;
import org.smooks.resource.URIResourceLocator;
import org.smooks.support.StreamUtils;
import org.xml.sax.SAXException;
/**
* EdifactModel contains all logic for handling imports for the
* edi-message-mapping model.
*/
public class EdifactModel {
private static final URI UNSPECIFIED = URI.create("unspecified");
private Description description;
private String mappingConfig;
private final URI modelURI;
private final URI importBaseURI;
private volatile Edimap edimap;
private Collection associateModels;
/**
* Public Constructor.
* @param edimap Mapping Model.
*/
public EdifactModel(Edimap edimap) {
AssertArgument.isNotNull(edimap, "edimap");
this.edimap = edimap;
modelURI = UNSPECIFIED;
importBaseURI = URIResourceLocator.getSystemBaseURI();
}
/**
* Public Constructor.
*/
public EdifactModel(InputStream mappingModelStream) throws IOException {
AssertArgument.isNotNull(mappingModelStream, "mappingModelStream");
this.importBaseURI = URIResourceLocator.getSystemBaseURI();
modelURI = UNSPECIFIED;
try {
this.mappingConfig = StreamUtils.readStreamAsString(mappingModelStream, "UTF-8");
} finally {
mappingModelStream.close();
}
}
/**
* Public constructor.
* @param modelURI The model resource URI.
* @param importBaseURI The base URI for loading imports.
* @param mappingModelStream The edi-message-mapping.
*/
public EdifactModel(URI modelURI, URI importBaseURI, InputStream mappingModelStream) throws IOException {
AssertArgument.isNotNull(importBaseURI, "importBaseURI");
AssertArgument.isNotNull(mappingModelStream, "mappingModelStream");
this.modelURI = modelURI;
this.importBaseURI = importBaseURI;
try {
this.mappingConfig = StreamUtils.readStreamAsString(mappingModelStream, "UTF-8");
} finally {
mappingModelStream.close();
}
}
/**
* Public constructor.
* @param modelURI The model resource URI.
* @param importBaseURI The base URI for loading imports.
* @param mappingModelStream The edi-message-mapping.
*/
public EdifactModel(URI modelURI, URI importBaseURI, Reader mappingModelStream) throws IOException {
AssertArgument.isNotNull(importBaseURI, "importBaseURI");
AssertArgument.isNotNull(mappingModelStream, "mappingModelStream");
this.modelURI = modelURI;
this.importBaseURI = importBaseURI;
try {
this.mappingConfig = StreamUtils.readStream(mappingModelStream);
} finally {
mappingModelStream.close();
}
}
public void setDescription(Description description) {
this.description = description;
}
public Description getDescription() {
if(description != null) {
return description;
}
return getEdimap().getDescription();
}
/**
* Returns the edimap containing the parser logic.
* @return edi-message-mapping.
*/
public Edimap getEdimap() {
if(edimap == null) {
// Lazy parsing of the Edimap configuration...
try {
parseSequence();
} catch (Exception e) {
throw new EDIConfigurationException("Error parsing EDI Mapping Model [" + mappingConfig + "].", e);
}
}
return edimap;
}
/**
* Returns the delimiters used in edifact format.
* @return delimiters.
*/
public Delimiters getDelimiters() {
return getEdimap().getDelimiters();
}
/**
* Get the model URI.
* @return The model URI.
*/
public URI getModelURI() {
return modelURI;
}
/**
* Set a set of models that are associated with this model instance.
*
* An associate set of models could be (for example) the other models in
* a UN/EDIFACT model set, or some other interchange type.
*
* @param associateModels Associate models.
*/
public void setAssociateModels(Collection associateModels) {
this.associateModels = associateModels;
}
/**
* Set the edifact edimap from the mapping model InputStream.
* @throws EDIParseException is thrown when EdifactModel is unable to initialize edimap.
* @throws EDIConfigurationException is thrown when edi-message-mapping contains multiple or no namespace declaration.
* @throws IOException is thrown when error occurs when parsing edi-message-mapping.
*/
private synchronized void parseSequence() throws EDIConfigurationException, IOException, SAXException {
if(edimap != null) {
return;
}
//To prevent circular dependency the name/url of all imported urls are stored in a dependency tree.
//If a name/url already exists in a parent node, we have a circular dependency.
DependencyTree tree = new DependencyTree();
EDIConfigDigester digester = new EDIConfigDigester(modelURI, importBaseURI);
edimap = digester.digestEDIConfig(new StringReader(mappingConfig));
description = edimap.getDescription();
importFiles(tree.getRoot(), edimap, tree);
}
/**
* Handle all imports for the specified edimap. The parent Node is used by the
* DependencyTree tree to keep track of previous imports for preventing cyclic dependency.
* @param parent The node representing the importing file.
* @param edimap The importing edimap.
* @param tree The DependencyTree for preventing cyclic dependency in import.
* @throws EDIParseException Thrown when a cyclic dependency is detected.
* @throws EDIConfigurationException is thrown when edi-message-mapping contains multiple or no namespace declaration.
* @throws IOException is thrown when error occurs when parsing edi-message-mapping.
*/
private void importFiles(Node parent, Edimap edimap, DependencyTree tree) throws SAXException, EDIConfigurationException, IOException {
Edimap importedEdimap;
Node child, conflictNode;
for (Import imp : edimap.getImports()) {
URI importUri = imp.getResourceURI();
Map importedSegments = null;
child = new Node(importUri.toString());
conflictNode = tree.add(parent, child);
if ( conflictNode != null ) {
throw new EDIParseException(edimap, "Circular dependency encountered in edi-message-mapping with imported files [" + importUri + "] and [" + conflictNode.getValue() + "]");
}
importedSegments = getImportedSegments(importUri);
if(importedSegments == null) {
EDIConfigDigester digester = new EDIConfigDigester(importUri, URIResourceLocator.extractBaseURI(importUri));
importedEdimap = digester.digestEDIConfig(new URIResourceLocator().getResource(importUri.toString()));
importFiles(child, importedEdimap, tree);
importedSegments = createImportMap(importedEdimap);
}
applyImportOnSegments(edimap.getSegments().getSegments(), imp, importedSegments);
}
// Imports have been applied on the segments, so clear them now...
edimap.getImports().clear();
}
private Map getImportedSegments(URI importUri) {
if(associateModels != null) {
for(EdifactModel model : associateModels) {
if(model.getModelURI().equals(importUri)) {
return createImportMap(model.getEdimap());
}
}
}
return null;
}
private void applyImportOnSegments(List segmentGroup, Import imp, Map importedSegments) throws EDIParseException {
for (SegmentGroup segment : segmentGroup) {
if(segment instanceof Segment) {
applyImportOnSegment((Segment)segment, imp, importedSegments);
}
if (segment.getSegments() != null) {
applyImportOnSegments(segment.getSegments(), imp, importedSegments);
}
}
}
/**
* Inserts data from imported segment into the importing segment. Continues through all
* the child segments of the importing segment.
* @param segment the importing segment.
* @param imp import information like url and namespace.
* @param importedSegments the imported segment.
* @throws EDIParseException Thrown when a segref attribute in importing segment contains
* a value not located in the imported segment but with the namespace referencing the imported file.
*/
private void applyImportOnSegment(Segment segment, Import imp, Map importedSegments) throws EDIParseException {
if (segment.getNodeTypeRef() != null && segment.getNodeTypeRef().startsWith(imp.getNamespace()+":")) {
String key = segment.getNodeTypeRef().substring(segment.getNodeTypeRef().indexOf(':') + 1);
Segment importedSegment = importedSegments.get(key);
if (importedSegment == null) {
throw new EDIParseException(edimap, "Referenced segment [" + key + "] does not exist in imported edi-message-mapping [" + imp.getResource() + "]");
}
insertImportedSegmentInfo(segment, importedSegment, imp.isTruncatableSegments(), imp.isTruncatableFields(), imp.isTruncatableComponents());
}
}
/**
* Inserts fields and segments from the imported segment into the importing segment. Also
* overrides the truncatable attributes in Fields and Components of the imported file if
* values are set to true or false in truncatableFields or truncatableComponents.
* @param segment the importing segment.
* @param importedSegment the imported segment.
* @param truncatableFields a global attribute for overriding the truncatable attribute in imported segment.
* @param truncatableComponents a global attribute for overriding the truncatable attribute in imported segment.
*/
private void insertImportedSegmentInfo(Segment segment, Segment importedSegment, Boolean truncatableSegments, Boolean truncatableFields, Boolean truncatableComponents) {
//Overwrite all existing fields in segment, but add additional segments to existing segments.
segment.getFields().addAll(importedSegment.getFields());
segment.setImportXmlTag(importedSegment.getXmltag());
if (importedSegment.getSegments().size() > 0) {
segment.getSegments().addAll(importedSegment.getSegments());
}
//If global truncatable attributes are set in importing mapping, then
//override the attributes in the imported files.
if (truncatableSegments != null) {
segment.setTruncatable(truncatableSegments);
}
if (truncatableFields != null || truncatableComponents != null) {
for ( Field field : segment.getFields()) {
field.setTruncatable(isTruncatable(truncatableFields, field.isTruncatable()));
if ( truncatableComponents != null ) {
for (Component component : field.getComponents()) {
component.setTruncatable(isTruncatable(truncatableComponents, component.isTruncatable()));
}
}
}
}
}
/**
* Creates a Map given an Edimap. All segments in edimap are stored as values in the Map
* with the corresponding segcode as key.
* @param edimap the edimap containing segments to be inserted into Map.
* @return Map containing all segment in edimap.
*/
private Map createImportMap(Edimap edimap) {
HashMap result = new HashMap();
for (SegmentGroup segmentGroup : edimap.getSegments().getSegments()) {
if(segmentGroup instanceof Segment) {
result.put(((Segment)segmentGroup).getSegcode(), (Segment) segmentGroup);
}
}
return result;
}
/**
* Returns truncatable attributes specified in import element in the importing edi-message-mapping
* if it exists. Otherwise it sets value of the truncatable attribute found the imported segment.
* @param truncatableImporting truncatable value found in import element in importing edi-message-mapping.
* @param truncatableImported truncatable value found in imported segment.
* @return truncatable from importing edi-message-mapping if it exists, otherwise return value from imported segment.
*/
private Boolean isTruncatable(Boolean truncatableImporting, boolean truncatableImported) {
Boolean result = truncatableImported;
if (truncatableImporting != null) {
result = truncatableImporting;
}
return result;
}
/************************************************************************
* Private classes used for locating and preventing cyclic dependency. *
************************************************************************/
private class DependencyTree {
Node root;
public DependencyTree() {
root = new Node(null);
}
public Node getRoot() {
return root;
}
/**
* Add child to parent Node if value does not exist in direct path from child to root
* node, i.e. in any ancestralnode.
* @param parent parent node
* @param child the child node to add.
* @return null if the value in child is not in confilct with value in any ancestor Node, otherwise return the conflicting ancestor Node.
*/
public Node add(Node parent, Node child){
Node node = parent;
while (node != null ) {
if (node != root && node.getValue().equals(child.getValue())) {
return node;
}
node = node.getParent();
}
child.setParent(parent);
parent.getChildren().add(child);
return null;
}
public List getUniqueValues() {
List result = new ArrayList();
return getUniqueValuesForNode(root, result);
}
private List getUniqueValuesForNode(Node node, List list) {
if ( node.getValue() != null && !list.contains( node.getValue() ) ) {
list.add(node.getValue());
}
return list;
}
}
private class Node {
private T value;
private Node parent;
private List> children;
public Node(T value) {
children = new ArrayList>();
this.value = value;
}
public T getValue() {
return value;
}
public Node getParent() {
return parent;
}
public void setParent(Node parent) {
this.parent = parent;
}
public List> getChildren() {
return children;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy