/**
* Copyright (C) 2010 Orbeon, Inc.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU Lesser General Public License as published by the Free Software Foundation; either version
* 2.1 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
*/
package org.orbeon.oxf.xforms;
import org.apache.log4j.Logger;
import org.orbeon.oxf.common.OXFException;
import org.orbeon.oxf.util.IndentedLogger;
import org.orbeon.oxf.util.LoggerFactory;
import org.orbeon.oxf.xforms.analysis.XPathDependencies;
import org.orbeon.oxf.xforms.control.*;
import org.orbeon.oxf.xforms.control.controls.XFormsRepeatControl;
import org.orbeon.oxf.xforms.control.controls.XFormsRepeatIterationControl;
import org.orbeon.oxf.xforms.itemset.Itemset;
import org.orbeon.oxf.xforms.model.XFormsModel;
import org.orbeon.oxf.xforms.state.ControlState;
import org.orbeon.saxon.om.Item;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Represents all this XForms containing document controls and the context in which they operate.
*/
public class XFormsControls implements XFormsObjectResolver {
public static final String LOGGING_CATEGORY = "control";
public static final Logger logger = LoggerFactory.createLogger(XFormsModel.class);
public final IndentedLogger indentedLogger;
private boolean initialized;
private ControlTree initialControlTree;
private ControlTree currentControlTree;
// Crude flag to indicate that something might have changed since the last request. This caches simples cases where
// an incoming change on the document does not cause any change to the data or controls. In that case, the control
// trees need not be compared. A general mechanism detecting mutations in the proper places would be better.
private boolean dirtySinceLastRequest;
// Whether we currently require a UI refresh
private boolean requireRefresh = false;
// Whether we are currently in a refresh
private boolean inRefresh = false;
private final XFormsContainingDocument containingDocument;
private Map<String, Itemset> constantItems;
private final XPathDependencies xpathDependencies;
public XFormsControls(XFormsContainingDocument containingDocument) {
this.indentedLogger = containingDocument.getIndentedLogger(LOGGING_CATEGORY);
this.containingDocument = containingDocument;
this.xpathDependencies = containingDocument.getXPathDependencies();
// Create minimal tree
initialControlTree = new ControlTree(indentedLogger);
currentControlTree = initialControlTree;
}
public boolean isInitialized() {
return initialized;
}
public IndentedLogger getIndentedLogger() {
return indentedLogger;
}
public boolean isDirtySinceLastRequest() {
return dirtySinceLastRequest;
}
public void markDirtySinceLastRequest(boolean bindingsAffected) {
dirtySinceLastRequest = true;
if (bindingsAffected)
currentControlTree.markBindingsDirty();
}
private void markCleanSinceLastRequest() {
dirtySinceLastRequest = false;
currentControlTree.markBindingsClean();
}
public void requireRefresh() {
this.requireRefresh = true;
markDirtySinceLastRequest(true);
}
public boolean isRequireRefresh() {
return requireRefresh;
}
public boolean isInRefresh() {
return inRefresh;
}
public void refreshStart() {
requireRefresh = false;
inRefresh = true;
xpathDependencies.refreshStart();
}
public void refreshDone() {
inRefresh = false;
xpathDependencies.refreshDone();
}
/**
* Create the controls, whether upon initial creation of restoration of the controls.
*/
public void createControlTree(scala.Option<scala.collection.immutable.Map<String, ControlState>> state) {
assert !initialized;
if (containingDocument.getStaticState().topLevelPart().hasControls()) {
// Create new controls tree
// NOTE: We set this first so that the tree is made available during construction to XPath functions like index() or case()
currentControlTree = initialControlTree = new ControlTree(indentedLogger);
// Set this here so that while initialize() runs below, refresh events will find the flag set
initialized = true;
// Initialize new control tree
currentControlTree.initialize(containingDocument, state);
} else {
// Consider initialized
initialized = true;
}
}
/**
* Adjust the controls after sending a response.
*
* This makes sure that we don't keep duplicate control trees.
*/
public void afterUpdateResponse() {
assert initialized;
if (containingDocument.getStaticState().topLevelPart().hasControls()) {
// Keep only one control tree
initialControlTree = currentControlTree;
// We are now clean
markCleanSinceLastRequest();
// Need to make sure that current == initial within controls
Controls.visitAllControls(containingDocument, new Controls.XFormsControlVisitorAdapter() {
public boolean startVisitControl(XFormsControl control) {
control.resetLocal();
return true;
}
});
}
}
public XFormsContainingDocument getContainingDocument() {
return containingDocument;
}
/**
* Create a new repeat iteration for insertion into the current tree of controls.
*
* @param repeatControl repeat control
* @param iterationIndex new iteration index (1..repeat size + 1)
* @return newly created repeat iteration control
*/
public XFormsRepeatIterationControl createRepeatIterationTree(XFormsRepeatControl repeatControl, int iterationIndex) {
if (initialControlTree == currentControlTree && containingDocument.isHandleDifferences())
throw new OXFException("Cannot call insertRepeatIteration() when initialControlTree == currentControlTree");
final XFormsRepeatIterationControl repeatIterationControl;
indentedLogger.startHandleOperation("controls", "adding iteration");
repeatIterationControl = currentControlTree.createRepeatIterationTree(containingDocument, repeatControl, iterationIndex);
indentedLogger.endHandleOperation();
return repeatIterationControl;
}
/**
* Get the ControlTree computed in the initialize() method.
*/
public ControlTree getInitialControlTree() {
return initialControlTree;
}
/**
* Get the last computed ControlTree.
*/
public ControlTree getCurrentControlTree() {
return currentControlTree;
}
/**
* Clone the current controls tree if:
*
* 1. it hasn't yet been cloned
* 2. we are not during the XForms engine initialization
*
* The rationale for #2 is that there is no controls comparison needed during initialization. Only during further
* client requests do the controls need to be compared.
*/
public void cloneInitialStateIfNeeded() {
if (initialControlTree == currentControlTree && containingDocument.isHandleDifferences()) {
indentedLogger.startHandleOperation("controls", "cloning");
{
// NOTE: We clone "back", that is the new tree is used as the "initial" tree. This is done so that
// if we started working with controls in the initial tree, we can keep using those references safely.
initialControlTree = (ControlTree) currentControlTree.getBackCopy();
}
indentedLogger.endHandleOperation();
}
}
/**
* Get object with the effective id specified.
*
* @param effectiveId effective id of the target
* @return object, or null if not found
*/
public XFormsControl getObjectByEffectiveId(String effectiveId) {
return currentControlTree.findControlOrNullJava(effectiveId);
}
/**
* Resolve an object. This optionally depends on a source control, and involves resolving whether the source is within a
* repeat or a component.
*
* @param sourceControlEffectiveId effective id of the source control
* @param targetStaticId static id of the target
* @param contextItem context item, or null (used for bind resolution only)
* @return object, or null if not found
*/
public XFormsObject resolveObjectById(String sourceControlEffectiveId, String targetStaticId, Item contextItem) {
return Controls.resolveObjectByIdJava(containingDocument, sourceControlEffectiveId, targetStaticId);
}
/**
* Get the items for a given control id. This is not an effective id, but an original control id.
*
* @param controlId original control id
* @return itemset
*/
public Itemset getConstantItems(String controlId) {
if (constantItems == null)
return null;
else
return constantItems.get(controlId);
}
/**
* Set the items for a given control id. This is not an effective id, but an original control id.
*
* @param controlId static control id
* @param itemset itemset
*/
public void setConstantItems(String controlId, Itemset itemset) {
if (constantItems == null)
constantItems = new HashMap<String, Itemset>();
constantItems.put(controlId, itemset);
}
public void doRefresh() {
if (inRefresh) {
// Ignore "nested refresh"
// See https://github.com/orbeon/orbeon-forms/issues/1550
indentedLogger.logDebug("controls", "attempt to do nested refresh");
return;
}
// This method implements the new refresh event algorithm:
// http://wiki.orbeon.com/forms/doc/developer-guide/xforms-refresh-events
// Don't do anything if there are no children controls
if (getCurrentControlTree().children().isEmpty()) {
indentedLogger.logDebug("controls", "not performing refresh because no controls are available");
refreshStart();
refreshDone();
} else {
indentedLogger.startHandleOperation("controls", "performing refresh");
{
final XFormsControl focusedBefore;
final Controls.BindingUpdater updater;
final List<String> controlsEffectiveIds;
// Notify dependencies
refreshStart();
try {
// Focused control before updating bindings
focusedBefore = getFocusedControl();
// Update control bindings
// NOTE: During this process, ideally, no events are dispatched. However, at this point, the code
// can an dispatch, upon removed repeat iterations, xforms-disabled, DOMFocusOut and possibly events
// arising from updating the binding of nested XBL controls.
// This unfortunately means that side effects can take place. This should be fixed, maybe by simply
// detaching removed iterations first, and then dispatching events after all bindings have been
// updated as part of dispatchRefreshEvents() below. This requires that controls are able to kind of
// stay alive in detached mode, and then that the index is also available while these events are
// dispatched.
updater = updateControlBindings();
// There are potentially event handlers for UI events, so do the whole processing
// Gather controls to which to dispatch refresh events
controlsEffectiveIds = (updater != null) ? gatherControlsForRefresh() : null;
} finally {
// "Actions that directly invoke rebuild, recalculate, revalidate, or refresh always have an immediate
// effect, and clear the corresponding flag."
refreshDone();
}
if (updater != null) {
// Dispatch events
currentControlTree.dispatchRefreshEventsJava(controlsEffectiveIds);
// Handle focus changes
Focus.updateFocusWithEvents(focusedBefore, updater.partialFocusRepeat());
}
}
indentedLogger.endHandleOperation();
}
}
/**
* Update all the control bindings.
*
* Return null if controls are not initialized or if control bindings are not dirty. Otherwise, control bindings are
* updated and the BindingUpdater is returned.
*/
private Controls.BindingUpdater updateControlBindings() {
if (!initialized) {
return null;
} else {
// This is the regular case
// Don't do anything if bindings are clean
if (!currentControlTree.bindingsDirty())
return null;
// Clone if needed
cloneInitialStateIfNeeded();
// Visit all controls and update their bindings
indentedLogger.startHandleOperation("controls", "updating bindings");
final Controls.BindingUpdater updater = Controls.updateBindings(containingDocument);
indentedLogger.endHandleOperation(
"controls visited", Integer.toString(updater.visitedCount()),
"bindings evaluated", Integer.toString(updater.updatedCount()),
"bindings optimized", Integer.toString(updater.optimizedCount())
);
// Controls are clean
initialControlTree.markBindingsClean();
currentControlTree.markBindingsClean();
return updater;
}
}
private List<String> gatherControlsForRefresh() {
final List<String> eventsToDispatch = new ArrayList<String>();
Controls.visitAllControls(containingDocument, new Controls.XFormsControlVisitorAdapter() {
public boolean startVisitControl(XFormsControl control) {
if (XFormsControl.controlSupportsRefreshEvents(control)) {// test here to make smaller list
eventsToDispatch.add(control.getEffectiveId());
}
return true;
}
});
return eventsToDispatch;
}
/**
* Do a refresh of a subtree of controls starting at the given container control.
*
* @param containerControl container control
*/
public void doPartialRefresh(XFormsContainerControl containerControl) {
// Focused control before updating bindings
final XFormsControl focusedBefore = getFocusedControl();
// Update bindings starting at the container control
final Controls.BindingUpdater updater = updateSubtreeBindings(containerControl);
// There are potentially event handlers for UI events, so do the whole processing
// Gather controls to which to dispatch refresh events
final List<String> eventsToDispatch = gatherControlsForRefresh(containerControl);
// Dispatch events
currentControlTree.dispatchRefreshEventsJava(eventsToDispatch);
// Handle focus changes
Focus.updateFocusWithEvents(focusedBefore, updater.partialFocusRepeat());
}
/**
* Update the bindings of a container control and its descendants.
*
* @param containerControl container control
*/
private Controls.BindingUpdater updateSubtreeBindings(XFormsContainerControl containerControl) {
// Clone if needed
cloneInitialStateIfNeeded();
indentedLogger.startHandleOperation("controls", "updating bindings", "container", ((XFormsControl) containerControl).getEffectiveId());
final Controls.BindingUpdater updater = Controls.updateBindings(containerControl);
indentedLogger.endHandleOperation(
"controls visited", Integer.toString(updater.visitedCount()),
"bindings evaluated", Integer.toString(updater.updatedCount()),
"bindings optimized", Integer.toString(updater.optimizedCount())
);
return updater;
}
private List<String> gatherControlsForRefresh(XFormsContainerControl containerControl) {
final List<String> eventsToDispatch = new ArrayList<String>();
Controls.visitControls((XFormsControl) containerControl, new Controls.XFormsControlVisitorAdapter() {
public boolean startVisitControl(XFormsControl control) {
if (XFormsControl.controlSupportsRefreshEvents(control)) {// test here to make smaller list
eventsToDispatch.add(control.getEffectiveId());
}
return true;
}
}, true);
return eventsToDispatch;
}
// Remember which control owns focus if any
private XFormsControl focusedControl = null;
public XFormsControl getFocusedControl() {
return focusedControl;
}
public void setFocusedControl(XFormsControl focusedControl) {
this.focusedControl = focusedControl;
}
public void clearFocusedControl() {
this.focusedControl = null;
}
}