XDocumentImpl.java
/*
** Module : XDocumentImpl
** Abstract : DOM XML Document resource type implementation.
**
** Copyright (c) 2013-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 EVL 20130204 Created initial version. Major rework to use w3c backend.
** 002 OM 20130304 Refactored isValid and isUnknown of WrappedResource to valid and unknown.
** 003 CA 20130308 Refactored as XmlFactory APIs now receive a handle instance instead of
** returning a new one.
** 004 EVK 20130531 Adding support PUBLIC-ID, SYSTEM-ID, ENCODING,
** SCHEMA-LOCATION, SCHEMA-PATH attributes and
** INITIALIZE-DOCUMENT-TYPE,IMPORT-NODE methods.
** Implement valid(),unknown(),getNode() methods.
** Add implementation for CREATE-NODE,GET-DOCUMENT-ELEMENT,
** 005 CA 20130930 Changes related to determining resource type from LegacyResource annotation.
** 006 SVL 20131203 Changes caused by renaming of Nameable.get/setName to Nameable.name.
** 007 HC 20131215 Implemented UNIQUE-ID attribute.
** 008 EVK 20140113 Removed method valid(moved to XCommonImpl class) and implemented
** resourceDelete method to properly remove the object.
** 009 ECF 20140312 Fixed save for memptr mode: use direct assign instead of setBytes.
** 010 MAG 20140625 Adding importNode method with Java boolean parameter.
** 011 GES 20150209 NPE protection logic added to resource delete. longchar support added to
** load(). load() is pretty badly broken and needs work, see TODOs there.
** 012 OM 20160331 Moved SAVE and LOAD file operation on client side. Improved error handling for
** these operations.
** 012 CA 20160428 When parsing a byte stream from a memptr, the content is read only until '\0'
** is encountered - everything else after the first '\0' will be discarded;
** otherwise, SAX parser will throw 'Content is not allowed in trailing section.'
** exception.
** Added a c'tor to create a X-DOCUMENT resource from an existing Document.
** Added support for X-DOCUMENT:SAVE("LONGCHAR", lcvar) mode.
** 013 HC 20160502 Javadoc fix.
** 014 ECF 20171101 Replaced internal com.sun imports with org.apache equivalents.
** 015 CA 20180520 Added all-string initializeDocumentType version.
** 016 OM 20201120 Renamed NAMESPACE-PREFIX setter to fit buffer attributes naming convention.
** OM 20210309 Removed deprecation warnings.
** OM 20210312 Fixed error handling when LOADing from a LONGCHAR stream.
** ME 20210326 Remove last trailing null char, if found, when loading data from memptr.
** CA 20210709 XML parse must use a pristine byte stream and use the reader to decode it (using the
** encoding as specified at the XML prolog).
** CA 20220127 Empty CDATA sections must be emitted for legacy DOM serialization - added a new
** XmlHelper.writeLegacyCompatible which uses W3C DOM to serialize the XML.
** CA 20220211 Fixed setEncoding - find the Charset alias which is IANA-compatible (a side-effect moving
** to xerces implementation in CA/20220127, in XmlHelper.writeLegacyCompatible).
** TJD 20220331 Exchanged DocumentImpl.setEncoding to DocumentImpl.setEncoding
** CA 20230116 Avoid using handle.unwrap, handle.getReference or other BDT usage from within FWD runtime.
** 017 CA 20230720 'isSuppressNamespaceProcessing' needs to be negated when passed to 'XmlHelper.parse', to
** properly report LOCAL-NAME attribute without the namespace prefix.
** 018 CA 20240325 Fixed support for NONAMESPACE-SCHEMA-LOCATION, ADD-SCHEMA-LOCATION, SCHEMA-PATH for XML
** document load.
** 019 ICP 20250129 Used logical constants to leverage cached instances.
*/
/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero 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 Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
** Under Section 7 of the GNU Affero GPL version 3, the following additional
** terms apply to the works covered under the License. These additional terms
** are non-permissive additional terms allowed under Section 7 of the GNU
** Affero GPL version 3 and may not be removed by you.
**
** 0. Attribution Requirement.
**
** You must preserve all legal notices or author attributions in the covered
** work or Appropriate Legal Notices displayed by works containing the covered
** work. You may not remove from the covered work any author or developer
** credit already included within the covered work.
**
** 1. No License To Use Trademarks.
**
** This license does not grant any license or rights to use the trademarks
** Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
** of Golden Code Development Corporation. You are not authorized to use the
** name Golden Code, FWD, or the names of any author or contributor, for
** publicity purposes without written authorization.
**
** 2. No Misrepresentation of Affiliation.
**
** You may not represent yourself as Golden Code Development Corporation or FWD.
**
** You may not represent yourself for publicity purposes as associated with
** Golden Code Development Corporation, FWD, or any author or contributor to
** the covered work, without written authorization.
**
** 3. No Misrepresentation of Source or Origin.
**
** You may not represent the covered work as solely your work. All modified
** versions of the covered work must be marked in a reasonable way to make it
** clear that the modified work is not originating from Golden Code Development
** Corporation or FWD. All modified versions must contain the notices of
** attribution required in this license.
*/
package com.goldencode.p2j.xml;
import com.goldencode.p2j.util.*;
import com.goldencode.p2j.util.UniqueIdGenerator.IdKind;
import com.goldencode.util.*;
import org.apache.xerces.dom.*;
import org.apache.xerces.impl.*;
import org.apache.xerces.util.*;
import org.w3c.dom.*;
import org.w3c.dom.traversal.*;
import org.xml.sax.*;
import javax.xml.*;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.*;
import java.io.*;
import java.nio.charset.*;
import java.util.*;
/**
* Implementation of the X-Document specific features of the Progress DOM XML
* support objects.
*/
public class XDocumentImpl
extends XEntityImpl
implements XDocument
{
/** The real document to assign with handle. */
private Document xdoc;
/** The flag to maintain suppress namespace processing. */
private boolean isSuppressNamespaceProcessing = false;
/** Schema paths locations variable */
private String schemaPaths = null;
/** The value of the NONAMESPACE-SCHEMA-LOCATION attribute. */
private String noNamespaceSchemaLocation = null;
/** The added schema locations, per namespace URI. */
private Map<String, String> schemaLocations = new HashMap<>();
/**
* Associate this X-DOCUMENT resource with an existing document
*
* @param xdoc
* The real document to assign with handle.
*/
public XDocumentImpl(Document xdoc)
{
super(IdKind.X_DOCUMENT);
this.xdoc = xdoc;
}
/**
* Default constructor.
*/
public XDocumentImpl()
{
super(IdKind.X_DOCUMENT);
try
{
xdoc = XmlHelper.newDocument();
}
catch (ParserConfigurationException e)
{
// impossible error
xdoc = null;
}
}
/**
* Getting the Element backend object for this XEntity.
*
* @return The Element associated with object.
*/
@Override
public Node getNode()
{
return xdoc;
}
/**
* Delete the resource.
*
* @return <code>true</code> if the resource was deleted.
*/
@Override
protected boolean resourceDelete()
{
if (xdoc == null)
{
isValid = false;
return true;
}
DocumentTraversal traversal = (DocumentTraversal) xdoc;
if (xdoc.getDocumentElement() != null)
{
NodeIterator iterator = traversal.createNodeIterator(xdoc.getDocumentElement(),
NodeFilter.SHOW_ELEMENT,
null,
true);
for (Node node = iterator.nextNode(); node != null; node = iterator.nextNode())
{
node.setUserData(XEntityImpl.IS_INVALID_NODE, true, null);
}
}
isValid = false;
xdoc.setUserData(XEntityImpl.IS_INVALID_NODE, true, null);
xdoc = null;
return true;
}
/**
* Setter for NAMESPACE-PREFIX attribute.
*
* @param prefix
* The new value of the NAMESPACE-PREFIX attribute.
*/
@Override
public void namespacePrefix(character prefix)
{
readOnlyError("NAMESPACE-PREFIX", handle.UNKNOWN_ARGUMENT);
}
/**
* Get the <code>name</code> attribute of handle.
*
* @return See above.
*/
@Override
public character name()
{
if (!isInitialized())
{
invalidNodeError("NAME");
return new character();
}
return new character("");
}
/**
* Get the <code>name</code> attribute of handle as a string.
*
* @return See above.
*/
@Override
public String _name()
{
return name().toJavaType();
}
/**
* Creating the new node within the current Document object.
*
* @param hChild
* The valid handle class instance of the child to be created and
* returned on method call to use further.
* @param name
* The name of the node to be created.
* @param type
* The type of the node to be created.
*
* @return The <code>true</code> in case of success
* <code>false</code> otherwise.
*/
@Override
public logical createNode(handle hChild, String name, String type)
{
return createNode(hChild, new character(name), new character(type));
}
/**
* Creating the new node within the current Document object.
*
* @param hChild
* The valid handle class instance of the child to be created and
* returned on method call to use further.
* @param name
* The name of the node to be created.
* @param type
* The type of the node to be created.
*
* @return The <code>true</code> in case of success
* <code>false</code> otherwise.
*/
@Override
public logical createNode(handle hChild, character name, String type)
{
return createNode(hChild, name, new character(type));
}
/**
* Creating the new node within the current Document object.
*
* @param hChild
* The valid handle class instance of the child to be created and
* returned on method call to use further.
* @param name
* The name of the node to be created.
* @param type
* The type of the node to be created.
*
* @return The <code>true</code> in case of success
* <code>false</code> otherwise.
*/
@Override
public logical createNode(handle hChild, String name, character type)
{
return createNode(hChild, new character(name), type);
}
/**
* Creating the new node within the current Document object.
*
* @param hChild
* The valid handle class instance of the child to be created and
* returned on method call to use further.
* @param name
* The name of the node to be created.
* @param type
* The type of the node to be created.
*
* @return The <code>true</code> in case of success <code>false</code> otherwise.
*/
@Override
public logical createNode(handle hChild, character name, character type)
{
if (!isInitialized())
{
invalidNodeError("CREATE-NODE");
return new logical(false);
}
if (!isXNodeHandle(hChild))
{
invalidArgumentError("CREATE-NODE");
return new logical(false);
}
try
{
XmlFactory.createXNodeReference(hChild, new handle(this), name, type);
}
catch (DOMException e)
{
if (e.code == DOMException.INVALID_CHARACTER_ERR)
{
commonDomError("CREATE-NODE", "An invalid character was specified");
}
else if (e.code == DOMException.NAMESPACE_ERR)
{
commonDomError("CREATE-NODE", "XML Parser not initialized");
}
else if (e.code == DOMException.SYNTAX_ERR)
{
unsupportedDomError(type.toStringMessage());
invalidArgumentError("CREATE-NODE");
}
else
{
unsupportedDomError(type.toStringMessage());
}
return new logical(false);
}
return new logical(true);
}
/**
* Creating the new node within the current Document object using different
* approach based on the name space URI.
*
* @param hChild
* The valid handle class instance of the child to be created and
* returned on method call to use further.
* @param uri
* The URI namespace of the node to be created.
* @param name
* The name of the node to be created.
* @param type
* The type of the node to be created.
*
* @return The <code>true</code> in case of success
* <code>false</code> otherwise.
*/
@Override
public logical createNodeNamespace(handle hChild, String uri, String name, String type)
{
return createNodeNamespace(hChild,
new character(uri),
new character(name),
new character(type));
}
/**
* Creating the new node within the current Document object using different
* approach based on the name space URI.
*
* @param hChild
* The valid handle class instance of the child to be created and
* returned on method call to use further.
* @param uri
* The URI namespace of the node to be created.
* @param name
* The name of the node to be created.
* @param type
* The type of the node to be created.
*
* @return The <code>true</code> in case of success
* <code>false</code> otherwise.
*/
@Override
public logical createNodeNamespace(handle hChild, character uri, String name, String type)
{
return createNodeNamespace(hChild, uri, new character(name), new character(type));
}
/**
* Creating the new node within the current Document object using different
* approach based on the name space URI.
*
* @param hChild
* The valid handle class instance of the child to be created and
* returned on method call to use further.
* @param uri
* The URI namespace of the node to be created.
* @param name
* The name of the node to be created.
* @param type
* The type of the node to be created.
*
* @return The <code>true</code> in case of success
* <code>false</code> otherwise.
*/
@Override
public logical createNodeNamespace(handle hChild, String uri, character name, String type)
{
return createNodeNamespace(hChild, new character(uri), name, new character(type));
}
/**
* Creating the new node within the current Document object using different
* approach based on the name space URI.
*
* @param hChild
* The valid handle class instance of the child to be created and
* returned on method call to use further.
* @param uri
* The URI namespace of the node to be created.
* @param name
* The name of the node to be created.
* @param type
* The type of the node to be created.
*
* @return The <code>true</code> in case of success
* <code>false</code> otherwise.
*/
@Override
public logical createNodeNamespace(handle hChild, String uri,String name, character type)
{
return createNodeNamespace(hChild, new character(uri), new character(name), type);
}
/**
* Creating the new node within the current Document object using different
* approach based on the name space URI.
*
* @param hChild
* The valid handle class instance of the child to be created and
* returned on method call to use further.
* @param uri
* The URI namespace of the node to be created.
* @param name
* The name of the node to be created.
* @param type
* The type of the node to be created.
*
* @return The <code>true</code> in case of success
* <code>false</code> otherwise.
*/
@Override
public logical createNodeNamespace(handle hChild,
String uri,
character name,
character type)
{
return createNodeNamespace(hChild, new character(uri), name, type);
}
/**
* Creating the new node within the current Document object using different
* approach based on the name space URI.
*
* @param hChild
* The valid handle class instance of the child to be created and
* returned on method call to use further.
* @param uri
* The URI namespace of the node to be created.
* @param name
* The name of the node to be created.
* @param type
* The type of the node to be created.
*
* @return The <code>true</code> in case of success
* <code>false</code> otherwise.
*/
@Override
public logical createNodeNamespace(handle hChild,
character uri,
String name,
character type)
{
return createNodeNamespace(hChild, uri, new character(name), type);
}
/**
* Creating the new node within the current Document object using different
* approach based on the name space URI.
*
* @param hChild
* The valid handle class instance of the child to be created and
* returned on method call to use further.
* @param uri
* The URI namespace of the node to be created.
* @param name
* The name of the node to be created.
* @param type
* The type of the node to be created.
*
* @return The <code>true</code> in case of success
* <code>false</code> otherwise.
*/
@Override
public logical createNodeNamespace(handle hChild,
character uri,
character name,
String type)
{
return createNodeNamespace(hChild, uri, name, new character(type));
}
/**
* Creating the new node within the current Document object using different
* approach based on the name space URI.
*
* @param hChild
* The valid handle class instance of the child to be created and
* returned on method call to use further.
* @param uri
* The URI namespace of the node to be created.
* @param name
* The name of the node to be created.
* @param type
* The type of the node to be created.
*
* @return The <code>true</code> in case of success
* <code>false</code> otherwise.
*/
@Override
public logical createNodeNamespace(handle hChild,
character uri,
character name,
character type)
{
if (!isInitialized())
{
invalidNodeError("CREATE-NODE-NAMESPACE");
return new logical(false);
}
if (!type.isUnknown() && !type.toStringMessage().isEmpty())
{
Integer nodeType = XmlFactory.convertAblNodeToNodeId(type.toStringMessage());
if (nodeType == null ||
!(nodeType == XmlTokenTypes.ELEMENT_NODE ||
nodeType == XmlTokenTypes.ATTRIBUTE_NODE))
{
unsupportedDomError(type.toStringMessage());
invalidArgumentError("CREATE-NODE-NAMESPACE");
return new logical(false);
}
}
if (!isXNodeHandle(hChild))
{
invalidArgumentError("CREATE-NODE-NAMESPACE");
return new logical(false);
}
if (!uri.isUnknown() && uri.toStringMessage().isEmpty())
{
commonDomError("CREATE-NODE-NAMESPACE", "XML Parser not initialized");
return new logical(false);
}
try
{
XmlFactory.createXNodeReference(hChild, new handle(this), uri, name, type);
}
catch (DOMException e)
{
if (e.code == DOMException.INVALID_CHARACTER_ERR)
{
commonDomError("CREATE-NODE-NAMESPACE", "An invalid character was specified");
}
else if (e.code == DOMException.NAMESPACE_ERR)
{
commonDomError("CREATE-NODE-NAMESPACE", "XML Parser not initialized");
}
return new logical(false);
}
return new logical(true);
}
/**
* Getting the root node for the current document. The handle is placed
* into passed object reference.
*
* @param hRoot
* The valid handle class instance variable to receive the
* result.
*
* @return The <code>true</code> in case of success
* <code>false</code> otherwise.
*/
@Override
public logical getDocumentElement(handle hRoot)
{
if (!isInitialized() || !valid() || !isXNodeHandle(hRoot))
{
invalidArgumentError("GET-DOCUMENT-ELEMENT");
return new logical(false);
}
Node documentElement = xdoc.getDocumentElement();
setNodeToHandle(hRoot, documentElement);
return new logical(true);
}
/**
* Loading X-Document form file or memptr sources.
*
* @param mode
* Variable indicating the type of source to read. "file" means
* reading from file, "memptr" means reading from memory buffer.
* @param source
* If mode == "file" this is the file name, if "memptr" this is
* the memptr object.
* @param validate
* Flag indicating whether to validate while reading or not.
*
* @return The <code>true</code> in case of success
* <code>false</code> otherwise.
*/
@Override
public logical load(String mode, Object source, logical validate)
{
return load(new character(mode), source, validate);
}
/**
* Loading X-Document form file or memptr sources.
*
* @param mode
* Variable indicating the type of source to read. "file" means
* reading from file, "memptr" means reading from memory buffer.
* @param source
* If mode == "file" this is the file name, if "memptr" this is
* the memptr object.
* @param validate
* Flag indicating whether to validate while reading or not.
*
* @return The <code>true</code> in case of success
* <code>false</code> otherwise.
*/
@Override
public logical load(String mode, Object source, boolean validate)
{
return load(new character(mode), source, logical.of(validate));
}
/**
* Loading X-Document form file or memptr sources.
*
* @param mode
* Variable indicating the type of source to read. "file" means reading from file and
* source contains file name, "memptr" means reading from memory buffer.
* @param source
* If mode == "file" this is the file name, if "memptr" this is the memptr object.
* @param validate
* Flag indicating whether to validate while reading or not.
*
* @return The {@code true} in case of success {@code false} otherwise. If {@code validate}
* is {@code true} and xml failed validation return {@code false} but the xml document
* is loaded anyway.
*/
@Override
public logical load(character mode, Object source, boolean validate)
{
return load(mode, source, logical.of(validate));
}
/**
* Loading X-Document form file or memptr sources.
*
* @param mode
* Variable indicating the type of source to read. "file" means reading from file and
* source contains file name, "memptr" means reading from memory buffer.
* @param source
* If mode == "file" this is the file name, if "memptr" this is the memptr object.
* @param validate
* Flag indicating whether to validate while reading or not. If {@code unknown},
* validation will not be done.
*
* @return The {@code true} in case of success {@code false} otherwise. If {@code validate}
* is {@code true} and xml failed validation return {@code false} but the xml document
* is loaded anyway.
*/
@Override
public logical load(character mode, Object source, logical validate)
{
// TODO: refactor the code below here with a shared implementation of the error handling
if (source == null ||
((source instanceof BaseDataType) && ((BaseDataType) source).isUnknown()))
{
invalidArgumentError("LOAD");
return logical.FALSE;
}
String loadMode = mode.isUnknown()
? "file"
: StringHelper.safeTrimTrailing(mode.toStringMessage()).toLowerCase();
if ("file".equals(loadMode))
{
String fName = null;
// we can't assume that source is a string
if (source instanceof String)
{
fName = (String) source;
}
else if (source instanceof com.goldencode.p2j.util.Text)
{
com.goldencode.p2j.util.Text txt = (com.goldencode.p2j.util.Text) source;
fName = txt.toStringMessage();
}
else
{
invalidArgumentError("LOAD");
return logical.FALSE;
}
Stream inStr;
try
{
inStr = StreamFactory.openFileStream(fName, false, false, -1, false);
}
catch (ErrorConditionException e)
{
String err = String.format(
"FATAL ERROR: file '', line '0', column '0', message 'An exception occurred! " +
"Type:RuntimeException, Message:The primary document entity could not be " +
"opened. Id=%s'",
fName);
commonDomError("LOAD", err);
return logical.FALSE;
}
try
{
logical retVal = new logical();
InputStream inputStream = new InputStreamWrapper(inStr);
String encoding = XmlHelper.readXMLEncoding(inputStream);
if (encoding == null)
{
// always default to UTF-8
encoding = "UTF-8";
}
Reader filtered = new EscapeControlCharsReader(new InputStreamReader(inputStream, encoding));
InputSource is = new InputSource(new EscapeControlCharsReader(filtered));
xdoc = XmlHelper.parse(is,
validate.booleanValue(), // unknown & false mean no validation
!isSuppressNamespaceProcessing,
retVal, // OUTPUT param mechanism
buildSchema());
inStr.close();
return retVal;
}
catch (IOException e)
{
// should not enter here, the exception is thrown by XmlHelper.parse() but the file
// is already "open" by StreamFactory.openFileStream()
String err = String.format("\"%s\" was not found", fName);
ErrorManager.recordOrShowError(293, err, false, true, false);
invalidArgumentError("LOAD");
}
catch (ParserConfigurationException e)
{
// impossible error
invalidArgumentError("LOAD");
}
catch (SAXParseException e)
{
String msg = "FATAL ERROR: file '%s', line '%s', column '%s', message '%s'";
String err = String.format(msg, source, e.getLineNumber(),
e.getColumnNumber(), e.getMessage());
commonDomError("LOAD", err);
}
catch (SAXException e)
{
String msg = "FATAL ERROR: file '%s', message '%s'";
String err = String.format(msg, source, e.getMessage());
commonDomError("LOAD", err);
}
}
else if ("memptr".equals(loadMode))
{
if (!(source instanceof memptr))
{
invalidArgumentError("LOAD");
return new logical(false);
}
try
{
memptr mptr = (memptr) source;
byte[] bytes = mptr.getByteArray();
ByteArrayInputStream bais = null;
// prepare the input stream: keep all bytes until '\0' is encountered
int last = 0;
for (byte b : bytes)
{
if (b == 0)
{
break;
}
last = last + 1;
}
if (last != bytes.length)
{
// we have at least 1 non-zero byte
byte[] cleaned = new byte[last];
System.arraycopy(bytes, 0, cleaned, 0, last);
bais = new ByteArrayInputStream(cleaned);
}
else
{
bais = new ByteArrayInputStream(bytes);
}
xdoc = XmlHelper.parse(bais,
validate.getValue(),
null,
!isSuppressNamespaceProcessing);
return new logical(true);
}
catch (IOException | ParserConfigurationException e)
{
invalidArgumentError("LOAD");
}
catch (SAXException e)
{
String msg = "X-NODEREF or X-DOCUMENT LOAD got an error: FATAL ERROR: " +
"file '%s', message '%s'";
String err = String.format(msg, source, e.getMessage());
commonDomError("LOAD", err);
}
}
else if ("longchar".equals(loadMode))
{
if (!(source instanceof longchar))
{
invalidArgumentError("LOAD");
return new logical(false);
}
try
{
longchar lc = (longchar) source;
String content = lc.toStringMessage();
// TODO: I18N implications need to be handled, this is probably WRONG!
ByteArrayInputStream bais = new ByteArrayInputStream(content.getBytes());
xdoc = XmlHelper.parse(bais,
validate.getValue(),
null,
!isSuppressNamespaceProcessing);
return new logical(true);
}
catch (IOException | ParserConfigurationException e)
{
invalidArgumentError("LOAD");
}
catch (SAXException e)
{
commonDomError("LOAD", "X-NODEREF or X-DOCUMENT LOAD got an error: '" + e.getMessage() + "'");
}
}
else
{
invalidReadWriteModeError("LOAD");
}
return new logical(false);
}
/**
* Saving X-Document to file or stream or memptr object.
*
* @param mode
* Variable indicating the type of target to write. "file" means writing to file,
* "stream" means writing to the stream and "memptr" means writing to memory buffer.
* @param target
* If mode == "file" this is the file name, if "stream" means output stream, if
* "memptr" this is the memptr object.
*
* @return The {@code true} in case of success {@code false} otherwise.
*/
@Override
public logical save(String mode, Object target)
{
return save(new character(mode), target);
}
/**
* Saving X-Document to file or stream or memptr object.
*
* @param mode
* Variable indicating the type of target to write. "file" means writing to file,
* "stream" means writing to the stream and "memptr" means writing to memory buffer.
* @param target
* If mode == "file" this is the file name, if "stream" means output stream, if
* "memptr" this is the memptr object.
*
* @return The {@code true} in case of success {@code false} otherwise.
*/
@Override
public logical save(character mode, Object target)
{
if (target == null ||
((target instanceof BaseDataType) && ((BaseDataType) target).isUnknown()))
{
invalidArgumentError("SAVE");
return new logical(false);
}
String writeMode = mode.isUnknown()
? "file"
: StringHelper.safeTrimTrailing(mode.toStringMessage()).toLowerCase();
if ("file".equals(writeMode))
{
String fName = null;
try
{
// we can't assume that source is a string
if (target instanceof String)
{
fName = (String) target;
}
else if (target instanceof com.goldencode.p2j.util.Text)
{
com.goldencode.p2j.util.Text txt = (com.goldencode.p2j.util.Text) target;
fName = txt.toStringMessage();
}
else
{
invalidArgumentError("SAVE");
return new logical(false);
}
Stream outStr = StreamFactory.openFileStream(fName, true, false, -1, false);
XmlHelper.writeLegacyCompatible(xdoc, new OutputStreamWrapper(outStr));
outStr.close(); // flush and close remote
return new logical(true);
}
catch (IOException e)
{
commonDomError("SAVE", "XML Parser Exception: Invalid file or path name");
}
}
else if ("stream".equals(writeMode))
{
if (!(target instanceof OutputStream))
{
invalidArgumentError("LOAD");
return new logical(false);
}
try
{
XmlHelper.writeLegacyCompatible(xdoc, (OutputStream) target);
return new logical(true);
}
catch (IOException e)
{
commonDomError("SAVE", "XML Parser Exception: Invalid stream");
}
}
else if ("memptr".equals(writeMode))
{
if (!(target instanceof memptr))
{
invalidArgumentError("LOAD");
return new logical(false);
}
try
{
memptr memptrTarget = (memptr)target;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
XmlHelper.writeLegacyCompatible(xdoc, baos);
memptrTarget.assign(baos.toByteArray());
return new logical(true);
}
catch (IOException e)
{
commonDomError("SAVE", "XML Parser Exception: Invalid memptr");
}
}
else if ("longchar".equals(writeMode))
{
if (!(target instanceof longchar))
{
invalidArgumentError("LOAD");
return new logical(false);
}
try
{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
XmlHelper.writeLegacyCompatible(xdoc, baos);
longchar lc = (longchar) target;
lc.assign(baos.toString());
return new logical(true);
}
catch (IOException e)
{
commonDomError("SAVE", "XML Parser Exception: Invalid longchar");
}
}
else
{
invalidReadWriteModeError("SAVE");
}
return new logical(false);
}
/**
* Creates a new XML document, initializes the document based on the referenced DTD,
* and creates its root node.
*
* @param namespaceUri
* character expression representing the namespace Uniform Resource Identifier
* (URI) you want associated with the root node of the XML document. The
* namespace-uri must be unique and persistent.
* @param rootNodeName
* A character expression representing the name of the root node as defined in the
* XML document. If you are using namespaces and you want to associate a prefix
* with the namespace, you must qualify this node name with the namespace-uri
* and a colon character prefix (for example, namespace-uri:root-node-name).
* You must explicitly set the xmlns attribute on the root node.
* @param publicId
* An optional character expression representing the public ID of the DTD.
* @param systemId
* A required character expression representing the system ID of the DTD. This
* contains the path to the DTD which is either a file system path or an HTTP URL.
*
* @return The <code>true</code> in case of success
* <code>false</code> otherwise.
*/
@Override
public logical initializeDocumentType(character namespaceUri,
character rootNodeName,
character publicId,
character systemId)
{
if (rootNodeName.isUnknown())
{
invalidArgumentError("INITIALIZE-DOCUMENT-TYPE");
return new logical(false);
}
try
{
xdoc = XmlHelper.newDocument(rootNodeName.getValue(),
publicId.getValue(),
systemId.getValue(),
namespaceUri.getValue());
}
catch (ParserConfigurationException e)
{
//impossible error
return new logical(false);
}
catch (DOMException e)
{
if (e.code == DOMException.INVALID_CHARACTER_ERR)
{
commonDomError("INITIALIZE-DOCUMENT-TYPE", "XML Parser not initialized");
return new logical(false);
}
else if (e.code == DOMException.NAMESPACE_ERR)
{
commonDomError("INITIALIZE-DOCUMENT-TYPE", "XML Parser not initialized");
return new logical(false);
}
else if (e.code == DOMException.WRONG_DOCUMENT_ERR)
{
//impossible error, cues document has been created inside XmlHelper.newDocument
commonDomError("INITIALIZE-DOCUMENT-TYPE", "XML Parser not initialized");
return new logical(false);
}
}
return new logical(true);
}
/**
* Creates a new XML document, initializes the document based on the referenced DTD,
* and creates its root node.
*
* @param namespaceUri
* character expression representing the namespace Uniform Resource Identifier
* (URI) you want associated with the root node of the XML document. The
* namespace-uri must be unique and persistent.
* @param rootNodeName
* A character expression representing the name of the root node as defined in the
* XML document. If you are using namespaces and you want to associate a prefix
* with the namespace, you must qualify this node name with the namespace-uri
* and a colon character prefix (for example, namespace-uri:root-node-name).
* You must explicitly set the xmlns attribute on the root node.
* @param publicId
* An optional character expression representing the public ID of the DTD.
* @param systemId
* A required character expression representing the system ID of the DTD. This
* contains the path to the DTD which is either a file system path or an HTTP URL.
*
* @return The <code>true</code> in case of success
* <code>false</code> otherwise.
*/
@Override
public logical initializeDocumentType(character namespaceUri,
String rootNodeName,
character publicId,
character systemId)
{
return initializeDocumentType(namespaceUri, new character(rootNodeName),
publicId, systemId);
}
/**
* Creates a new XML document, initializes the document based on the referenced DTD,
* and creates its root node.
*
* @param namespaceUri
* character expression representing the namespace Uniform Resource Identifier
* (URI) you want associated with the root node of the XML document. The
* namespace-uri must be unique and persistent.
* @param rootNodeName
* A character expression representing the name of the root node as defined in the
* XML document. If you are using namespaces and you want to associate a prefix
* with the namespace, you must qualify this node name with the namespace-uri
* and a colon character prefix (for example, namespace-uri:root-node-name).
* You must explicitly set the xmlns attribute on the root node.
* @param publicId
* An optional character expression representing the public ID of the DTD.
* @param systemId
* A required character expression representing the system ID of the DTD. This
* contains the path to the DTD which is either a file system path or an HTTP URL.
*
* @return The <code>true</code> in case of success
* <code>false</code> otherwise.
*/
@Override
public logical initializeDocumentType(String namespaceUri,
character rootNodeName,
String publicId,
String systemId)
{
return initializeDocumentType(new character(namespaceUri), new character(rootNodeName),
new character(publicId), new character(systemId));
}
/**
* Creates a new XML document, initializes the document based on the referenced DTD,
* and creates its root node.
*
* @param namespaceUri
* character expression representing the namespace Uniform Resource Identifier
* (URI) you want associated with the root node of the XML document. The
* namespace-uri must be unique and persistent.
* @param rootNodeName
* A character expression representing the name of the root node as defined in the
* XML document. If you are using namespaces and you want to associate a prefix
* with the namespace, you must qualify this node name with the namespace-uri
* and a colon character prefix (for example, namespace-uri:root-node-name).
* You must explicitly set the xmlns attribute on the root node.
* @param publicId
* An optional character expression representing the public ID of the DTD.
* @param systemId
* A required character expression representing the system ID of the DTD. This
* contains the path to the DTD which is either a file system path or an HTTP URL.
*
* @return The <code>true</code> in case of success
* <code>false</code> otherwise.
*/
@Override
public logical initializeDocumentType(String namespaceUri,
String rootNodeName,
String publicId,
String systemId)
{
return initializeDocumentType(new character(namespaceUri), new character(rootNodeName),
new character(publicId), new character(systemId));
}
/**
* This method returns the public ID of the external DTD from which an XML document was
* generated.
*
* @return public-id of DTD
*/
@Override
public character getPublicId()
{
if (!isInitialized())
{
invalidNodeError("PUBLIC-ID");
return new character();
}
DocumentType documentType = xdoc.getDoctype();
if (documentType != null)
{
String publicId = documentType.getPublicId();
return new character(publicId == null ? "" : publicId);
}
return new character("");
}
/**
* Returns the system ID of the external DTD from which an XML document was generated. This
* contains the path to the DTD which is either a file system path or an HTTP URL. The ABL
* parser uses this information to retrieve the DTD when parsing the document.
*
* @return system-id of DTD
*/
@Override
public character getSystemId()
{
if (!isInitialized())
{
invalidNodeError("SYSTEM-ID");
return new character();
}
DocumentType documentType = xdoc.getDoctype();
if (documentType != null)
{
String systemId = documentType.getSystemId();
return new character(systemId == null ? "" : systemId);
}
return new character("");
}
/**
* Determines the list of namespace/location pairs of an XML Schema file to validate
* against.
*
* @return schema location, default value is "".
*/
@Override
public character getSchemaLocation()
{
if (!isInitialized())
{
invalidNodeError("SCHEMA-LOCATION");
return new character();
}
String schemaLocation =
(String)xdoc.getDomConfig().getParameter(Constants.DOM_SCHEMA_LOCATION);
return new character(schemaLocation);
}
/**
* Creating the new node within the current Document object.
*
* @param xNodeRef
* The valid handle class instance to use for the new XML node.
* @param xSourceNodeRef
* The valid handle class instance to import from.
* @param deep
* A logical that if <code>true</code> specifies that the whole sub-tree is to be
* copied. The default value is {@code false}.
*
* @return The <code>true</code> in case of success <code>false</code> otherwise.
*/
@Override
public logical importNode(handle xNodeRef, handle xSourceNodeRef, logical deep)
{
if (!isInitialized())
{
invalidArgumentError("IMPORT-NODE");
return new logical(false);
}
if (!isXNodeHandle(xNodeRef) ||
!isXNodeHandle(xSourceNodeRef) ||
!((XEntityImpl) xSourceNodeRef.getResource()).isInitialized())
{
invalidArgumentError("IMPORT-NODE");
return new logical(false);
}
if (xNodeRef == xSourceNodeRef) // check that xNodeRef and xSourceNodeRef are different
{
xSourceNodeRef.assign(new XNodeRefImpl());//make handle unknown
invalidNodeError("IMPORT-NODE");
return new logical(false);
}
XNodeRefImpl nodeRef = (XNodeRefImpl) xSourceNodeRef.get();
try
{
Node node = xdoc.importNode(nodeRef.getNode(), deep.booleanValue());
setNodeToHandle(xNodeRef, node);
return xNodeRef.isValid();
}
catch (DOMException e)
{
if (e.code == DOMException.INVALID_CHARACTER_ERR)
{
commonDomError("CREATE-NODE-NAMESPACE", "An invalid character was specified");
}
return new logical(false);
}
}
/**
* Creating the new node within the current Document object.
*
* @param xNodeRef
* The valid handle class instance to use for the new XML node.
* @param xSourceNodeRef
* The valid handle class instance to import from.
* @param deep
* A boolean that if <code>true</code> specifies that the whole sub-tree is to be
* copied. The default value is {@code false}.
*
* @return The <code>true</code> in case of success <code>false</code> otherwise.
*/
@Override
public logical importNode(handle xNodeRef, handle xSourceNodeRef, boolean deep)
{
return importNode(xNodeRef, xSourceNodeRef, new logical(deep));
}
/**
* Determines the list of namespace/location pairs of an XML Schema file to validate
* against.
*
* @param schemaLocation
* Determines the list of namespace/location pairs of an XML Schema file to
* validate against.
*/
@Override
public void setSchemaLocation(character schemaLocation)
{
if (!isInitialized())
{
invalidNodeError("SCHEMA-LOCATION");
return;
}
if (schemaLocation.isUnknown())
{
invalidArgumentAssignError("SCHEMA-LOCATION");
return;
}
String schemaLoc = schemaLocation.toStringMessage();
xdoc.getDomConfig().setParameter(Constants.DOM_SCHEMA_LOCATION, schemaLoc);
}
/**
* Get encoding
*
* @return encoding of xDoc
*/
@Override
public character getEncoding()
{
if (!isInitialized())
{
invalidNodeError("ENCODING");
return new character();
}
String xdocEncoding = xdoc.getXmlEncoding();
final character result;
if (xdocEncoding == null)
{
result = new character("");
}
else
{
result = new character(xdocEncoding);
}
return result;
}
/**
* A delimiter-separated list of directory paths used to locate the XML Document Type
* Definition (DTD) associated with a particular XML document. It is searched if the XML
* document contains a relative path to the DTD.
*
* @return schema path, default value is unknown(?).
*/
@Override
public character getSchemaPath()
{
if (!isInitialized())
{
return new character();
}
return new character(schemaPaths);
}
/**
* Sets the new value of the encoding attribute for the given X-Document object.
*
* @param encoding
* The encoding value to be set for the given writer.
*/
@Override
public void setEncoding(String encoding)
{
setEncoding(new character(encoding));
}
/**
* A delimiter-separated list of directory paths used to locate the XML Document Type
* Definition (DTD) associated with a particular XML document. It is searched if the XML
* document contains a relative path to the DTD.
*
* @param schemaPath
* To separate directory paths, use a comma, or a delimiter character that your
* operating system recognizes.
*/
@Override
public void setSchemaPath(character schemaPath)
{
if (!isInitialized())
{
invalidNodeError("SCHEMA-PATH");
return;
}
if (schemaPath.isUnknown())
{
invalidArgumentAssignError("SCHEMA-PATH");
return;
}
schemaPaths = schemaPath.toStringMessage();
}
/**
* Sets the new value of the encoding attribute for the given X-Document object.
*
* @param encoding
* The encoding value to be set for the given writer.
*/
@Override
public void setEncoding(character encoding)
{
if (!isInitialized() || encoding.isUnknown())
{
return;
}
String encodingStr = encoding.toStringMessage();
String javaEncoding = EncodingMap.getIANA2JavaMapping(encodingStr);
if (javaEncoding == null)
{
Charset charset = Charset.forName(encodingStr);
for (String alias : charset.aliases())
{
javaEncoding = EncodingMap.getIANA2JavaMapping(alias);
if (javaEncoding != null)
{
encodingStr = alias;
break;
}
}
}
((DocumentImpl) xdoc).setXmlEncoding(encodingStr);
}
/**
* Gets the value of the XML schema file for elements with no namespaces.
*
* @return The value of the nonamespace-schema-location attribute.
*/
@Override
public character getNonamespaceSchemaLocation()
{
if (!isInitialized())
{
return new character();
}
DOMConfiguration domConfig = xdoc.getDomConfig();
String schemaLocation;
try
{
schemaLocation = (String) domConfig.getParameter(Constants.SCHEMA_NONS_LOCATION);
}
catch (DOMException e)
{
// impossible error for in our case
return new character("");
}
return new character(schemaLocation);
}
/**
* Setting the new value for the nonamespace-schema-location attribute.
*
* @param location
* New value of the nonamespace-schema-location attribute to set.
*/
@Override
public void setNonamespaceSchemaLocation(character location)
{
if (!isInitialized()|| location.isUnknown())
{
return;
}
this.noNamespaceSchemaLocation = location.toStringMessage();
}
/**
* Adding new match pair for namespace schema and physical file location.
*
* @param namespace
* Target namespace of the schema or am empty string or Unknown value.
* @param location
* Location of the XML schema file.
*
* @return The <code>true</code> in case of success <code>false</code> otherwise.
*/
@Override
public logical addSchemaLocation(character namespace, character location)
{
if (!isInitialized())
{
invalidNodeError("SCHEMA-LOCATION");
return new logical(false);
}
if (namespace.isUnknown() || location.isUnknown())
{
invalidArgumentError("SCHEMA-LOCATION");
return new logical(false);
}
DOMConfiguration domConfig = xdoc.getDomConfig();
String namespaceStr = namespace.toStringMessage();
String locationEntity = namespaceStr + ' ' + location.toStringMessage();
String oldSchemaLocation =
(String)xdoc.getDomConfig().getParameter(Constants.DOM_SCHEMA_LOCATION);
String newSchemaLocation =
oldSchemaLocation == null ? " " : oldSchemaLocation;
domConfig.setParameter(Constants.DOM_SCHEMA_LOCATION, newSchemaLocation + locationEntity);
this.schemaLocations.put(namespace.toStringMessage(), location.toStringMessage());
return new logical(true);
}
/**
* Gets the number of child for the current one. Only for XNodRef
*
* @return The integer number of the child, 1-based index of the child node.
*/
@Override
public integer getChildNum()
{
if (!isInitialized())
{
invalidNodeError("NUM-CHILDREN");
return new integer();
}
return new integer();
}
/**
* Indicates if the namespace processing is suppressed or not.
*
* @return The logical value representing the current suppress-namespace-processing
* attribute.
*/
@Override
public logical getSuppressNamespaceProcessing()
{
if (!isInitialized())
{
return new logical(false);
}
return new logical(isSuppressNamespaceProcessing);
}
/**
* Redefines whether the namespace processing is suppressed or not.
*
* @param suppress
* The new value the current suppress-namespace-processing attribute.
*/
@Override
public void setSuppressNamespaceProcessing(logical suppress)
{
if (!isInitialized())
{
return;
}
if (suppress.isUnknown())
{
invalidArgumentAssignError("SUPPRESS-NAMESPACE-PROCESSING");
return;
}
isSuppressNamespaceProcessing = suppress.booleanValue();
}
/**
* Redefines whether the namespace processing is suppressed or not.
*
* @param suppress
* The new value the current suppress-namespace-processing attribute.
*/
@Override
public void setSuppressNamespaceProcessing(boolean suppress)
{
setSuppressNamespaceProcessing(new logical(suppress));
}
/**
* Indicates if the strict entity resolution is enabled.
*
* @return The logical value representing the current value.
*/
@Override
public logical getStrictEntityResolution()
{
// TODO
return new logical();
}
/**
* Enable or disable strict entity resolution.
*
* @param strict
* The new value for the attribute.
*/
@Override
public void setStrictEntityResolution(logical strict)
{
// TODO
}
/**
* Enable or disable strict entity resolution.
*
* @param strict
* The new value for the attribute.
*/
@Override
public void setStrictEntityResolution(boolean strict)
{
setStrictEntityResolution(new logical(strict));
}
/**
* Get the entity expansion limit.
*
* @return The integer value of the attribute.
*/
@Override
public integer getEntityExpansionLimit()
{
// TODO
return new integer();
}
/**
* Set the entity expansion limit.
*
* @param limit
* The new value for the attribute.
*/
@Override
public void setEntityExpansionLimit(int64 limit)
{
// TODO
}
/**
* Set the entity expansion limit.
*
* @param limit
* The new value for the attribute.
*/
@Override
public void setEntityExpansionLimit(long limit)
{
setEntityExpansionLimit(new int64(limit));
}
/**
* Getting the Document the current reference belongs to.
*
* @return The handle of the Document containing tne current reference.
*/
@Override
public handle getOwnerDocument()
{
invalidNodeError("OWNER-DOCUMENT");
return new handle();
}
/**
* Process and display invalid node error. It displays appropriate error message.
*
* @param attribute
* The read-only attribute.
*/
private void invalidReadWriteModeError(String attribute)
{
String msg = "Argument for LOAD/SAVE must be 'file', 'stream', " +
"'stream-handle' or 'memptr'";
ErrorManager.recordOrShowError(9170, msg, false, false, false);
invalidArgumentError(attribute);
}
/**
* Build the validation schema using the {@link #noNamespaceSchemaLocation}, {@link #schemaPaths} and
* {@link #addSchemaLocation} values.
*
* @return A schema used for validation, or <code>null</code> if one could ont be built.
*/
private Schema buildSchema()
{
List<Source> sources = new ArrayList<>();
String sep = EnvironmentOps.getLegacyPathSeparator();
String[] paths = schemaPaths == null ? null : schemaPaths.split(sep);
if (noNamespaceSchemaLocation != null && !noNamespaceSchemaLocation.trim().isEmpty())
{
byte[] schemaFile = resolveSchemaFile(paths, noNamespaceSchemaLocation);
if (schemaFile != null)
{
sources.add(new StreamSource(new ByteArrayInputStream(schemaFile)));
}
}
// TODO: use [ns, location] for other 'add-schema-location'; TODO: remote URLs?
for (String file : schemaLocations.values())
{
byte[] schemaFile = resolveSchemaFile(paths, file);
if (schemaFile != null)
{
sources.add(new StreamSource(new ByteArrayInputStream(schemaFile)));
}
}
if (sources.isEmpty())
{
return null;
}
try
{
Schema schema = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI)
.newSchema(sources.toArray(new Source[0]));
return schema;
}
catch (SAXException e)
{
return null;
}
}
/**
* Resolve the schema file from the FWD client's file-system.
*
* @param paths
* The paths to check against.
* @param file
* The file to locate.
*
* @return The content of the file or <code>null</code> if it can not be resolved.
*/
private byte[] resolveSchemaFile(String[] paths, String file)
{
if (paths == null)
{
try
{
return FileSystemOps.getFileContents(file);
}
catch (IOException exc)
{
return null;
}
}
String sep = EnvironmentOps.getLegacyFileSeparator();
for (int i = 0; i < paths.length; i++)
{
try
{
return FileSystemOps.getFileContents(paths[i] + sep + file);
}
catch (IOException exc)
{
// ignore
}
}
return null;
}
}