/*******************************************************************************
* Copyright (c) 2004, 2008 John Krasnay and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* John Krasnay - initial API and implementation
*******************************************************************************/
package net.sf.vex.dom;
import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import com.wutka.dtd.DTD;
import com.wutka.dtd.DTDAny;
import com.wutka.dtd.DTDAttribute;
import com.wutka.dtd.DTDCardinal;
import com.wutka.dtd.DTDChoice;
import com.wutka.dtd.DTDContainer;
import com.wutka.dtd.DTDDecl;
import com.wutka.dtd.DTDElement;
import com.wutka.dtd.DTDEmpty;
import com.wutka.dtd.DTDEnumeration;
import com.wutka.dtd.DTDItem;
import com.wutka.dtd.DTDMixed;
import com.wutka.dtd.DTDName;
import com.wutka.dtd.DTDNotationList;
import com.wutka.dtd.DTDPCData;
import com.wutka.dtd.DTDParser;
import com.wutka.dtd.DTDSequence;
/**
* A validator driven by a DTD.
*/
public class DTDValidator extends AbstractValidator {
// DFA representing an EMPTY element; just a single non-accepting state
// with no transitions.
private static final DFAState emptyDFA = new DFAState();
// map element names to DFAs
private Map elementDFAs = new HashMap();
// list of all element names plus PCDATA
private Set anySet;
// map element names to arrays of AttributeDefinition objects
private Map attributeArrays = new HashMap();
// map element names to maps of attribute name to attribute def
private Map attributeMaps = new HashMap();
/**
* Creates a instance of DtdValidator given a URL.
*
* @param url URL of the DTD file to use.
*/
public static DTDValidator create(URL url) throws IOException {
// Compute the DFAs for each element in the DTD
DTDParser parser = new DTDParser(url);
DTD dtd = parser.parse();
DTDValidator validator = new DTDValidator();
Iterator iter = dtd.elements.values().iterator();
while (iter.hasNext()) {
DTDElement element = (DTDElement) iter.next();
DFAState dfa;
if (element.getContent() instanceof DTDEmpty) {
dfa = emptyDFA;
} else if (element.getContent() instanceof DTDAny) {
dfa = null;
} else {
DFABuilder.Node node = createDFANode(element.getContent());
dfa = DFABuilder.createDFA(node);
}
validator.elementDFAs.put(element.getName(), dfa);
Map defMap = new HashMap();
AttributeDefinition[] defArray = new AttributeDefinition[element.attributes.size()];
int i = 0;
Iterator iter2 = element.attributes.values().iterator();
while (iter2.hasNext()) {
DTDAttribute attr = (DTDAttribute) iter2.next();
AttributeDefinition.Type type;
String[] values = null;
if (attr.getType() instanceof DTDEnumeration) {
type = AttributeDefinition.Type.ENUMERATION;
values = ((DTDEnumeration)attr.getType()).getItems();
} else if (attr.getType() instanceof DTDNotationList) {
type = AttributeDefinition.Type.ENUMERATION;
values = ((DTDNotationList)attr.getType()).getItems();
} else if (attr.getType() instanceof String) {
type = AttributeDefinition.Type.get((String) attr.getType());
} else {
throw new RuntimeException("Unrecognized attribute type for element "
+ element.getName() + " attribute " + attr.getName()
+ " type " + attr.getType().getClass().getName());
}
AttributeDefinition ad = new AttributeDefinition(attr.getName(),
type,
attr.getDefaultValue(),
values,
attr.getDecl() == DTDDecl.REQUIRED,
attr.getDecl() == DTDDecl.FIXED);
defMap.put(attr.getName(), ad);
defArray[i] = ad;
i++;
}
validator.attributeMaps.put(element.getName(), defMap);
Arrays.sort(defArray);
validator.attributeArrays.put(element.getName(), defArray);
}
// Calculate anySet
validator.anySet = new HashSet();
validator.anySet.addAll(validator.elementDFAs.keySet());
validator.anySet.add(Validator.PCDATA);
return validator;
}
public AttributeDefinition getAttributeDefinition(String element, String attribute) {
Map attrMap = (Map) this.attributeMaps.get(element);
return attrMap == null ? null : (AttributeDefinition) attrMap.get(attribute);
}
public AttributeDefinition[] getAttributeDefinitions(String element) {
if (this.attributeArrays.containsKey(element)) {
return (AttributeDefinition[]) this.attributeArrays.get(element);
} else {
return new AttributeDefinition[0];
}
}
public Set getValidRootElements() {
return this.elementDFAs.keySet();
}
/** @see Validator#getValidItems */
public Set getValidItems(String element, String[] prefix, String[] suffix) {
// First, get a set of candidates. We'll later test to see if each is
// valid to insert here.
Set candidates = null;
DFAState dfa = (DFAState) elementDFAs.get(element);
if (dfa == null) {
// Anything goes!
return this.anySet;
}
DFAState target = dfa.getState(Arrays.asList(prefix));
if (target == null) {
return Collections.EMPTY_SET;
} else {
// If the last transition was due to PCDATA, adding more PCDATA
// is also valid
if (prefix.length > 0
&& prefix[prefix.length - 1].equals(Validator.PCDATA)) {
candidates = new HashSet();
candidates.addAll(target.getValidSymbols());
candidates.add(Validator.PCDATA);
} else {
candidates = target.getValidSymbols();
}
}
// Now, see if each candidate can be inserted at the given
// offset. This second test is necessary in some simple
// cases. Consider a <section> with an optional <title>; if
// we're at the first offset of the <section> and a <title>
// already exists, we should not allow another <title>.
Set results = new HashSet();
String[] middle = new String[1];
for (Iterator iter = candidates.iterator(); iter.hasNext(); ) {
middle[0] = (String) iter.next();
if (this.isValidSequence(element, prefix, middle, suffix, true)) {
results.add(middle[0]);
}
}
return Collections.unmodifiableSet(results);
}
/**
* @see Validator#isValidSequence
*/
public boolean isValidSequence(
String element,
String[] nodes,
boolean partial) {
DFAState dfa = (DFAState) this.elementDFAs.get(element);
if (dfa == null) {
// Unrecognized element. Give the user the benefit of the doubt.
return true;
}
DFAState target = dfa.getState(Arrays.asList(nodes));
return target != null && (partial || target.isAccepting());
}
//==================================================== PRIVATE
/**
* Homeys must call create()
*/
private DTDValidator() {
}
/**
* Create a DFABuilder.Node corresponding to the given DTDItem.
*/
private static DFABuilder.Node createDFANode(DTDItem item) {
DFABuilder.Node node = null;
if (item instanceof DTDName) {
String name = ((DTDName) item).getValue();
node = DFABuilder.createSymbolNode(name);
} else if (item instanceof DTDPCData) {
node = DFABuilder.createSymbolNode(Validator.PCDATA);
} else if (item instanceof DTDChoice) {
Iterator iter = ((DTDContainer)item).getItemsVec().iterator();
while (iter.hasNext()) {
DTDItem child = (DTDItem) iter.next();
DFABuilder.Node newNode = createDFANode(child);
if (node == null) {
node = newNode;
} else {
node = DFABuilder.createChoiceNode(node, newNode);
}
}
} else if (item instanceof DTDMixed) {
Iterator iter = ((DTDContainer)item).getItemsVec().iterator();
while (iter.hasNext()) {
DTDItem child = (DTDItem) iter.next();
DFABuilder.Node newNode = createDFANode(child);
if (node == null) {
node = newNode;
} else {
node = DFABuilder.createChoiceNode(node, newNode);
}
}
DFABuilder.Node pcdata =
DFABuilder.createSymbolNode(Validator.PCDATA);
node = DFABuilder.createChoiceNode(node, pcdata);
} else if (item instanceof DTDSequence) {
Iterator iter = ((DTDContainer)item).getItemsVec().iterator();
while (iter.hasNext()) {
DTDItem child = (DTDItem) iter.next();
DFABuilder.Node newNode = createDFANode(child);
if (node == null) {
node = newNode;
} else {
node = DFABuilder.createSequenceNode(node, newNode);
}
}
} else {
throw new RuntimeException("Unexpected DTDItem subclass: " +
item.getClass().getName());
}
// Cardinality is moot if it's a null node
if (node == null) {
return node;
}
if (item.cardinal == DTDCardinal.OPTIONAL) {
node = DFABuilder.createOptionalNode(node);
} else if (item.cardinal == DTDCardinal.ZEROMANY) {
node = DFABuilder.createRepeatingNode(node, 0);
} else if (item.cardinal == DTDCardinal.ONEMANY) {
node = DFABuilder.createRepeatingNode(node, 1);
}
return node;
}
}