/**
 * Simple implementation of the ExternalContext and related interfaces. When embedding Orbeon Forms
 * (e.g. in Eclipse), this class can be used directly or can be subclassed.
 */
public class SimpleExternalContext implements ExternalContext {

  private static final Logger logger = LoggerFactory.createLogger(SimpleExternalContext.class);

  protected class Request implements ExternalContext.Request {

    protected Map<String, Object> attributesMap = new HashMap<String, Object>();

    public String getContainerType() {
      return "simple";
    }

    public String getContainerNamespace() {
      return "";
    }

    public String getContextPath() {
      return "";
    }

    public String getPathInfo() {
      return "";
    }

    public String getRemoteAddr() {
      return "127.0.0.1";
    }

    public Map<String, Object> getAttributesMap() {
      return attributesMap;
    }

    public Map<String, String> getHeaderMap() {
      return Collections.emptyMap();
    }

    public Map<String, String[]> getHeaderValuesMap() {
      return Collections.emptyMap();
    }

    public Map<String, Object[]> getParameterMap() {
      return Collections.emptyMap();
    }

    public String getAuthType() {
      return "basic";
    }

    public String getRemoteUser() {
      return null;
    }

    public boolean isSecure() {
      return false;
    }

    public boolean isUserInRole(String role) {
      return false;
    }

    public void sessionInvalidate() {}

    public String getCharacterEncoding() {
      return "utf-8";
    }

    public int getContentLength() {
      return 0;
    }

    public String getContentType() {
      return "";
    }

    public String getServerName() {
      return "";
    }

    public int getServerPort() {
      return 0;
    }

    public String getMethod() {
      return "GET";
    }

    public String getProtocol() {
      return "http";
    }

    public String getRemoteHost() {
      return "";
    }

    public String getScheme() {
      return "";
    }

    public String getPathTranslated() {
      return "";
    }

    public String getQueryString() {
      return "";
    }

    public String getRequestedSessionId() {
      return "";
    }

    public String getRequestPath() {
      return "";
    }

    public String getRequestURI() {
      return "";
    }

    public String getRequestURL() {
      return "";
    }

    public String getServletPath() {
      return "";
    }

    public String getClientContextPath(String urlString) {
      return getContextPath();
    }

    public Reader getReader() throws IOException {
      return null;
    }

    public InputStream getInputStream() throws IOException {
      return null;
    }

    public Locale getLocale() {
      return null;
    }

    public Enumeration getLocales() {
      return null;
    }

    public boolean isRequestedSessionIdValid() {
      return false;
    }

    public Principal getUserPrincipal() {
      return null;
    }

    public Object getNativeRequest() {
      return SimpleExternalContext.this.getNativeRequest();
    }

    public String getPortletMode() {
      return null;
    }

    public String getWindowState() {
      return null;
    }
  }

  protected class Response implements ExternalContext.Response {
    protected ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    protected StringWriter writer = new StringWriter();
    protected String contentType;
    protected int status;
    protected Map<String, String> headers = new HashMap<String, String>();

    public OutputStream getOutputStream() throws IOException {
      return outputStream;
    }

    public PrintWriter getWriter() throws IOException {
      return new PrintWriter(writer);
    }

    public boolean isCommitted() {
      return false;
    }

    public void reset() {
      outputStream.reset();
      writer.getBuffer().delete(0, writer.getBuffer().length());
    }

    public void setContentType(String contentType) {
      this.contentType = contentType;
    }

    public void setStatus(int status) {
      this.status = status;
    }

    public void setHeader(String name, String value) {
      headers.put(name, value);
    }

    public void addHeader(String name, String value) {
      headers.put(name, value);
    }

    public void sendRedirect(
        String pathInfo,
        Map parameters,
        boolean isServerSide,
        boolean isExitPortal,
        boolean isNoRewrite)
        throws IOException {}

    public void setContentLength(int len) {}

    public void sendError(int code) throws IOException {}

    public String getCharacterEncoding() {
      return "utf-8";
    }

    public void setCaching(long lastModified, boolean revalidate, boolean allowOverride) {}

    public void setResourceCaching(long lastModified, long expires) {}

    public boolean checkIfModifiedSince(long lastModified, boolean allowOverride) {
      return true;
    }

    public String rewriteActionURL(String urlString) {
      return "";
    }

    public String rewriteRenderURL(String urlString) {
      return "";
    }

    public String rewriteActionURL(String urlString, String portletMode, String windowState) {
      return "";
    }

    public String rewriteRenderURL(String urlString, String portletMode, String windowState) {
      return "";
    }

    public String rewriteResourceURL(String urlString) {
      return "";
    }

    public String rewriteResourceURL(String urlString, boolean generateAbsoluteURL) {
      return "";
    }

    public String rewriteResourceURL(String urlString, int rewriteMode) {
      return "";
    }

    public String getNamespacePrefix() {
      return "";
    }

    public void setTitle(String title) {}

    public Object getNativeResponse() {
      return SimpleExternalContext.this.getNativeResponse();
    }
  }

  protected class Session implements ExternalContext.Session {

