/*
* Copyright (C) 2010 The Android Open Source Project
*
* 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.apache.harmony.xml.dom;
import java.util.Map;
import java.util.TreeMap;
import org.w3c.dom.DOMConfiguration;
import org.w3c.dom.DOMError;
import org.w3c.dom.DOMErrorHandler;
import org.w3c.dom.DOMException;
import org.w3c.dom.DOMStringList;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
/**
* A minimal implementation of DOMConfiguration. This implementation uses inner
* parameter instances to centralize each parameter's behavior.
*/
public final class DOMConfigurationImpl implements DOMConfiguration {
private static final Map<String, Parameter> PARAMETERS
= new TreeMap<String, Parameter>(String.CASE_INSENSITIVE_ORDER);
static {
/*
* True to canonicalize the document (unsupported). This includes
* removing DocumentType nodes from the tree and removing unused
* namespace declarations. Setting this to true also sets these
* parameters:
* entities = false
* normalize-characters = false
* cdata-sections = false
* namespaces = true
* namespace-declarations = true
* well-formed = true
* element-content-whitespace = true
* Setting these parameters to another value shall revert the canonical
* form to false.
*/
PARAMETERS.put("canonical-form", new FixedParameter(false));
/*
* True to keep existing CDATA nodes; false to replace them/merge them
* into adjacent text nodes.
*/
PARAMETERS.put("cdata-sections", new BooleanParameter() {
public Object get(DOMConfigurationImpl config) {
return config.cdataSections;
}
public void set(DOMConfigurationImpl config, Object value) {
config.cdataSections = (Boolean) value;
}
});
/*
* True to check character normalization (unsupported).
*/
PARAMETERS.put("check-character-normalization", new FixedParameter(false));
/*
* True to keep comments in the document; false to discard them.
*/
PARAMETERS.put("comments", new BooleanParameter() {
public Object get(DOMConfigurationImpl config) {
return config.comments;
}
public void set(DOMConfigurationImpl config, Object value) {
config.comments = (Boolean) value;
}
});
/*
* True to expose schema normalized values. Setting this to true sets
* the validate parameter to true. Has no effect when validate is false.
*/
PARAMETERS.put("datatype-normalization", new BooleanParameter() {
public Object get(DOMConfigurationImpl config) {
return config.datatypeNormalization;
}
public void set(DOMConfigurationImpl config, Object value) {
if ((Boolean) value) {
config.datatypeNormalization = true;
config.validate = true;
} else {
config.datatypeNormalization = false;
}
}
});
/*
* True to keep whitespace elements in the document; false to discard
* them (unsupported).
*/
PARAMETERS.put("element-content-whitespace", new FixedParameter(true));
/*
* True to keep entity references in the document; false to expand them.
*/
PARAMETERS.put("entities", new BooleanParameter() {
public Object get(DOMConfigurationImpl config) {
return config.entities;
}
public void set(DOMConfigurationImpl config, Object value) {
config.entities = (Boolean) value;
}
});
/*
* Handler to be invoked when errors are encountered.
*/
PARAMETERS.put("error-handler", new Parameter() {
public Object get(DOMConfigurationImpl config) {
return config.errorHandler;
}
public void set(DOMConfigurationImpl config, Object value) {
config.errorHandler = (DOMErrorHandler) value;
}
public boolean canSet(DOMConfigurationImpl config, Object value) {
return value == null || value instanceof DOMErrorHandler;
}
});
/*
* Bulk alias to set the following parameter values:
* validate-if-schema = false
* entities = false
* datatype-normalization = false
* cdata-sections = false
* namespace-declarations = true
* well-formed = true
* element-content-whitespace = true
* comments = true
* namespaces = true.
* Querying this returns true if all of the above parameters have the
* listed values; false otherwise.
*/
PARAMETERS.put("infoset", new BooleanParameter() {
public Object get(DOMConfigurationImpl config) {
// validate-if-schema is always false
// element-content-whitespace is always true
// namespace-declarations is always true
return !config.entities
&& !config.datatypeNormalization
&& !config.cdataSections
&& config.wellFormed
&& config.comments
&& config.namespaces;
}
public void set(DOMConfigurationImpl config, Object value) {
if ((Boolean) value) {
// validate-if-schema is always false
// element-content-whitespace is always true
// namespace-declarations is always true
config.entities = false;
config.datatypeNormalization = false;
config.cdataSections = false;
config.wellFormed = true;
config.comments = true;
config.namespaces = true;
}
}
});
/*
* True to perform namespace processing; false for none.
*/
PARAMETERS.put("namespaces", new BooleanParameter() {
public Object get(DOMConfigurationImpl config) {
return config.namespaces;
}
public void set(DOMConfigurationImpl config, Object value) {
config.namespaces = (Boolean) value;
}
});
/**
* True to include namespace declarations; false to discard them
* (unsupported). Even when namespace declarations are discarded,
* prefixes are retained.
*
* Has no effect if namespaces is false.
*/
PARAMETERS.put("namespace-declarations", new FixedParameter(true));
/*
* True to fully normalize characters (unsupported).
*/
PARAMETERS.put("normalize-characters", new FixedParameter(false));
/*
* A list of whitespace-separated URIs representing the schemas to validate
* against. Has no effect if schema-type is null.
*/
PARAMETERS.put("schema-location", new Parameter() {
public Object get(DOMConfigurationImpl config) {
return config.schemaLocation;
}
public void set(DOMConfigurationImpl config, Object value) {
config.schemaLocation = (String) value;
}
public boolean canSet(DOMConfigurationImpl config, Object value) {
return value == null || value instanceof String;
}
});
/*
* URI representing the type of schema language, such as
* "http://www.w3.org/2001/XMLSchema" or "http://www.w3.org/TR/REC-xml".
*/
PARAMETERS.put("schema-type", new Parameter() {
public Object get(DOMConfigurationImpl config) {
return config.schemaType;
}
public void set(DOMConfigurationImpl config, Object value) {
config.schemaType = (String) value;
}
public boolean canSet(DOMConfigurationImpl config, Object value) {
return value == null || value instanceof String;
}
});
/*
* True to split CDATA sections containing "]]>"; false to signal an
* error instead.
*/
PARAMETERS.put("split-cdata-sections", new BooleanParameter() {
public Object get(DOMConfigurationImpl config) {
return config.splitCdataSections;
}
public void set(DOMConfigurationImpl config, Object value) {
config.splitCdataSections = (Boolean) value;
}
});
/*
* True to require validation against a schema or DTD. Validation will
* recompute element content whitespace, ID and schema type data.
*
* Setting this unsets validate-if-schema.
*/
PARAMETERS.put("validate", new BooleanParameter() {
public Object get(DOMConfigurationImpl config) {
return config.validate;
}
public void set(DOMConfigurationImpl config, Object value) {
// validate-if-schema is always false
config.validate = (Boolean) value;
}
});
/*
* True to validate if a schema was declared (unsupported). Setting this
* unsets validate.
*/
PARAMETERS.put("validate-if-schema", new FixedParameter(false));
/*
* True to report invalid characters in node names, attributes, elements,
* comments, text, CDATA sections and processing instructions.
*/
PARAMETERS.put("well-formed", new BooleanParameter() {
public Object get(DOMConfigurationImpl config) {
return config.wellFormed;
}
public void set(DOMConfigurationImpl config, Object value) {
config.wellFormed = (Boolean) value;
}
});
// TODO add "resource-resolver" property for use with LS feature...
}
private boolean cdataSections = true;
private boolean comments = true;
private boolean datatypeNormalization = false;
private boolean entities = true;
private DOMErrorHandler errorHandler;
private boolean namespaces = true;
private String schemaLocation;
private String schemaType;
private boolean splitCdataSections = true;
private boolean validate = false;
private boolean wellFormed = true;
interface Parameter {
Object get(DOMConfigurationImpl config);
void set(DOMConfigurationImpl config, Object value);
boolean canSet(DOMConfigurationImpl config, Object value);
}
static class FixedParameter implements Parameter {
final Object onlyValue;
FixedParameter(Object onlyValue) {
this.onlyValue = onlyValue;
}
public Object get(DOMConfigurationImpl config) {
return onlyValue;
}
public void set(DOMConfigurationImpl config, Object value) {
if (!onlyValue.equals(value)) {
throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
"Unsupported value: " + value);
}
}
public boolean canSet(DOMConfigurationImpl config, Object value) {
return onlyValue.equals(value);
}
}
static abstract class BooleanParameter implements Parameter {
public boolean canSet(DOMConfigurationImpl config, Object value) {
return value instanceof Boolean;
}
}
public boolean canSetParameter(String name, Object value) {
Parameter parameter = PARAMETERS.get(name);
return parameter != null && parameter.canSet(this, value);
}
public void setParameter(String name, Object value) throws DOMException {
Parameter parameter = PARAMETERS.get(name);
if (parameter == null) {
throw new DOMException(DOMException.NOT_FOUND_ERR, "No such parameter: " + name);
}
try {
parameter.set(this, value);
} catch (NullPointerException e) {
throw new DOMException(DOMException.TYPE_MISMATCH_ERR,
"Null not allowed for " + name);
} catch (ClassCastException e) {
throw new DOMException(DOMException.TYPE_MISMATCH_ERR,
"Invalid type for " + name + ": " + value.getClass());
}
}
public Object getParameter(String name) throws DOMException {
Parameter parameter = PARAMETERS.get(name);
if (parameter == null) {
throw new DOMException(DOMException.NOT_FOUND_ERR, "No such parameter: " + name);
}
return parameter.get(this);
}
public DOMStringList getParameterNames() {
final String[] result = PARAMETERS.keySet().toArray(new String[PARAMETERS.size()]);
return new DOMStringList() {
public String item(int index) {
return index < result.length ? result[index] : null;
}
public int getLength() {
return result.length;
}
public boolean contains(String str) {
return PARAMETERS.containsKey(str); // case-insensitive.
}
};
}
public void normalize(Node node) {
/*
* Since we don't validate, this code doesn't take into account the
* following "supported" parameters: datatype-normalization, entities,
* schema-location, schema-type, or validate.
*
* TODO: normalize namespaces
*/
switch (node.getNodeType()) {
case Node.CDATA_SECTION_NODE:
CDATASectionImpl cdata = (CDATASectionImpl) node;
if (cdataSections) {
if (cdata.needsSplitting()) {
if (splitCdataSections) {
cdata.split();
report(DOMError.SEVERITY_WARNING, "cdata-sections-splitted");
} else {
report(DOMError.SEVERITY_ERROR, "wf-invalid-character");
}
}
checkTextValidity(cdata.buffer);
break;
}
node = cdata.replaceWithText();
// fall through
case Node.TEXT_NODE:
TextImpl text = (TextImpl) node;
text = text.minimize();
if (text != null) {
checkTextValidity(text.buffer);
}
break;
case Node.COMMENT_NODE:
CommentImpl comment = (CommentImpl) node;
if (!comments) {
comment.getParentNode().removeChild(comment);
break;
}
if (comment.containsDashDash()) {
report(DOMError.SEVERITY_ERROR, "wf-invalid-character");
}
checkTextValidity(comment.buffer);
break;
case Node.PROCESSING_INSTRUCTION_NODE:
checkTextValidity(((ProcessingInstructionImpl) node).getData());
break;
case Node.ATTRIBUTE_NODE:
checkTextValidity(((AttrImpl) node).getValue());
break;
case Node.ELEMENT_NODE:
ElementImpl element = (ElementImpl) node;
NamedNodeMap attributes = element.getAttributes();
for (int i = 0; i < attributes.getLength(); i++) {
normalize(attributes.item(i));
}
// fall through
case Node.DOCUMENT_NODE:
case Node.DOCUMENT_FRAGMENT_NODE:
Node next;
for (Node child = node.getFirstChild(); child != null; child = next) {
// lookup next eagerly because normalize() may remove its subject
next = child.getNextSibling();
normalize(child);
}
break;
case Node.NOTATION_NODE:
case Node.DOCUMENT_TYPE_NODE:
case Node.ENTITY_NODE:
case Node.ENTITY_REFERENCE_NODE:
break;
default:
throw new DOMException(DOMException.NOT_SUPPORTED_ERR,
"Unsupported node type " + node.getNodeType());
}
}
private void checkTextValidity(CharSequence s) {
if (wellFormed && !isValid(s)) {
report(DOMError.SEVERITY_ERROR, "wf-invalid-character");
}
}
/**
* Returns true if all of the characters in the text are permitted for use
* in XML documents.
*/
private boolean isValid(CharSequence text) {
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
// as defined by http://www.w3.org/TR/REC-xml/#charsets.
boolean valid = c == 0x9 || c == 0xA || c == 0xD
|| (c >= 0x20 && c <= 0xd7ff)
|| (c >= 0xe000 && c <= 0xfffd);
if (!valid) {
return false;
}
}
return true;
}
private void report(short severity, String type) {
if (errorHandler != null) {
// TODO: abort if handleError returns false
errorHandler.handleError(new DOMErrorImpl(severity, type));
}
}
}