blob: 56caa164bbbb5b0c0919b7fe542b4e0233a4748a [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2002, 2013 Object Factory Inc.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Object Factory Inc. - Initial implementation
* IBM Corporation - Fix for bug 40951
*******************************************************************************/
package org.eclipse.ant.internal.ui.dtd.schema;
import java.util.HashSet;
import java.util.LinkedList;
import org.eclipse.ant.internal.ui.dtd.IModel;
import org.eclipse.ant.internal.ui.dtd.ISchema;
import org.xml.sax.SAXException;
import org.xml.sax.ext.DeclHandler;
import com.ibm.icu.text.MessageFormat;
/**
* SchemaFactory is a SAX DeclHandler that converts DTD ELEMENT and ATTLIST declarations to schema form on the fly. The only two methods available to
* external users of SchemaFactory are its constructor and <code>getSchema()</code>. The latter returns the schema built by this process and should
* not be called until the XML parser to which this handler is attached has finished parsing.
*
* @author Bob Foster
*/
public class SchemaFactory implements DeclHandler {
// used for parsing models
private char[] fBuf;
private int fLen;
private int fPos;
private Element fElement;
private Schema fSchema;
private static HashSet<String> fTypes = new HashSet<>();
private Exception fErrorException;
static {
fTypes.add("CDATA"); //$NON-NLS-1$
fTypes.add("ID"); //$NON-NLS-1$
fTypes.add("IDREF"); //$NON-NLS-1$
fTypes.add("IDREFS"); //$NON-NLS-1$
fTypes.add("NMTOKEN"); //$NON-NLS-1$
fTypes.add("NMTOKENS"); //$NON-NLS-1$
fTypes.add("ENTITY"); //$NON-NLS-1$
fTypes.add("ENTITIES"); //$NON-NLS-1$
}
/**
* Constructor.
*/
public SchemaFactory() {
fSchema = new Schema();
}
/**
* @return ISchema produced from the DeclHandler. The schema is always correct, though it may be incomplete if the parse was interrupted due to
* validation or well-formed errors.
*/
public ISchema getSchema() {
fSchema.setErrorException(fErrorException);
return fSchema;
}
@Override
public void attributeDecl(String eName, String aName, String type, String valueDefault, String value) {
Element element = getElement(eName);
Attribute attr = (Attribute) element.getAttributes().get(aName);
if (attr == null) {
attr = new Attribute(aName, element);
element.addAttribute(attr);
String[] enumeration = null;
if (fTypes.contains(type))
attr.setType(type);
else if (type.startsWith("NOTATION")) //$NON-NLS-1$
enumeration = parseValues(type.substring("NOTATION".length() + 1), ','); //$NON-NLS-1$
else {
type = stripSurroundingParentheses(type);
enumeration = parseValues(type, '|');
}
attr.setEnum(enumeration);
attr.setRequired(valueDefault == null || !valueDefault.equals("#IMPLIED")); //$NON-NLS-1$
attr.setFixed(valueDefault != null && valueDefault.equals("#FIXED")); //$NON-NLS-1$
attr.setDefault(value);
}
}
/**
* Strips the surrounding parentheses from <code>aString</code>.
* <P>
* i.e.: {@code (true|false) -> true|false }
*/
private String stripSurroundingParentheses(String aString) {
if (aString.startsWith("(")) { //$NON-NLS-1$
aString = aString.substring(1);
}
if (aString.endsWith(")")) { //$NON-NLS-1$
aString = aString.substring(0, aString.length() - 1);
}
return aString;
}
/**
* @param eName
* Element name
* @return Element from schema or new element. Either way the element is in the schema upon return.
*/
private Element getElement(String eName) {
Element element = (Element) fSchema.getElement(eName);
if (element == null) {
element = new Element(eName);
fSchema.addElement(element);
}
return element;
}
private String[] parseValues(String type, char separator) {
int start = 0, pos, len = type.length();
LinkedList<String> values = new LinkedList<>();
while (start < len) {
pos = type.indexOf(separator, start);
if (pos < 0)
pos = len;
String term = type.substring(start, pos);
start = pos + 1;
values.add(term);
}
return values.toArray(new String[values.size()]);
}
@Override
public void elementDecl(String name, String model) throws SAXException {
Element element = getElement(name);
if (!element.isUndefined()) {
// if the element has already been defined, this is an error
throw new SAXException(MessageFormat.format(AntDTDSchemaMessages.SchemaFactory_Doubly_defined, new Object[] { name }));
}
fElement = element;
if (model.equals("ANY")) { //$NON-NLS-1$
element.setAny(true);
} else if (model.equals("EMPTY")) { //$NON-NLS-1$
element.setEmpty(true);
} else if (model.equals("(#PCDATA)")) {//$NON-NLS-1$
element.setText(true);
} else {
element.setContentModel(parseModel(model));
}
}
/**
* Convert model string to IModel. The <code>fElement</code> variable is an implicit argument to this method, and it sets <code>fBuf</code>,
* <code>fPos</code> and <code>fLen</code> for use by other parser methods.
*
* @param model
* String from DTD, with parameter entities replaced.
* @return IModel
* @throws SAXException
* if syntax error detected in model. This is a validation error. Since the DTD is usually not read unless the parser is validating,
* we may not ever be handed a bad content model, but we need to check them, just the same.
*/
private IModel parseModel(String model) throws SAXException {
fBuf = model.toCharArray();
fLen = fBuf.length;
if (fBuf[0] != '(') {
throw new SAXException(MessageFormat.format(AntDTDSchemaMessages.SchemaFactory_Start_with_left_parenthesis, new Object[] {
fElement.getName() }));
}
boolean ortext = model.startsWith("(#PCDATA|"); //$NON-NLS-1$
if (ortext) {
fPos = 8; // "(#PCDATA".length()
} else {
fPos = 0;
}
IModel emodel = scanExpr();
return emodel;
}
/**
* Scan a parenthesized expression starting from the left parenthesis or leftmost operator.
*
* @return IModel
*/
private IModel scanExpr() throws SAXException {
// skip opening ( or |
fPos++;
return scanExpr(scanElement());
}
/**
* Scan a parenthesized expression with the first term in hand.
*
* @param term
* The first operand in the expression, pre-scanned.
* @return IModel
* @throws SAXException
* if errors are detected in the model.
*/
private IModel scanExpr(IModel term) throws SAXException {
checkLen();
if (fBuf[fPos] != ')') {
char op = fBuf[fPos];
if (op != '|' && op != ',') {
throw new SAXException(MessageFormat.format(AntDTDSchemaMessages.SchemaFactory_Expecting_operator_or_right_parenthesis, new Object[] {
fElement.getName(), String.valueOf(fBuf) }));
}
Model model = new Model(op == '|' ? IModel.CHOICE : IModel.SEQUENCE);
model.addModel(term);
term = model;
while (fBuf[fPos] == op) {
fPos++;
IModel next = scanElement();
model.addModel(next);
}
if (fBuf[fPos] != ')') {
throw new SAXException(MessageFormat.format(AntDTDSchemaMessages.SchemaFactory_Expecting_operator_or_right_parenthesis, new Object[] {
fElement.getName(), String.valueOf(fBuf) }));
}
fPos++;
}
return term;
}
/**
* Scan an element name or a parenthesized sub-expression.
*
* @return IModel
* @throws SAXException
*/
private IModel scanElement() throws SAXException {
checkLen();
if (fBuf[fPos] == '(')
return scanExpr();
StringBuilder sb = new StringBuilder();
while (fBuf[fPos] != '|' && fBuf[fPos] != ',' && fBuf[fPos] != ')' && fBuf[fPos] != '*' && fBuf[fPos] != '+' && fBuf[fPos] != '?') {
sb.append(fBuf[fPos++]);
checkLen();
}
String name = sb.toString();
Element element = getElement(name);
Model model = new Model(IModel.LEAF);
model.setLeaf(element);
return model;
}
private void checkLen() throws SAXException {
if (fPos == fLen) {
throw new SAXException(MessageFormat.format(AntDTDSchemaMessages.SchemaFactory_Unexpected_end, new Object[] { fElement.getName(),
String.valueOf(fBuf) }));
}
}
@Override
public void externalEntityDecl(String name, String publicId, String systemId) {
// do nothing
}
@Override
public void internalEntityDecl(String name, String value) {
// do nothing
}
public void setErrorException(Exception e) {
fErrorException = e;
}
}