    protected Map<String, Object> sessionAttributesMap = new HashMap<String, Object>();

    public long getCreationTime() {
      return 0;
    }

    public String getId() {
      return Integer.toString(sessionAttributesMap.hashCode());
    }

    public long getLastAccessedTime() {
      return 0;
    }

    public int getMaxInactiveInterval() {
      return 0;
    }

    public void invalidate() {
      sessionAttributesMap = new HashMap<String, Object>();
    }

    public boolean isNew() {
      return false;
    }

    public void setMaxInactiveInterval(int interval) {}

    public Map<String, Object> getAttributesMap() {
      return sessionAttributesMap;
    }

    public Map<String, Object> getAttributesMap(int scope) {
      if (scope != APPLICATION_SCOPE)
        throw new OXFException(
            "Invalid session scope scope: only the application scope is allowed in Eclipse");
      return getAttributesMap();
    }

    public void addListener(SessionListener sessionListener) {}

    public void removeListener(SessionListener sessionListener) {}
  }

  protected class Application implements ExternalContext.Application {

    public void addListener(ApplicationListener applicationListener) {}

    public void removeListener(ApplicationListener applicationListener) {}
  }

  protected Request request = new Request();
  protected Response response = new Response();
  protected Session session = new Session();
  protected Application application = new Application();

  public Object getNativeContext() {
    return null;
  }

  public Object getNativeRequest() {
    return null;
  }

  public Object getNativeResponse() {
    return null;
  }

  public Object getNativeSession(boolean create) {
    return null;
  }

  public ExternalContext.Request getRequest() {
    return request;
  }

  public ExternalContext.Response getResponse() {
    return response;
  }

  public ExternalContext.Session getSession(boolean create) {
    return session;
  }

  public ExternalContext.Application getApplication() {
    return application;
  }

  public Map<String, Object> getAttributesMap() {
    return Collections.emptyMap();
  }

  public Map<String, String> getInitAttributesMap() {
    return Collections.emptyMap();
  }

  public String getRealPath(String path) {
    return null;
  }

  public String getStartLoggerString() {
    return "Running processor";
  }

  public String getEndLoggerString() {
    return "Done running processor";
  }

  public void log(String message, Throwable throwable) {
    logger.error(message, throwable);
  }

  public void log(String msg) {
    logger.info(msg);
  }

  public ExternalContext.RequestDispatcher getNamedDispatcher(String name) {
    return null;
  }

  public ExternalContext.RequestDispatcher getRequestDispatcher(
      String path, boolean isContextRelative) {
    return null;
  }

  public String rewriteServiceURL(String urlString, boolean forceAbsolute) {
    return URLRewriterUtils.rewriteServiceURL(getRequest(), urlString, forceAbsolute);
  }
}
/**
 * This processor handles XForms initialization and produces an XHTML document which is a
 * translation from the source XForms + XHTML.
 */
public class XFormsToXHTML extends ProcessorImpl {

  public static final String LOGGING_CATEGORY = "html";
  private static final Logger logger = LoggerFactory.createLogger(XFormsToXHTML.class);

  private static final String INPUT_ANNOTATED_DOCUMENT = "annotated-document";
  private static final String OUTPUT_DOCUMENT = "document";

  public XFormsToXHTML() {
    addInputInfo(new ProcessorInputOutputInfo(INPUT_ANNOTATED_DOCUMENT));
    addInputInfo(
        new ProcessorInputOutputInfo(
            "namespace")); // This input ensures that we depend on a portlet namespace
    addOutputInfo(new ProcessorInputOutputInfo(OUTPUT_DOCUMENT));
  }

  /** Case where an XML response must be generated. */
  @Override
  public ProcessorOutput createOutput(final String outputName) {
    final ProcessorOutput output =
        new URIProcessorOutputImpl(XFormsToXHTML.this, outputName, INPUT_ANNOTATED_DOCUMENT) {
          public void readImpl(final PipelineContext pipelineContext, XMLReceiver xmlReceiver) {
            doIt(pipelineContext, xmlReceiver, this, outputName);
          }

          @Override
          protected boolean supportsLocalKeyValidity() {
            return true;
          }

          @Override
          public KeyValidity getLocalKeyValidity(
              PipelineContext pipelineContext, URIReferences uriReferences) {
            // NOTE: As of 2010-03, caching of the output should never happen
            // o more work is needed to make this work properly
            // o not many use cases benefit
            return null;
          }
        };
    addOutput(outputName, output);
    return output;
  }

  @Override
  public ProcessorInput createInput(final String inputName) {
    if (inputName.equals(INPUT_ANNOTATED_DOCUMENT)) {
      // Insert processor on the fly to handle dependencies. This is a bit tricky: we used to have
      // an
      // XSLT/XInclude before XFormsToXHTML. This step handled XBL dependencies. Now that it is
      // removed, we
      // need a mechanism to detect dependencies. So we insert a step here.

      // Return an input which handles dependencies
      // The system actually has two processors:
      // o stage1 is the processor automatically inserted below for the purpose of handling
      // dependencies
      // o stage2 is the actual oxf:xforms-to-xhtml which actually does XForms processing
      final ProcessorInput originalInput = super.createInput(inputName);
      return new DependenciesProcessorInput(XFormsToXHTML.this, inputName, originalInput) {
        @Override
        protected URIProcessorOutputImpl.URIReferences getURIReferences(
            PipelineContext pipelineContext) {
          // Return dependencies object, set by stage2 before reading its input
          return ((Stage2TransientState) XFormsToXHTML.this.getState(pipelineContext))
              .stage1CacheableState;
        }
      };
    } else {
      return super.createInput(inputName);
    }
  }

