@@ -630,7 +630,7 @@ private void validateTypeAndRequired(Bind bind, int position, Set<String> invali

// Don't try to apply validity to a node if it has children nodes or if it's not a node
// "The type model item property is not applied to instance nodes that contain child elements"
final Bind.BindNode bindNode = bind.getBindNode(position);
final BindNode bindNode = bind.getBindNode(position);
final NodeInfo currentNodeInfo = bindNode.nodeInfo;
if (currentNodeInfo == null || bindNode.hasChildrenElements)
return;
@@ -690,7 +690,8 @@ private void validateTypeAndRequired(Bind bind, int position, Set<String> invali
// Remember invalid instances
if (!typeValidity || !requiredValidity) {
final XFormsInstance instanceForNodeInfo = containingDocument.getInstanceForNode(currentNodeInfo);
invalidInstances.add(instanceForNodeInfo.getEffectiveId());
if (instanceForNodeInfo != null)
invalidInstances.add(instanceForNodeInfo.getEffectiveId());
}
}

@@ -699,7 +700,7 @@ private void validateConstraint(Bind bind, int position, Set<String> invalidInst
assert bind.staticBind.getConstraint() != null;

// Don't try to apply validity to a node if it's not a node
final Bind.BindNode bindNode = bind.getBindNode(position);
final BindNode bindNode = bind.getBindNode(position);
final NodeInfo currentNodeInfo = bindNode.nodeInfo;
if (currentNodeInfo == null)
return;
@@ -927,7 +928,7 @@ public Bind(BindTree.Bind staticBind, boolean isSingleNodeContext) {
{
// Create iteration and remember it
final boolean isNewSingleNodeContext = isSingleNodeContext && nodesetSize == 1;
final BindIteration currentBindIteration = new BindIteration(isNewSingleNodeContext, item, childrenStaticBinds);
final BindIteration currentBindIteration = new BindIteration(getStaticId(), isNewSingleNodeContext, item, childrenStaticBinds, typeQName);
bindNodes.add(currentBindIteration);

// Create mapping context node -> iteration
@@ -948,7 +949,7 @@ public Bind(BindTree.Bind staticBind, boolean isSingleNodeContext) {
bindNodes = new ArrayList<BindNode>(nodesetSize);

for (final Item item : nodeset)
bindNodes.add(new BindNode(item));
bindNodes.add(new BindNode(getStaticId(), item, typeQName));
}
}

@@ -1044,128 +1045,130 @@ public boolean isValid(int position) {
return getBindNode(position).isValid();
}

// BindNode holds MIP values for a given bind node
public class BindNode {

// Current MIP state
private boolean relevant = Model.DEFAULT_RELEVANT();
protected boolean readonly = Model.DEFAULT_READONLY();
private boolean required = Model.DEFAULT_REQUIRED();
private Map<String, String> customMips = null;

private boolean typeValidity = Model.DEFAULT_VALID();
private boolean requiredValidity = Model.DEFAULT_VALID();
private boolean constraintValidity = Model.DEFAULT_VALID();

public final NodeInfo nodeInfo;
public final boolean hasChildrenElements;

private BindNode(Item item) {
if (item instanceof NodeInfo) {
nodeInfo = (NodeInfo) item;
hasChildrenElements = nodeInfo.getNodeKind() == org.w3c.dom.Document.ELEMENT_NODE && XML.hasChildElement(nodeInfo);

// Add us to the node
InstanceData.addBindNode(nodeInfo, this);
if (Bind.this.typeQName != null)
InstanceData.setBindType(nodeInfo, Bind.this.typeQName);
} else {
nodeInfo = null;
hasChildrenElements = false;
}
}
// Bind node that also contains nested binds
private class BindIteration extends BindNode {// TODO: if bind doesn't have MIPs, BindNode storage is not needed

public String getBindStaticId() {
return Bind.this.getStaticId();
}
private List<Bind> childrenBinds;

public void setRelevant(boolean value) {
this.relevant = value;
}
public BindIteration(String bindStaticId, boolean isSingleNodeContext, Item item, List<BindTree.Bind> childrenStaticBinds, QName typeQName) {

public void setReadonly(boolean value) {
this.readonly = value;
}
super(bindStaticId, item, typeQName);

public void setRequired(boolean value) {
this.required = value;
}
assert childrenStaticBinds.size() > 0;

public void setCustom(String name, String value) {
if (customMips == null)
customMips = new HashMap<String, String>(); // maybe should be LinkedHashMap for reproducibility
customMips.put(name, value);
// Iterate over children and create children binds
childrenBinds = new ArrayList<Bind>(childrenStaticBinds.size());
for (final BindTree.Bind staticBind : childrenStaticBinds)
childrenBinds.add(new Bind(staticBind, isSingleNodeContext));
}

public void setTypeValidity(boolean value) {
this.typeValidity = value;
public void applyBinds(BindRunner bindRunner) {
for (final Bind currentBind : childrenBinds)
currentBind.applyBinds(bindRunner);
}

public void setRequiredValidity(boolean value) {
this.requiredValidity = value;
public Bind getBind(String bindId) {
for (final Bind currentBind : childrenBinds)
if (currentBind.staticBind.staticId().equals(bindId))
return currentBind;
return null;
}
}
}

public void setConstraintValidity(boolean value) {
this.constraintValidity = value;
// BindNode holds MIP values for a given bind node
public static class BindNode {

// Current MIP state
private boolean relevant = Model.DEFAULT_RELEVANT();
protected boolean readonly = Model.DEFAULT_READONLY();
private boolean required = Model.DEFAULT_REQUIRED();
private Map<String, String> customMips = null;

private boolean typeValidity = Model.DEFAULT_VALID();
private boolean requiredValidity = Model.DEFAULT_VALID();
private boolean constraintValidity = Model.DEFAULT_VALID();

public final String bindStaticId;
public final NodeInfo nodeInfo;
public final boolean hasChildrenElements;

public BindNode(String bindStaticId, Item item, QName typeQName) {
this.bindStaticId = bindStaticId;
if (item instanceof NodeInfo) {
nodeInfo = (NodeInfo) item;
hasChildrenElements = nodeInfo.getNodeKind() == org.w3c.dom.Document.ELEMENT_NODE && XML.hasChildElement(nodeInfo);

// Add us to the node
InstanceData.addBindNode(nodeInfo, this);
if (typeQName != null)
InstanceData.setBindType(nodeInfo, typeQName);
} else {
nodeInfo = null;
hasChildrenElements = false;
}
}

public boolean isRelevant() {
return relevant;
}
public String getBindStaticId() {
return bindStaticId;
}

public boolean isReadonly() {
return readonly;
}
public void setRelevant(boolean value) {
this.relevant = value;
}

public boolean isRequired() {
return required;
}
public void setReadonly(boolean value) {
this.readonly = value;
}

public boolean isValid() {
return typeValidity && requiredValidity && constraintValidity;
}
public void setRequired(boolean value) {
this.required = value;
}

public boolean isTypeValid() {
return typeValidity;
}
public void setCustom(String name, String value) {
if (customMips == null)
customMips = new HashMap<String, String>(); // maybe should be LinkedHashMap for reproducibility
customMips.put(name, value);
}

public boolean isConstraintValidity() {
return constraintValidity;
}
public void setTypeValidity(boolean value) {
this.typeValidity = value;
}

public Map<String, String> getCustomMips() {
return customMips == null ? null : Collections.unmodifiableMap(customMips);
}
public void setRequiredValidity(boolean value) {
this.requiredValidity = value;
}

// Bind node that also contains nested binds
private class BindIteration extends BindNode {// TODO: if bind doesn't have MIPs, BindNode storage is not needed
public void setConstraintValidity(boolean value) {
this.constraintValidity = value;
}

private List<Bind> childrenBinds;
public boolean isRelevant() {
return relevant;
}

public BindIteration(boolean isSingleNodeContext, Item item, List<BindTree.Bind> childrenStaticBinds) {
public boolean isReadonly() {
return readonly;
}

super(item);
public boolean isRequired() {
return required;
}

assert childrenStaticBinds.size() > 0;
public boolean isValid() {
return typeValidity && requiredValidity && constraintValidity;
}

// Iterate over children and create children binds
childrenBinds = new ArrayList<Bind>(childrenStaticBinds.size());
for (final BindTree.Bind staticBind : childrenStaticBinds)
childrenBinds.add(new Bind(staticBind, isSingleNodeContext));
}
public boolean isTypeValid() {
return typeValidity;
}

public void applyBinds(BindRunner bindRunner) {
for (final Bind currentBind : childrenBinds)
currentBind.applyBinds(bindRunner);
}
public boolean isConstraintValidity() {
return constraintValidity;
}

public Bind getBind(String bindId) {
for (final Bind currentBind : childrenBinds)
if (currentBind.staticBind.staticId().equals(bindId))
return currentBind;
return null;
}
public Map<String, String> getCustomMips() {
return customMips == null ? null : Collections.unmodifiableMap(customMips);
}
}
}
@@ -17,11 +17,13 @@ import org.orbeon.oxf.xforms.action.XFormsAPI._
import org.orbeon.scaxon.XML._
import org.orbeon.oxf.fb.FormBuilderFunctions._
import org.orbeon.oxf.fb.ControlOps._
import org.orbeon.oxf.fb.ContainerOps._
import org.orbeon.saxon.om._
import org.orbeon.oxf.xforms.control.XFormsControl
import org.orbeon.oxf.xforms.analysis.controls.SingleNodeTrait
import org.orbeon.oxf.xml.NamespaceMapping
import org.orbeon.oxf.xml.dom4j.Dom4jUtils
import org.orbeon.oxf.xforms.InstanceData
import org.orbeon.oxf.xforms.XFormsModelBinds.BindNode

object DataModel {

@@ -121,17 +123,22 @@ object DataModel {

// Function called via `dataModel:bindRef()` from the binds to retrieve the adjusted bind node at design time. If
// the bound item is non-empty and acceptable for the control, then it is returned. Otherwise, a pointer to
// `instance('fb-readonly')` is returned.
def bindRef(bindId: String, i: SequenceIterator) = {
val itemOption = asScalaSeq(i).headOption

if (isAllowedBoundItem(controlName(bindId), itemOption))
// a dangling element is returned. This allows that element to hold MIPs. The element is marked as readonly so that
// controls cannot write to it directly.
def bindRef(bindId: String, i: SequenceIterator): SequenceIterator =
if (isAllowedBoundItem(controlName(bindId), asScalaSeq(i).headOption))
i.getAnother
else
SingletonIterator.makeIterator(
getFormModel.getInstance("fb-readonly") ensuring (_ ne null, "did not find fb-readonly") getInstanceRootElementInfo)

}
else {
// Create and wrap the dangling element
val newElement = Dom4jUtils.createElement("orbeon-dangling-element")
val newElementInfo = containingDocument.getStaticState.documentWrapper.wrap(newElement)
// Then attach a readonly bind node to it. This is as if there was a <bind readonly="true" for the node>
val bindNode = new BindNode(bindId, newElementInfo, null)
bindNode.setReadonly(true)
InstanceData.addBindNode(newElementInfo, bindNode)

SingletonIterator.makeIterator(newElementInfo)
}

// For a given value control name and XPath expression, whether the resulting bound item is acceptable
// Called from control details dialog
@@ -22,14 +22,14 @@ import org.orbeon.oxf.xforms.XFormsConstants.{XFORMS_NAMESPACE_URI, XBL_NAMESPAC
import org.orbeon.oxf.xml.XMLConstants.{XHTML_NAMESPACE_URI, XSD_URI}
import org.orbeon.oxf.util.DebugLogger._
import org.orbeon.oxf.util.{UserAgent, NetUtils}
import org.orbeon.oxf.xforms.{XFormsModel, XFormsProperties, Loggers}
import org.orbeon.oxf.xforms.{XFormsModel, XFormsProperties}

/**
* Form Builder functions.
*/
object FormBuilderFunctions {

private implicit val Logger = Loggers.getIndentedLogger("form-builder")
implicit def logger = containingDocument.getIndentedLogger("form-builder")

val XH = XHTML_NAMESPACE_URI
val XF = XFORMS_NAMESPACE_URI
@@ -18,8 +18,12 @@ object DebugLogger {

// Simple debug with optional parameters
def warn(message: String, parameters: Seq[(String, String)] = Seq())(implicit logger: IndentedLogger) =
if (logger.isDebugEnabled)
logger.logWarning("", message, flattenTuples(parameters): _*)
logger.logWarning("", message, flattenTuples(parameters): _*)

// Simple debug with optional parameters
def info(message: String, parameters: Seq[(String, String)] = Seq())(implicit logger: IndentedLogger) =
if (logger.isInfoEnabled)
logger.logInfo("", message, flattenTuples(parameters): _*)

// Simple debug with optional parameters
def debug(message: String, parameters: Seq[(String, String)] = Seq())(implicit logger: IndentedLogger) =
@@ -18,11 +18,7 @@ import org.orbeon.saxon.expr.XPathContext
import org.orbeon.saxon.om.Item

/**
* xxforms:attribute(xs:string, xs:anyAtomicType?) as attribute()
*
* Creates a new XML attribute. The first argument is a string representing a QName representing the name of the
* attribute to create. If a prefix is present, it is resolved with the namespace mappings in scope where the expression
* is evaluated. The second attribute is an optional attribute value. The default is the empty string.
* xxforms:attribute()
*/
class XFormsAttribute extends XFormsFunction {
override def evaluateItem(xpathContext: XPathContext): Item = {
@@ -24,10 +24,7 @@ import org.orbeon.scaxon.XML._
import org.orbeon.oxf.xml.Dom4j

/**
* xxforms:element(xs:string) as element()
*
* Creates a new XML element. The argument is a string representing a QName. If a prefix is present, it is resolved
* with the namespace mappings in scope where the expression is evaluated.
* xxforms:element()
*/
class XFormsElement extends XFormsFunction {