blob: 90114bc1d912ea895b298343f5c2c20c2d171b96 [file] [log] [blame]
/* Copyright 2004 The Apache Software Foundation
*
* 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.xmlbeans.impl.common;
import org.apache.xmlbeans.impl.common.ValidatorListener.Event;
import javax.xml.namespace.QName;
import org.apache.xmlbeans.*;
import org.apache.xmlbeans.impl.common.SequencedHashSet;
import java.util.*;
/**
* Identity constraint engine. Performs streaming validation of identity constraints.
* This includes key, keyref, & unique, as well as ID & IDRef.
*/
public class IdentityConstraint {
private ConstraintState _constraintStack;
private ElementState _elementStack;
private Collection _errorListener;
private boolean _invalid;
private boolean _trackIdrefs; // We only track idrefs if validating from the root element
public IdentityConstraint(Collection errorListener, boolean trackIdrefs) {
_errorListener = errorListener;
_trackIdrefs = trackIdrefs;
}
public void element(Event e, SchemaType st, SchemaIdentityConstraint[] ics) {
// Construct a new state for the element
newState();
// First dispatch this element event
for (ConstraintState cs = _constraintStack ; cs != null ; cs = cs._next)
cs.element(e, st);
// Create a new SelectorState for each new Identity Constraint
for (int i = 0 ; ics != null && i < ics.length ; i++)
newConstraintState(ics[i], e, st);
}
public void endElement(Event e) {
// Pop the element state stack and any constraints at this depth
if (_elementStack._hasConstraints)
{
for (ConstraintState cs = _constraintStack ; cs != null && cs != _elementStack._savePoint ; cs = cs._next)
cs.remove( e );
_constraintStack = _elementStack._savePoint;
}
_elementStack = _elementStack._next;
// Dispatch the event
for (ConstraintState cs = _constraintStack ; cs != null ; cs = cs._next)
cs.endElement(e);
}
public void attr(Event e, QName name, SchemaType st, String value) {
for (ConstraintState cs = _constraintStack ; cs != null ; cs = cs._next)
cs.attr(e, name, st, value);
}
public void text(Event e, SchemaType st, String value, boolean emptyContent) {
for (ConstraintState cs = _constraintStack ; cs != null ; cs = cs._next)
cs.text(e, st, value, emptyContent);
}
public boolean isValid() {
return !_invalid;
}
private void newConstraintState(SchemaIdentityConstraint ic, Event e, SchemaType st)
{
if (ic.getConstraintCategory() == SchemaIdentityConstraint.CC_KEYREF)
new KeyrefState(ic, e, st);
else
new SelectorState(ic, e, st);
}
private void buildIdStates()
{
// Construct states to hold the values for IDs and IDRefs
IdState ids = new IdState();
if (_trackIdrefs)
new IdRefState(ids);
}
private void newState() {
boolean firstTime = _elementStack == null;
ElementState st = new ElementState();
st._next = _elementStack;
_elementStack = st;
if (firstTime)
buildIdStates();
}
private void emitError ( Event event, String msg )
{
_invalid = true;
if (_errorListener != null)
{
if (XmlBeans.ASSERTS)
XmlBeans.assertTrue(event != null);
_errorListener.add(
XmlError.forCursor( msg, event.getLocationAsCursor() ) );
}
}
private void setSavePoint( ConstraintState cs )
{
if (! _elementStack._hasConstraints)
_elementStack._savePoint = cs;
_elementStack._hasConstraints = true;
}
private static XmlObject newValue(SchemaType st, String value)
{
try {
return st.newValue(value);
}
catch (IllegalArgumentException e) {
// This is a bit hacky. newValue throws XmlValueOutOfRangeException which is
// unchecked and declared in typeimpl. I can only catch its parent class,
// typeimpl is built after common.
// Ignore these exceptions. Assume that validation will catch them.
return null;
}
}
/**
* Return the simple type for schema type. If the schema type is already
* simple, just return it. If it is a complex type with simple content,
* return the simple type it extends.
*/
static SchemaType getSimpleType(SchemaType st)
{
if (XmlBeans.ASSERTS)
XmlBeans.assertTrue(st.isSimpleType() || st.getContentType() == SchemaType.SIMPLE_CONTENT,
st + " does not have simple content.");
while (! st.isSimpleType() )
st = st.getBaseType();
return st;
}
static boolean hasSimpleContent(SchemaType st)
{
return st.isSimpleType() || st.getContentType() == SchemaType.SIMPLE_CONTENT;
}
public abstract class ConstraintState {
ConstraintState _next;
ConstraintState()
{
setSavePoint(_constraintStack);
_next = _constraintStack;
_constraintStack = this;
}
abstract void element(Event e, SchemaType st);
abstract void endElement(Event e);
abstract void attr(Event e, QName name, SchemaType st, String value);
abstract void text(Event e, SchemaType st, String value, boolean emptyContent);
abstract void remove(Event e);
}
public class SelectorState extends ConstraintState {
SchemaIdentityConstraint _constraint;
Set _values = new SequencedHashSet();
XPath.ExecutionContext _context;
SelectorState(SchemaIdentityConstraint constraint, Event e, SchemaType st) {
_constraint = constraint;
_context = new XPath.ExecutionContext();
_context.init((XPath)_constraint.getSelectorPath());
if ( ( _context.start() & XPath.ExecutionContext.HIT ) != 0 )
createFieldState(e, st);
}
void addFields(XmlObjectList fields, Event e)
{
if (_constraint.getConstraintCategory() == SchemaIdentityConstraint.CC_KEYREF)
_values.add(fields);
else if (_values.contains(fields))
emitError(e, "Duplicate key '" + fields + "' for key or unique constraint " + QNameHelper.pretty(_constraint.getName()));
else
_values.add(fields);
}
void element(Event e, SchemaType st)
{
if ( ( _context.element(e.getName()) & XPath.ExecutionContext.HIT) != 0 )
createFieldState(e, st);
}
void endElement(Event e)
{
_context.end();
}
void createFieldState(Event e, SchemaType st) {
new FieldState(this, e, st);
}
void remove(Event e) {
// Bubble up key, unique values to keyrefs
for (ConstraintState cs = _next ; cs != null ; cs = cs._next )
{
if (cs instanceof KeyrefState)
{
KeyrefState kr = (KeyrefState)cs;
if (kr._constraint.getReferencedKey() == this)
kr.addKeyValues(_values);
}
}
}
void attr(Event e, QName name, SchemaType st, String value) {}
void text(Event e, SchemaType st, String value, boolean emptyContent) {}
}
public class KeyrefState extends SelectorState {
Set _keyValues = new HashSet();
KeyrefState(SchemaIdentityConstraint constraint, Event e, SchemaType st) {
super(constraint, e, st);
}
void addKeyValues(final Set values)
{
// BUG: remove duplicates
_keyValues.addAll(values);
}
void remove(Event e) {
// First check if there are any keys at the same stack level as this
// that may contribute key values to me
for (ConstraintState cs = _next ; cs != null && cs != _elementStack._savePoint ; cs = cs._next)
{
if (cs instanceof SelectorState)
{
SelectorState sel = (SelectorState)cs;
if (sel._constraint == _constraint.getReferencedKey())
addKeyValues(sel._values);
}
}
// validate all values have been seen
for (Iterator it = _values.iterator() ; it.hasNext() ; )
{
XmlObjectList fields = (XmlObjectList)it.next();
if (fields.unfilled() < 0 && ! _keyValues.contains(fields))
{
emitError(e, "Key '" + fields + "' not found (keyRef " + QNameHelper.pretty(_constraint.getName()) + ")");
return;
}
}
}
}
public class FieldState extends ConstraintState {
SelectorState _selector;
XPath.ExecutionContext[] _contexts;
boolean[] _needsValue;
XmlObjectList _value;
FieldState(SelectorState selector, Event e, SchemaType st) {
// System.out.println("Creating new Field State for: " + e.getName());
_selector = selector;
SchemaIdentityConstraint ic = selector._constraint;
int fieldCount = ic.getFields().length;
_contexts = new XPath.ExecutionContext[fieldCount];
_needsValue = new boolean[fieldCount];
_value = new XmlObjectList(fieldCount);
for (int i = 0 ; i < fieldCount ; i++)
{
_contexts[i] = new XPath.ExecutionContext();
_contexts[i].init((XPath)ic.getFieldPath(i));
if ( ( _contexts[i].start() & XPath.ExecutionContext.HIT ) != 0 )
{
// System.out.println("hit for element: " + e.getName());
if (!hasSimpleContent(st))
emitError(e, "Identity constraint field must have simple content");
else
_needsValue[i] = true;
}
}
}
void element(Event e, SchemaType st)
{
for (int i = 0 ; i < _contexts.length ; i++) {
if (_needsValue[i])
{
emitError(e, "Identity constraint field must have simple content");
_needsValue[i] = false;
}
}
for (int i = 0 ; i < _contexts.length ; i++) {
if ( ( _contexts[i].element(e.getName()) & XPath.ExecutionContext.HIT) != 0 )
{
if (! hasSimpleContent(st))
emitError(e, "Identity constraint field must have simple content");
else
_needsValue[i] = true;
}
}
}
void attr(Event e, QName name, SchemaType st, String value) {
// Null value indicates previously reported validation problem
if (value == null) return;
for (int i = 0 ; i < _contexts.length ; i++) {
if ( _contexts[i].attr(name) ) {
XmlObject o = newValue(st, value);
// Ignore invalid values. Assume that validation catches these
if (o == null) return;
boolean set = _value.set(o, i);
if (! set)
emitError(e, "Multiple instances of field with xpath: '"
+ _selector._constraint.getFields()[i] + "' for a selector");
}
}
}
void text(Event e, SchemaType st, String value, boolean emptyContent) {
// Null value indicates previously reported validation problem
if (value == null && !emptyContent) return;
for (int i = 0 ; i < _contexts.length ; i++) {
if ( _needsValue[i] ) {
if (emptyContent || !hasSimpleContent(st))
{
emitError(e, "Identity constraint field must have simple content");
return;
}
SchemaType simpleType = getSimpleType(st);
XmlObject o = newValue(simpleType, value);
// Ignore invalid values. Assume that validation catches these
if (o == null) return;
boolean set = _value.set(o, i);
if (! set)
emitError(e, "Multiple instances of field with xpath: '"
+ _selector._constraint.getFields()[i] + "' for a selector");
}
}
}
void endElement(Event e) {
// reset any _needsValue flags
// assume that if we didn't see the text, it was because of another validation
// error, so don't emit another one.
for (int i = 0 ; i < _needsValue.length ; i++)
{
_contexts[i].end();
_needsValue[i] = false;
}
}
void remove(Event e)
{
if (_selector._constraint.getConstraintCategory() == SchemaIdentityConstraint.CC_KEY &&
_value.unfilled() >= 0 )
{
// keys must have all values supplied
emitError(e, "Key " + QNameHelper.pretty(_selector._constraint.getName()) + " is missing field with xpath: '" + _selector._constraint.getFields()[_value.unfilled()] + "'");
}
else
{
// Finished. Add these fields to the selector state
_selector.addFields(_value, e);
}
}
}
public class IdState extends ConstraintState
{
Set _values = new SequencedHashSet();
IdState() { }
void attr(Event e, QName name, SchemaType st, String value)
{
handleValue(e, st, value);
}
void text(Event e, SchemaType st, String value, boolean emptyContent)
{
if (emptyContent)
return;
handleValue(e, st, value);
}
private void handleValue(Event e, SchemaType st, String value)
{
// Null value indicates previously reported validation problem
if (value == null) return;
if (st == null || st.isNoType())
{
// ignore invalid values. Assume that validation catches these
return;
}
if (XmlID.type.isAssignableFrom(st))
{
XmlObjectList xmlValue = new XmlObjectList(1);
XmlObject o = newValue(XmlID.type, value);
// Ignore invalid values. Assume that validation catches these
if (o == null) return;
xmlValue.set(o, 0);
if (_values.contains(xmlValue))
emitError(e, "Duplicate ID value '" + value + "'");
else
_values.add(xmlValue);
}
}
void element(Event e, SchemaType st) {}
void endElement(Event e){}
void remove(Event e){}
}
public class IdRefState extends ConstraintState
{
IdState _ids;
List _values;
IdRefState(IdState ids)
{
_ids = ids;
_values = new ArrayList();
}
private void handleValue(Event e, SchemaType st, String value)
{
// Null value indicates previously reported validation problem
if (value == null) return;
if (st == null || st.isNoType())
{
// ignore invalid values. Assume that validation catches these
return;
}
if (XmlIDREFS.type.isAssignableFrom(st))
{
XmlIDREFS lv = (XmlIDREFS)newValue(XmlIDREFS.type, value);
// Ignore invalid values. Assume that validation catches these
if (lv == null) return;
List l = lv.xgetListValue();
// Add one value for each idref in the list
for (int i = 0 ; i < l.size() ; i++)
{
XmlObjectList xmlValue = new XmlObjectList(1);
XmlIDREF idref = (XmlIDREF)l.get(i);
xmlValue.set(idref, 0);
_values.add(xmlValue);
}
}
else if (XmlIDREF.type.isAssignableFrom(st))
{
XmlObjectList xmlValue = new XmlObjectList(1);
XmlIDREF idref = (XmlIDREF)st.newValue(value);
// Ignore invalid values. Assume that validation catches these
if (idref == null) return;
xmlValue.set(idref, 0);
_values.add(xmlValue);
}
}
void attr(Event e, QName name, SchemaType st, String value)
{
handleValue(e, st, value);
}
void text(Event e, SchemaType st, String value, boolean emptyContent)
{
if (emptyContent)
return;
handleValue(e, st, value);
}
void remove(Event e)
{
// Validate each ref has a corresponding ID
for (Iterator it = _values.iterator() ; it.hasNext() ; )
{
Object o = it.next();
if (! _ids._values.contains(o))
{
emitError(e, "ID not found for IDRef value '" + o + "'");
}
}
}
void element(Event e, SchemaType st) { }
void endElement(Event e) { }
}
private static class ElementState {
ElementState _next;
boolean _hasConstraints;
ConstraintState _savePoint;
}
}