  @Override
  public void reset(PipelineContext context) {
    setState(context, new Stage2TransientState());
  }

  // State passed by the second stage to the first stage.
  // NOTE: This extends URIReferencesState because we use URIProcessorOutputImpl.
  // It is not clear that we absolutely need URIProcessorOutputImpl in the second stage, but right
  // now we keep it,
  // because XFormsURIResolver requires URIProcessorOutputImpl.
  private class Stage2TransientState extends URIProcessorOutputImpl.URIReferencesState {
    public Stage1CacheableState stage1CacheableState;
  }

  private static final boolean DO_TEST_STATE = false;
  private static Stage2CacheableState TEST_STATE;

  private void doIt(
      final PipelineContext pipelineContext,
      XMLReceiver xmlReceiver,
      final URIProcessorOutputImpl processorOutput,
      String outputName) {

    final ExternalContext externalContext = XFormsUtils.getExternalContext(pipelineContext);
    final IndentedLogger indentedLogger =
        XFormsContainingDocument.getIndentedLogger(
            XFormsToXHTML.logger, XFormsServer.getLogger(), LOGGING_CATEGORY);

    // ContainingDocument and XFormsState created below
    final XFormsContainingDocument[] containingDocument = new XFormsContainingDocument[1];
    final boolean[] cachedStatus = new boolean[] {false};

    final Stage2CacheableState stage2CacheableState;
    if (TEST_STATE == null) {

      // Read and try to cache the complete XForms+XHTML document with annotations
      stage2CacheableState =
          (Stage2CacheableState)
              readCacheInputAsObject(
                  pipelineContext,
                  getInputByName(INPUT_ANNOTATED_DOCUMENT),
                  new CacheableInputReader() {
                    public Object read(
                        PipelineContext pipelineContext, ProcessorInput processorInput) {

                      // Compute annotated XForms document + static state document
                      final Stage1CacheableState stage1CacheableState = new Stage1CacheableState();
                      final Stage2CacheableState stage2CacheableState;
                      final XFormsStaticState[] staticState = new XFormsStaticState[1];
                      {
                        // Store dependencies container in state before reading
                        ((Stage2TransientState) XFormsToXHTML.this.getState(pipelineContext))
                                .stage1CacheableState =
                            stage1CacheableState;

                        // Read static state from input
                        stage2CacheableState =
                            readStaticState(
                                pipelineContext, externalContext, indentedLogger, staticState);
                      }

                      // Create containing document and initialize XForms engine
                      // NOTE: Create document here so we can do appropriate analysis of caching
                      // dependencies
                      final XFormsURIResolver uriResolver =
                          new XFormsURIResolver(
                              XFormsToXHTML.this,
                              processorOutput,
                              pipelineContext,
                              INPUT_ANNOTATED_DOCUMENT,
                              URLGenerator.DEFAULT_HANDLE_XINCLUDE);
                      containingDocument[0] =
                          new XFormsContainingDocument(
                              pipelineContext,
                              staticState[0],
                              stage2CacheableState.getAnnotatedTemplate(),
                              uriResolver);

                      // Gather set caching dependencies
                      gatherInputDependencies(
                          pipelineContext,
                          containingDocument[0],
                          indentedLogger,
                          stage1CacheableState);

                      return stage2CacheableState;
                    }

                    @Override
                    public void foundInCache() {
                      cachedStatus[0] = true;
                    }
                  },
                  false);

      TEST_STATE = DO_TEST_STATE ? stage2CacheableState : null;

    } else {
      stage2CacheableState = TEST_STATE;
    }

    try {
      // Create containing document if not done yet
      if (containingDocument[0] == null) {
        assert cachedStatus[0];
        // In this case, we found the static state digest and more in the cache, but we must now
        // create a new XFormsContainingDocument from this information
        indentedLogger.logDebug(
            "",
            "annotated document and static state digest obtained from cache",
            "digest",
            stage2CacheableState.getStaticStateDigest());

        final XFormsStaticState staticState;
        {
          final XFormsStaticState cachedState =
              XFormsStaticStateCache.instance()
                  .getDocument(pipelineContext, stage2CacheableState.getStaticStateDigest());
          if (cachedState != null && cachedState.getMetadata().checkBindingsIncludes()) {
            // Found static state in cache
            indentedLogger.logDebug("", "found up-to-date static state by digest in cache");

            staticState = cachedState;
          } else {
            // Not found static state in cache OR it is out of date, create static state from input
            // NOTE: In out of date case, could clone static state and reprocess instead?
            if (cachedState != null)
              indentedLogger.logDebug("", "found out-of-date static state by digest in cache");
            else indentedLogger.logDebug("", "did not find static state by digest in cache");

            final StaticStateBits staticStateBits =
                new StaticStateBits(
                    pipelineContext,
                    externalContext,
                    indentedLogger,
                    stage2CacheableState.getStaticStateDigest());
            staticState =
                new XFormsStaticState(
                    pipelineContext,
                    staticStateBits.staticStateDocument,
                    stage2CacheableState.getStaticStateDigest(),
                    staticStateBits.metadata);

            // Store in cache
            XFormsStaticStateCache.instance().storeDocument(pipelineContext, staticState);
          }
        }

        final XFormsURIResolver uriResolver =
            new XFormsURIResolver(
                XFormsToXHTML.this,
                processorOutput,
                pipelineContext,
                INPUT_ANNOTATED_DOCUMENT,
                URLGenerator.DEFAULT_HANDLE_XINCLUDE);
        containingDocument[0] =
            new XFormsContainingDocument(
                pipelineContext,
                staticState,
                stage2CacheableState.getAnnotatedTemplate(),
                uriResolver);
      } else {
        assert !cachedStatus[0];
        indentedLogger.logDebug(
            "", "annotated document and static state digest not obtained from cache.");
      }

      // Output resulting document
      if (outputName.equals("document")) {
        // Normal case where we output XHTML
        outputResponseDocument(
            pipelineContext,
            externalContext,
            indentedLogger,
            stage2CacheableState.getAnnotatedTemplate(),
            containingDocument[0],
            xmlReceiver);
      } else {
        // Output in test mode
        testOutputResponseState(
            pipelineContext, containingDocument[0], indentedLogger, xmlReceiver);
      }

      // Notify state manager
      XFormsStateManager.instance().afterInitialResponse(pipelineContext, containingDocument[0]);

    } catch (Throwable e) {
      indentedLogger.logDebug("", "throwable caught during initialization.");
      throw new OXFException(e);
    }
  }

