/** * 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.processor.handlers; import org.apache.commons.lang3.StringUtils; import org.orbeon.oxf.common.ValidationException; import org.orbeon.oxf.externalcontext.ExternalContext; import org.orbeon.oxf.xforms.*; import org.orbeon.oxf.xforms.control.XFormsComponentControl; import org.orbeon.oxf.xforms.control.XFormsControl; import org.orbeon.oxf.xforms.control.controls.XFormsCaseControl; import org.orbeon.oxf.xforms.control.controls.XFormsRepeatControl; import org.orbeon.oxf.xforms.control.controls.XFormsRepeatIterationControl; import org.orbeon.oxf.xforms.control.controls.XXFormsDynamicControl; import org.orbeon.oxf.xml.ElementHandlerController; import org.orbeon.oxf.xml.XMLConstants; import org.orbeon.oxf.xml.dom4j.LocationData; import org.xml.sax.*; import java.util.*; /** * Context used when converting XHTML+XForms into XHTML. */ public class HandlerContext { // Passed from constructor private final ElementHandlerController controller; private final XFormsContainingDocument containingDocument; private final ExternalContext externalContext; private final String topLevelControlEffectiveId; // Computed during construction private final String[] documentOrder; private final String labelElementName; private final String hintElementName; private final String helpElementName; private final String alertElementName; public final boolean isNoscript; // Context information private final Stack<PartAnalysis> partAnalysisStack; private Stack<String> componentContextStack; private Stack<RepeatContext> repeatContextStack; private Stack<Boolean> caseContextStack; public HandlerContext(ElementHandlerController controller, XFormsContainingDocument containingDocument, ExternalContext externalContext, String topLevelControlEffectiveId) { this.controller = controller; this.containingDocument = containingDocument; this.externalContext = externalContext; this.topLevelControlEffectiveId = topLevelControlEffectiveId; this.documentOrder = StringUtils.split(containingDocument.lhhacOrder()); this.labelElementName = containingDocument.getLabelElementName(); this.hintElementName = containingDocument.getHintElementName(); this.helpElementName = containingDocument.getHelpElementName(); this.alertElementName = containingDocument.getAlertElementName(); this.isNoscript = containingDocument.noscript(); // Top-level part is containing document this.partAnalysisStack = new Stack<PartAnalysis>(); this.partAnalysisStack.push(containingDocument.getStaticState().topLevelPart()); } public void pushPartAnalysis(PartAnalysis partAnalysis) { partAnalysisStack.push(partAnalysis); } public void popPartAnalysis() { partAnalysisStack.pop(); } public PartAnalysis getPartAnalysis() { return partAnalysisStack.peek(); } final public ElementHandlerController getController() { return controller; } final public XFormsContainingDocument getContainingDocument() { return containingDocument; } final public ExternalContext getExternalContext() { return externalContext; } final public String[] getDocumentOrder() { return documentOrder; } public String getLabelElementName() { return labelElementName; } public String getHintElementName() { return hintElementName; } public String getHelpElementName() { return helpElementName; } public String getAlertElementName() { return alertElementName; } final public boolean isNoScript() { return isNoscript; } public String findXHTMLPrefix() { final String prefix = controller.getNamespaceContext().getPrefix(XMLConstants.XHTML_NAMESPACE_URI); if (prefix != null) return prefix; if (XMLConstants.XHTML_NAMESPACE_URI.equals(controller.getNamespaceContext().getURI(""))) { return ""; } // TEMP: in this case, we should probably map our own prefix, or set // the default namespace and restore it on children elements throw new ValidationException("No prefix found for HTML namespace", LocationData.createIfPresent(controller.getLocator())); } private String findFormattingPrefix() { final String prefix = controller.getNamespaceContext().getPrefix(XMLConstants.OPS_FORMATTING_URI); if (prefix != null) return prefix; if (XMLConstants.OPS_FORMATTING_URI.equals(controller.getNamespaceContext().getURI(""))) { return ""; } return null; } public String findFormattingPrefixDeclare() throws SAXException { final String formattingPrefix; final boolean isNewPrefix; final String existingFormattingPrefix = findFormattingPrefix(); if (StringUtils.isEmpty(existingFormattingPrefix)) { // No prefix is currently mapped formattingPrefix = findNewPrefix(); isNewPrefix = true; } else { formattingPrefix = existingFormattingPrefix; isNewPrefix = false; } // Start mapping if needed if (isNewPrefix) getController().getOutput().startPrefixMapping(formattingPrefix, XMLConstants.OPS_FORMATTING_URI); return formattingPrefix; } public void findFormattingPrefixUndeclare(String formattingPrefix) throws SAXException { final String existingFormattingPrefix = findFormattingPrefix(); final boolean isNewPrefix = StringUtils.isEmpty(existingFormattingPrefix); // End mapping if needed if (isNewPrefix) getController().getOutput().endPrefixMapping(formattingPrefix); } public String findNewPrefix() { int i = 0; while (controller.getNamespaceContext().getURI("p" + i) != null) { i++; } return "p" + i; } public String getPrefixedId(Attributes controlElementAttributes) { final String id = controlElementAttributes.getValue("id"); return (id != null) ? getIdPrefix() + id : null; } public String getEffectiveId(Attributes controlElementAttributes) { final String prefixedId = getPrefixedId(controlElementAttributes); return (prefixedId != null) ? prefixedId + getIdPostfix() : null; } /** * Return true iif the given control effective id is the same as the top-level control passed during construction. * * NOTE: This is used by the repeat handler to not output delimiters during full updates. * * @param effectiveId control effective id * @return true iif id matches the id passed during construction */ public boolean isFullUpdateTopLevelControl(String effectiveId) { return effectiveId.equals(topLevelControlEffectiveId); } /** * Return location data associated with the current SAX event. * * @return LocationData, null if no Locator was found */ public LocationData getLocationData() { return LocationData.createIfPresent(getController().getLocator()); } public String getIdPrefix() { return (componentContextStack == null || componentContextStack.size() == 0) ? "" : componentContextStack.peek(); } public void pushComponentContext(String prefixedId) { final String newIdPrefix = prefixedId + XFormsConstants.COMPONENT_SEPARATOR; if (componentContextStack == null) componentContextStack = new Stack<String>(); componentContextStack.push(newIdPrefix); } public void popComponentContext() { componentContextStack.pop(); } public void pushCaseContext(boolean visible) { if (caseContextStack == null) caseContextStack = new Stack<Boolean>(); final boolean currentVisible = caseContextStack.size() == 0 ? true : caseContextStack.peek(); caseContextStack.push(currentVisible && visible); } public void popCaseContext() { caseContextStack.pop(); } public boolean getCaseVisibility() { if (caseContextStack == null || caseContextStack.size() == 0) return true; else return caseContextStack.peek(); } public String getIdPostfix() { if (repeatContextStack == null || repeatContextStack.size() == 0) return ""; else return (repeatContextStack.peek()).getIdPostfix(); } public boolean isTemplate() { if (repeatContextStack == null || repeatContextStack.size() == 0) return false; else return (repeatContextStack.peek()).isGenerateTemplate(); } public boolean isRepeatSelected() { if (repeatContextStack == null || repeatContextStack.size() == 0) return false; else return (repeatContextStack.peek()).isRepeatSelected(); } public int getCurrentIteration() { if (repeatContextStack == null || repeatContextStack.size() == 0) return 0; else return (repeatContextStack.peek()).getIteration(); } public int countParentRepeats() { return (repeatContextStack == null) ? 0 : repeatContextStack.size(); } public void pushRepeatContext(boolean generateTemplate, int iteration, boolean repeatSelected) { final String currentIdPostfix = getIdPostfix(); final String newIdPostfix; if (generateTemplate) { // No postfix is added for templates newIdPostfix = ""; } else { // Create postfix depending on whether we are appending to an existing postfix or not newIdPostfix = (currentIdPostfix.length() == 0) ? "" + XFormsConstants.REPEAT_SEPARATOR + iteration : currentIdPostfix + XFormsConstants.REPEAT_INDEX_SEPARATOR + iteration; } if (repeatContextStack == null) repeatContextStack = new Stack<RepeatContext>(); repeatContextStack.push(new RepeatContext(generateTemplate, iteration, newIdPostfix, repeatSelected)); } public void popRepeatContext() { repeatContextStack.pop(); } private static class RepeatContext { private boolean generateTemplate; private int iteration; private String idPostfix; private boolean repeatSelected; public RepeatContext(boolean generateTemplate, int iteration, String idPostfix, boolean repeatSelected) { this.generateTemplate = generateTemplate; this.iteration = iteration; this.idPostfix = idPostfix; this.repeatSelected = repeatSelected; } public boolean isGenerateTemplate() { return generateTemplate; } public int getIteration() { return iteration; } public String getIdPostfix() { return idPostfix; } public boolean isRepeatSelected() { return repeatSelected; } } /** * Restore the handler state up to (but excluding) the given control. * * Used if the context is not used from the root of the control tree. * * This restores repeats and components state. * * @param control control */ public void restoreContext(XFormsControl control) { // Get ancestor controls final List<XFormsControl> controls = new ArrayList<XFormsControl>(); { XFormsControl currentControl = control.parent(); while (currentControl != null) { controls.add(currentControl); currentControl = currentControl.parent(); } Collections.reverse(controls); } // Iterate from root to leaf for (final XFormsControl currentControl: controls) { if (currentControl instanceof XFormsRepeatIterationControl) { // Repeat final XFormsRepeatIterationControl repeatIterationControl = (XFormsRepeatIterationControl) currentControl; final XFormsRepeatControl repeatControl = repeatIterationControl.repeat(); final boolean isTopLevelRepeat = countParentRepeats() == 0; final boolean isRepeatSelected = isRepeatSelected() || isTopLevelRepeat; final int currentRepeatIteration = repeatIterationControl.iterationIndex(); pushRepeatContext(false, currentRepeatIteration, isRepeatSelected || currentRepeatIteration == repeatControl.getIndex()); } else if (currentControl instanceof XFormsComponentControl) { // Component pushComponentContext(currentControl.getPrefixedId()); } else if (currentControl instanceof XXFormsDynamicControl) { // Dynamic pushComponentContext(currentControl.getPrefixedId()); pushPartAnalysis(((XXFormsDynamicControl) currentControl).nested().get().partAnalysis()); } else if (currentControl instanceof XFormsCaseControl) { // Case (not used as of 2012-04-16) pushCaseContext(((XFormsCaseControl) currentControl).isVisible()); } } } }