  private Stage2CacheableState readStaticState(
      PipelineContext pipelineContext,
      ExternalContext externalContext,
      IndentedLogger indentedLogger,
      XFormsStaticState[] staticState) {

    final StaticStateBits staticStateBits =
        new StaticStateBits(pipelineContext, externalContext, indentedLogger, null);

    {
      final XFormsStaticState cachedState =
          XFormsStaticStateCache.instance()
              .getDocument(pipelineContext, staticStateBits.staticStateDigest);
      if (cachedState != null && cachedState.getMetadata().checkBindingsIncludes()) {
        // Found static state in cache
        indentedLogger.logDebug("", "found up-to-date static state by digest in cache");

        staticState[0] = cachedState;
      } else {
        // Not found static state in cache OR it is out of date, create and initialize static state
        // object
        // NOTE: In out of date case, could clone static state and reprocess instead?
        if (cachedState != null)
          indentedLogger.logDebug("", "found out-of-date static state by digest in cache");
        else indentedLogger.logDebug("", "did not find static state by digest in cache");

        staticState[0] =
            new XFormsStaticState(
                pipelineContext,
                staticStateBits.staticStateDocument,
                staticStateBits.staticStateDigest,
                staticStateBits.metadata);

        // Store in cache
        XFormsStaticStateCache.instance().storeDocument(pipelineContext, staticState[0]);
      }
    }

    // Update input dependencies object
    return new Stage2CacheableState(
        staticStateBits.annotatedTemplate, staticStateBits.staticStateDigest);
  }

  private class StaticStateBits {

    private final boolean isLogStaticStateInput =
        XFormsProperties.getDebugLogging().contains("html-static-state");

    public final XFormsStaticState.Metadata metadata = new XFormsStaticState.Metadata();
    public final SAXStore annotatedTemplate = new SAXStore();

    public final Document staticStateDocument;
    public final String staticStateDigest;

    public StaticStateBits(
        PipelineContext pipelineContext,
        ExternalContext externalContext,
        IndentedLogger indentedLogger,
        String existingStaticStateDigest) {

      final boolean computeDigest = isLogStaticStateInput || existingStaticStateDigest == null;

      indentedLogger.startHandleOperation(
          "", "reading input", "existing digest", existingStaticStateDigest);

      final TransformerXMLReceiver documentReceiver =
          TransformerUtils.getIdentityTransformerHandler();
      final LocationDocumentResult documentResult = new LocationDocumentResult();
      documentReceiver.setResult(documentResult);

      final XMLUtils.DigestContentHandler digestReceiver =
          computeDigest ? new XMLUtils.DigestContentHandler("MD5") : null;
      final XMLReceiver extractorOutput;
      if (isLogStaticStateInput) {
        extractorOutput =
            computeDigest
                ? new TeeXMLReceiver(
                    documentReceiver, digestReceiver, getDebugReceiver(indentedLogger))
                : new TeeXMLReceiver(documentReceiver, getDebugReceiver(indentedLogger));
      } else {
        extractorOutput =
            computeDigest ? new TeeXMLReceiver(documentReceiver, digestReceiver) : documentReceiver;
      }

      // Read the input through the annotator and gather namespace mappings
      //
      // Output of annotator is:
      //
      // o annotated page template (TODO: this should not include model elements)
      // o extractor
      //
      // Output of extractor is:
      //
      // o static state document
      // o optionally: digest
      // o optionally: debug output
      //
      readInputAsSAX(
          pipelineContext,
          INPUT_ANNOTATED_DOCUMENT,
          new XFormsAnnotatorContentHandler(
              annotatedTemplate,
              new XFormsExtractorContentHandler(extractorOutput, metadata),
              metadata));

      this.staticStateDocument = documentResult.getDocument();
      this.staticStateDigest =
          computeDigest ? NumberUtils.toHexString(digestReceiver.getResult()) : null;

      assert !isLogStaticStateInput
          || existingStaticStateDigest == null
          || this.staticStateDigest.equals(existingStaticStateDigest);

      indentedLogger.endHandleOperation("computed digest", this.staticStateDigest);
    }

    private XMLReceiver getDebugReceiver(final IndentedLogger indentedLogger) {
      final TransformerXMLReceiver identity = TransformerUtils.getIdentityTransformerHandler();
      final StringBuilderWriter writer = new StringBuilderWriter();
      identity.setResult(new StreamResult(writer));

      return new ForwardingXMLReceiver(identity) {
        @Override
        public void endDocument() throws SAXException {
          super.endDocument();
          // Log out at end of document
          indentedLogger.logDebug("", "static state input", "input", writer.toString());
        }
      };
    }
  }

  // What can be cached by the first stage: URI dependencies
  private static class Stage1CacheableState extends URIProcessorOutputImpl.URIReferences {}

  // What can be cached by the second stage: SAXStore and static state
  private static class Stage2CacheableState extends URIProcessorOutputImpl.URIReferences {

    private final SAXStore annotatedTemplate;
    private final String staticStateDigest;

    public Stage2CacheableState(SAXStore annotatedTemplate, String staticStateDigest) {
      this.annotatedTemplate = annotatedTemplate;
      this.staticStateDigest = staticStateDigest;
    }

    public SAXStore getAnnotatedTemplate() {
      return annotatedTemplate;
    }

    public String getStaticStateDigest() {
      return staticStateDigest;
    }
  }

  private void gatherInputDependencies(
      PipelineContext pipelineContext,
      XFormsContainingDocument containingDocument,
      IndentedLogger indentedLogger,
      Stage1CacheableState stage1CacheableState) {

    final String forwardSubmissionHeaders =
        XFormsProperties.getForwardSubmissionHeaders(containingDocument);

    // Add static instance source dependencies for top-level models
    // TODO: check all models/instances
    final XFormsStaticState staticState = containingDocument.getStaticState();
    for (final Model model :
        staticState.getModelsForScope(staticState.getXBLBindings().getTopLevelScope())) {
      for (final Instance instance : model.instancesMap().values()) {
        if (instance.dependencyURL() != null) {

          final String resolvedDependencyURL =
              XFormsUtils.resolveServiceURL(
                  pipelineContext,
                  containingDocument,
                  instance.element(),
                  instance.dependencyURL(),
                  ExternalContext.Response.REWRITE_MODE_ABSOLUTE);

          if (!instance.isCacheHint()) {
            stage1CacheableState.addReference(
                null,
                resolvedDependencyURL,
                instance.xxformsUsername(),
                instance.xxformsPassword(),
                instance.xxformsPassword(),
                forwardSubmissionHeaders);

            if (indentedLogger.isDebugEnabled())
              indentedLogger.logDebug(
                  "",
                  "adding document cache dependency for non-cacheable instance",
                  "instance URI",
                  resolvedDependencyURL);

          } else {
            // Don't add the dependency as we don't want the instance URI to be hit
            // For all practical purposes, globally shared instances must remain constant!
            if (indentedLogger.isDebugEnabled())
              indentedLogger.logDebug(
                  "",
                  "not adding document cache dependency for cacheable instance",
                  "instance URI",
                  resolvedDependencyURL);
          }
        }
      }
    }

    // Set caching dependencies if the input was actually read
    // TODO: check all models/instances
    // Q: should use static dependency information instead? what about schema imports and instance
    // replacements?
    for (final XFormsModel currentModel : containingDocument.getModels()) {
      // Add schema dependencies
      final String[] schemaURIs = currentModel.getSchemaURIs();
      // TODO: We should also use dependencies computed in XFormsModelSchemaValidator.SchemaInfo
      if (schemaURIs != null) {
        for (final String currentSchemaURI : schemaURIs) {
          if (indentedLogger.isDebugEnabled())
            indentedLogger.logDebug(
                "", "adding document cache dependency for schema", "schema URI", currentSchemaURI);

          stage1CacheableState.addReference(
              null,
              currentSchemaURI,
              null,
              null,
              null,
              forwardSubmissionHeaders); // TODO: support username / password on schema refs
        }
      }
    }
    // TODO: Add @src attributes from controls? Not used often.

    // Set caching dependencies for XBL inclusions
    {
      final XFormsStaticState.Metadata metadata = containingDocument.getStaticState().getMetadata();
      final Set<String> includes = metadata.getBindingsIncludes();
      if (includes != null) {
        for (final String include : includes) {
          stage1CacheableState.addReference(null, "oxf:" + include, null, null, null, null);
        }
      }
    }
  }

  public static void outputResponseDocument(
      final PipelineContext pipelineContext,
      final ExternalContext externalContext,
      final IndentedLogger indentedLogger,
      final SAXStore annotatedDocument,
      final XFormsContainingDocument containingDocument,
      final XMLReceiver xmlReceiver)
      throws SAXException, IOException {

    final List<XFormsContainingDocument.Load> loads = containingDocument.getLoadsToRun();
    if (containingDocument.isGotSubmissionReplaceAll()) {
      // 1. Got a submission with replace="all"

      // NOP: Response already sent out by a submission
      // TODO: modify XFormsModelSubmission accordingly
      indentedLogger.logDebug("", "handling response for submission with replace=\"all\"");
    } else if (loads != null && loads.size() > 0) {
      // 2. Got at least one xforms:load

      // Send redirect out

      // Get first load only
      final XFormsContainingDocument.Load load = loads.get(0);

      // Send redirect
      final String redirectResource = load.getResource();
      indentedLogger.logDebug(
          "", "handling redirect response for xforms:load", "url", redirectResource);
      // Set isNoRewrite to true, because the resource is either a relative path or already contains
      // the servlet context
      externalContext.getResponse().sendRedirect(redirectResource, null, false, false, true);

      // Still send out a null document to signal that no further processing must take place
      XMLUtils.streamNullDocument(xmlReceiver);
    } else {
      // 3. Regular case: produce an XHTML document out

      final ElementHandlerController controller = new ElementHandlerController();

      // Register handlers on controller (the other handlers are registered by the body handler)
      {
        controller.registerHandler(
            XHTMLHeadHandler.class.getName(), XMLConstants.XHTML_NAMESPACE_URI, "head");
        controller.registerHandler(
            XHTMLBodyHandler.class.getName(), XMLConstants.XHTML_NAMESPACE_URI, "body");

        // Register a handler for AVTs on HTML elements
        final boolean hostLanguageAVTs =
            XFormsProperties
                .isHostLanguageAVTs(); // TODO: this should be obtained per document, but we only
        // know about this in the extractor
        if (hostLanguageAVTs) {
          controller.registerHandler(
              XXFormsAttributeHandler.class.getName(),
              XFormsConstants.XXFORMS_NAMESPACE_URI,
              "attribute");
          controller.registerHandler(
              XHTMLElementHandler.class.getName(), XMLConstants.XHTML_NAMESPACE_URI);
        }

        // Swallow XForms elements that are unknown
        controller.registerHandler(
            NullHandler.class.getName(), XFormsConstants.XFORMS_NAMESPACE_URI);
        controller.registerHandler(
            NullHandler.class.getName(), XFormsConstants.XXFORMS_NAMESPACE_URI);
        controller.registerHandler(NullHandler.class.getName(), XFormsConstants.XBL_NAMESPACE_URI);
      }

      // Set final output
      controller.setOutput(new DeferredXMLReceiverImpl(xmlReceiver));
      // Set handler context
      controller.setElementHandlerContext(
          new HandlerContext(
              controller, pipelineContext, containingDocument, externalContext, null));
      // Process the entire input
      annotatedDocument.replay(
          new ExceptionWrapperXMLReceiver(controller, "converting XHTML+XForms document to XHTML"));
    }

    containingDocument.afterInitialResponse();
  }

  private void testOutputResponseState(
      final PipelineContext pipelineContext,
      final XFormsContainingDocument containingDocument,
      final IndentedLogger indentedLogger,
      final XMLReceiver xmlReceiver)
      throws SAXException {
    // Output XML response

    XFormsServer.outputAjaxResponse(
        containingDocument, indentedLogger, null, pipelineContext, null, xmlReceiver, false, true);
  }
}
/** XHTML to PDF converter using the Flying Saucer library. */
public class XHTMLToPDFProcessor
    extends HttpBinarySerializer { // TODO: HttpBinarySerializer is supposedly deprecated

  private static final Logger logger = LoggerFactory.createLogger(XHTMLToPDFProcessor.class);

  public static String DEFAULT_CONTENT_TYPE = "application/pdf";

  public XHTMLToPDFProcessor() {
    addInputInfo(new ProcessorInputOutputInfo(INPUT_DATA));
  }

  protected String getDefaultContentType() {
    return DEFAULT_CONTENT_TYPE;
  }

  protected void readInput(
      final PipelineContext pipelineContext,
      final ProcessorInput input,
      Config config,
      OutputStream outputStream) {

    final ExternalContext externalContext = NetUtils.getExternalContext();

    // Read the input as a DOM
    final Document domDocument = readInputAsDOM(pipelineContext, input);

    // Create renderer and add our own callback

    final float DEFAULT_DOTS_PER_POINT = 20f * 4f / 3f;
    final int DEFAULT_DOTS_PER_PIXEL = 14;

    final ITextRenderer renderer =
        new ITextRenderer(DEFAULT_DOTS_PER_POINT, DEFAULT_DOTS_PER_PIXEL);

    // Embed fonts if needed, based on configuration properties
    embedFonts(renderer);

    try {
      final ITextUserAgent callback =
          new ITextUserAgent(renderer.getOutputDevice()) {
            // Called for:
            //
            // - CSS URLs
            // - image URLs
            // - link clicked / form submission (not relevant for our usage)
            // - resolveAndOpenStream below
            public String resolveURI(String uri) {
              // Our own resolver

              // All resources we care about here are resource URLs. The PDF pipeline makes sure
              // that the servlet
              // URL rewriter processes the XHTML output to rewrite resource URLs to absolute paths,
              // including
              // the servlet context and version number if needed. In addition, CSS resources must
              // either use
              // relative paths when pointing to other CSS files or images, or go through the XForms
              // CSS rewriter,
              // which also generates absolute paths.
              // So all we need to do here is rewrite the resulting path to an absolute URL.
              // NOTE: We used to call rewriteResourceURL() here as the PDF pipeline did not do URL
              // rewriting.
              // However this caused issues, for example resources like background images referred
              // by CSS files
              // could be rewritten twice: once by the XForms resource rewriter, and a second time
              // here.
              return URLRewriterUtils.rewriteServiceURL(
                  NetUtils.getExternalContext().getRequest(),
                  uri,
                  ExternalContext.Response.REWRITE_MODE_ABSOLUTE
                      | ExternalContext.Response.REWRITE_MODE_ABSOLUTE_PATH_NO_CONTEXT);
            }

            // Called by:
            //
            // - getCSSResource
            // - getImageResource below
            // - getBinaryResource (not sure when called)
            // - getXMLResource (not sure when called)
            protected InputStream resolveAndOpenStream(String uri) {

              final String resolvedURI = resolveURI(uri);
              // TODO: Use xforms:submission code instead

              // Tell callee we are loading that we are a servlet environment, as in effect we act
              // like
              // a browser retrieving resources directly, not like a portlet. This is the case also
              // if we are
              // called by the proxy portlet or if we are directly within a portlet.
              final Map<String, String[]> headers = new HashMap<String, String[]>();
              headers.put("Orbeon-Client", new String[] {"servlet"});

              final ConnectionResult connectionResult =
                  new Connection()
                      .open(
                          externalContext,
                          new IndentedLogger(logger, ""),
                          false,
                          Connection.Method.GET.name(),
                          URLFactory.createURL(resolvedURI),
                          null,
                          null,
                          null,
                          headers,
                          Connection.getForwardHeaders());

              if (connectionResult.statusCode != 200) {
                connectionResult.close();
                throw new OXFException(
                    "Got invalid return code while loading resource: "
                        + uri
                        + ", "
                        + connectionResult.statusCode);
              }

              pipelineContext.addContextListener(
                  new PipelineContext.ContextListener() {
                    public void contextDestroyed(boolean success) {
                      connectionResult.close();
                    }
                  });

              return connectionResult.getResponseInputStream();
            }

            public ImageResource getImageResource(String uri) {
              final InputStream is = resolveAndOpenStream(uri);
              final String localURI = NetUtils.inputStreamToAnyURI(is, NetUtils.REQUEST_SCOPE);
              return super.getImageResource(localURI);
            }
          };
      callback.setSharedContext(renderer.getSharedContext());
      renderer.getSharedContext().setUserAgentCallback(callback);
      //        renderer.getSharedContext().setDPI(150);

      // Set the document to process
      renderer.setDocument(
          domDocument,
          // No base URL if can't get request URL from context
          externalContext.getRequest() == null
              ? null
              : externalContext.getRequest().getRequestURL());

      // Do the layout and create the resulting PDF
      renderer.layout();
      final List pages = renderer.getRootBox().getLayer().getPages();
      try {
        // Page count might be zero, and if so createPDF
        if (pages != null && pages.size() > 0) {
          renderer.createPDF(outputStream);
        } else {
          // TODO: log?
        }
      } catch (Exception e) {
        throw new OXFException(e);
      } finally {
        try {
          outputStream.close();
        } catch (IOException e) {
          // NOP
          // TODO: log?
        }
      }
    } finally {
      // Free resources associated with the rendering context
      renderer.getSharedContext().reset();
    }
  }

  public static void embedFonts(ITextRenderer renderer) {
    final PropertySet propertySet = Properties.instance().getPropertySet();
    for (final String propertyName : propertySet.getPropertiesStartsWith("oxf.fr.pdf.font.path")) {
      final String path = StringUtils.trimToNull(propertySet.getString(propertyName));
      if (path != null) {
        try {
          // Overriding the font family is optional
          final String family;
          {
            final String[] tokens = StringUtils.split(propertyName, '.');
            if (tokens.length >= 6) {
              final String id = tokens[5];
              family =
                  StringUtils.trimToNull(
                      propertySet.getString("oxf.fr.pdf.font.family" + '.' + id));
            } else {
              family = null;
            }
          }

          // Add the font
          renderer
              .getFontResolver()
              .addFont(path, family, BaseFont.IDENTITY_H, BaseFont.EMBEDDED, null);
        } catch (Exception e) {
          logger.warn(
              "Failed to load font by path: '"
                  + path
                  + "' specified with property '"
                  + propertyName
                  + "'");
        }
      }
    }
  }
}
/** XForms events definitions. */
public class XFormsEvents {

  public static final String LOGGING_CATEGORY = "event";
  public static final Logger logger = LoggerFactory.createLogger(XFormsEvents.class);

  // Custom initialization events
  public static final String XXFORMS_ALL_EVENTS_REQUIRED = "xxforms-all-events-required";
  public static final String XXFORMS_READY = "xxforms-ready";

  // Other custom events
  public static final String XXFORMS_SESSION_HEARTBEAT = "xxforms-session-heartbeat";
  public static final String XXFORMS_SUBMIT = "xxforms-submit";
  public static final String XXFORMS_SUBMIT_REPLACE = "xxforms-submit-replace";
  public static final String XXFORMS_LOAD = "xxforms-load";
  public static final String XXFORMS_REPEAT_FOCUS = "xxforms-repeat-focus";
  public static final String XXFORMS_OFFLINE = "xxforms-offline";
  public static final String XXFORMS_ONLINE = "xxforms-online";

  public static final String XXFORMS_DIALOG_CLOSE = "xxforms-dialog-close";
  public static final String XXFORMS_DIALOG_OPEN = "xxforms-dialog-open";
  public static final String XXFORMS_INSTANCE_INVALIDATE = "xxforms-instance-invalidate";

  public static final String XXFORMS_DND = "xxforms-dnd";

  public static final String XXFORMS_VALID = "xxforms-valid";
  public static final String XXFORMS_INVALID = "xxforms-invalid";

  public static final String XXFORMS_VALUE_CHANGE_WITH_FOCUS_CHANGE =
      "xxforms-value-change-with-focus-change";
  public static final String XXFORMS_VALUE_OR_ACTIVATE = "xxforms-value-or-activate";

  public static final String XXFORMS_VALUE_CHANGED = "xxforms-value-changed";
  public static final String XXFORMS_NODESET_CHANGED = "xxforms-nodeset-changed";

  // Standard XForms events
  public static final String XFORMS_MODEL_CONSTRUCT = "xforms-model-construct";
  public static final String XFORMS_MODEL_CONSTRUCT_DONE = "xforms-model-construct-done";
  public static final String XFORMS_READY = "xforms-ready";
  public static final String XFORMS_MODEL_DESTRUCT = "xforms-model-destruct";
  public static final String XFORMS_REBUILD = "xforms-rebuild";
  public static final String XFORMS_RECALCULATE = "xforms-recalculate";
  public static final String XFORMS_REVALIDATE = "xforms-revalidate";
  public static final String XFORMS_REFRESH = "xforms-refresh";
  public static final String XFORMS_RESET = "xforms-reset";
  public static final String XFORMS_SUBMIT = "xforms-submit";
  public static final String XFORMS_SUBMIT_SERIALIZE = "xforms-submit-serialize";
  public static final String XFORMS_SUBMIT_DONE = "xforms-submit-done";

  public static final String XFORMS_VALUE_CHANGED = "xforms-value-changed";
  public static final String XFORMS_VALID = "xforms-valid";
  public static final String XFORMS_INVALID = "xforms-invalid";
  public static final String XFORMS_REQUIRED = "xforms-required";
  public static final String XFORMS_OPTIONAL = "xforms-optional";
  public static final String XFORMS_READWRITE = "xforms-readwrite";
  public static final String XFORMS_READONLY = "xforms-readonly";
  public static final String XFORMS_ENABLED = "xforms-enabled";
  public static final String XFORMS_DISABLED = "xforms-disabled";

  public static final String XFORMS_DESELECT = "xforms-deselect";
  public static final String XFORMS_SELECT = "xforms-select";

  public static final String XFORMS_INSERT = "xforms-insert";
  public static final String XFORMS_DELETE = "xforms-delete";

  public static final String XFORMS_FOCUS = "xforms-focus";

  public static final String XFORMS_SCROLL_FIRST = "xforms-scroll-first";
  public static final String XFORMS_SCROLL_LAST = "xforms-scroll-last";

  public static final String XFORMS_HELP = "xforms-help";
  public static final String XFORMS_HINT = "xforms-hint";

  // DOM events
  public static final String XFORMS_DOM_ACTIVATE = "DOMActivate";
  public static final String XFORMS_DOM_FOCUS_OUT = "DOMFocusOut";
  public static final String XFORMS_DOM_FOCUS_IN = "DOMFocusIn";

  // Exceptions and errors
  public static final String XFORMS_LINK_EXCEPTION = "xforms-link-exception";
  public static final String XFORMS_LINK_ERROR = "xforms-link-error";
  public static final String XFORMS_COMPUTE_EXCEPTION = "xforms-compute-exception";
  public static final String XFORMS_SUBMIT_ERROR = "xforms-submit-error";
  public static final String XFORMS_BINDING_EXCEPTION = "xforms-binding-exception";

  private XFormsEvents() {}
}