content: /*
   *  Licensed to the Apache Software Foundation (ASF) under one or more
   *  contributor license agreements.  See the NOTICE file distributed with
   *  this work for additional information regarding copyright ownership.
   *  The ASF licenses this file to You 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
   *
   *      https://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.ivy.osgi.util;
  
  import java.util.HashMap;
  import java.util.Locale;
  import java.util.Map;
  
  import org.apache.ivy.util.Message;
  import org.xml.sax.Attributes;
  import org.xml.sax.ContentHandler;
  import org.xml.sax.DTDHandler;
  import org.xml.sax.ErrorHandler;
  import org.xml.sax.Locator;
  import org.xml.sax.SAXException;
  import org.xml.sax.SAXParseException;
  import org.xml.sax.helpers.DefaultHandler;
  
  public class DelegatingHandler extends DefaultHandler implements DTDHandler, ContentHandler,
          ErrorHandler {
  
      private DelegatingHandler delegate = null;
  
      DelegatingHandler parent;
  
      private final Map<String, DelegatingHandler> saxHandlerMapping = new HashMap<>();
  
      private final Map<String, ChildElementHandler<?>> childHandlerMapping = new HashMap<>();
  
      private final String tagName;
  
      private boolean started = false;
  
      private boolean skip = false;
  
      private boolean skipOnError = false;
  
      private StringBuilder charBuffer = new StringBuilder();
  
      private boolean bufferingChar = false;
  
      private Locator locator;
  
      public DelegatingHandler(String name) {
          this.tagName = name;
          charBuffer.setLength(0);
      }
  
      protected <DH extends DelegatingHandler> void addChild(DH saxHandler,
              ChildElementHandler<DH> elementHandler) {
          saxHandlerMapping.put(saxHandler.getName(), saxHandler);
          childHandlerMapping.put(saxHandler.getName(), elementHandler);
          saxHandler.parent = this;
      }
  
      public String getName() {
          return tagName;
      }
  
      public DelegatingHandler getParent() {
          return parent;
      }
  
      public void setBufferingChar(boolean bufferingChar) {
          this.bufferingChar = bufferingChar;
      }
  
      public void setSkipOnError(boolean skipOnError) {
          this.skipOnError = skipOnError;
      }
  
      public boolean isBufferingChar() {
          return bufferingChar;
      }
  
      public String getBufferedChars() {
          return charBuffer.toString();
      }
  
      @Override
      public void setDocumentLocator(Locator locator) {
          this.locator = locator;
          for (DelegatingHandler subHandler : saxHandlerMapping.values()) {
              subHandler.setDocumentLocator(locator);
          }
      }
  
      public Locator getLocator() {
          return locator;
      }
  
      /**
       * Return an sort of identifier of the current element being parsed. It will only be used for
       * logging purpose.
       *
       * @return an empty string by default
       */
      protected String getCurrentElementIdentifier() {
          return "";
      }
  
      public void skip() {
          skip = true;
          for (DelegatingHandler subHandler : saxHandlerMapping.values()) {
              subHandler.stopDelegating();
          }
      }
  
      protected void stopDelegating() {
          parent.delegate = null;
          skip = false;
          started = false;
          for (DelegatingHandler subHandler : saxHandlerMapping.values()) {
              subHandler.stopDelegating();
          }
      }
  
      private interface SkipOnErrorCallback {
          void call() throws SAXException;
      }
  
      private void skipOnError(SkipOnErrorCallback callback) throws SAXException {
          try {
              callback.call();
          } catch (SAXException e) {
              if (skipOnError) {
                  skip();
                  log(Message.MSG_ERR, e.getMessage(), e);
              } else {
                  throw e;
              }
          }
      }
  
      @Override
      public final void startDocument() throws SAXException {
          if (skip) {
              return;
          }
          if (delegate != null) {
              delegate.startDocument();
          } else {
              doStartDocument();
          }
      }
  
      /**
       * By default do nothing.
       *
       * @throws SAXException API told me so
       */
      protected void doStartDocument() throws SAXException {
      }
  
      @Override
      public final void endDocument() throws SAXException {
          if (skip) {
              return;
          }
          if (delegate != null) {
              delegate.endDocument();
          } else {
              doEndDocument();
          }
      }
  
      /**
       * By default do nothing.
       *
       * @throws SAXException API told me so
       */
      protected void doEndDocument() throws SAXException {
      }
  
      @Override
      public final void startElement(final String uri, final String localName, final String n,
              final Attributes atts) throws SAXException {
          // reset the char buffer
          charBuffer.setLength(0);
          if (delegate != null) {
              // we are already delegating, let's continue
              skipOnError(new SkipOnErrorCallback() {
                  public void call() throws SAXException {
                      delegate.startElement(uri, localName, n, atts);
                  }
              });
          } else {
              if (!started) { // first time called ?
                  // just for the root, check the expected element name
                  // not need to check the delegated as the mapping is already taking care of it
                  if (parent == null && !localName.equals(tagName)) {
                      // we are at the root and the saxed element doesn't match
                      throw new SAXException("The root element of the parsed document '" + localName
                              + "' didn't matched the expected one: '" + tagName + "'");
                  }
                  skipOnError(new SkipOnErrorCallback() {
                      public void call() throws SAXException {
                          handleAttributes(atts);
                      }
                  });
                  started = true;
              } else {
                  if (skip) {
                      // we con't care anymore about that part of the xml tree
                      return;
                  }
                  // time now to delegate for a new element
                  delegate = saxHandlerMapping.get(localName);
                  if (delegate != null) {
                      skipOnError(new SkipOnErrorCallback() {
                          public void call() throws SAXException {
                              delegate.startElement(uri, localName, n, atts);
                          }
                      });
                  }
              }
          }
      }
  
      /**
       * Called when the expected node is achieved; nothing to do by default.
       *
       * @param atts
       *            the xml attributes attached to the expected node
       * @exception SAXException
       *                in case the parsing should be completely stopped
       */
      protected void handleAttributes(Attributes atts) throws SAXException {
      }
  
      /**
       * By default do nothing.
       *
       * @param uri String
       * @param localName String
       * @param name String
       * @param atts Attributes
       * @throws SAXException API told me so
       */
      protected void doStartElement(String uri, String localName, String name, Attributes atts)
              throws SAXException {
      }
  
      @Override
      public final void endElement(final String uri, final String localName, final String n)
              throws SAXException {
          if (delegate != null) {
              final DelegatingHandler savedDelegate = delegate;
              // we are already delegating, let's continue
              skipOnError(new SkipOnErrorCallback() {
                  public void call() throws SAXException {
                      delegate.endElement(uri, localName, n);
                  }
              });
              if (delegate == null) {
                  // we just stopped delegating, it means that the child has ended
                  final ChildElementHandler<?> childHandler = childHandlerMapping.get(localName);
                  if (childHandler != null) {
                      skipOnError(new SkipOnErrorCallback() {
                          public void call() throws SAXException {
                              childHandler._childHandled(savedDelegate);
                          }
                      });
                  }
              }
          } else {
              if (!skip) {
                  doEndElement(uri, localName, n);
              }
              if (parent != null && tagName.equals(localName)) {
                  // the current element is closed, let's tell the parent to stop delegating
                  stopDelegating();
              }
          }
      }
  
      /**
       * By default do nothing.
       *
       * @param uri String
       * @param localName String
       * @param name String
       * @throws SAXException API told me so
       */
      protected void doEndElement(String uri, String localName, String name) throws SAXException {
      }
  
      public abstract static class ChildElementHandler<DH extends DelegatingHandler> {
  
          /**
           * @param child DH
           * @throws SAXParseException on failure
           * @deprecated because of renaming due spell check.
           */
          @Deprecated
          public void childHanlded(DH child) throws SAXParseException {
              childHandled(child);
          }
  
          public abstract void childHandled(DH child) throws SAXParseException;
  
          // because we know what we're doing
          @SuppressWarnings("unchecked")
          private void _childHandled(DelegatingHandler delegate) throws SAXParseException {
              childHandled((DH) delegate);
          }
  
      }
  
      @Override
      public final void characters(char[] ch, int start, int length) throws SAXException {
          if (skip) {
              return;
          }
          if (delegate != null) {
              delegate.characters(ch, start, length);
          } else {
              doCharacters(ch, start, length);
          }
      }
  
      /**
       * @param ch char[]
       * @param start int
       * @param length int
       * @throws SAXException if something goes wrong
       */
      protected void doCharacters(char[] ch, int start, int length) throws SAXException {
          if (bufferingChar) {
              charBuffer.append(ch, start, length);
          }
      }
  
      @Override
      public final void startPrefixMapping(String prefix, String uri) throws SAXException {
          if (skip) {
              return;
          }
          if (delegate != null) {
              delegate.startPrefixMapping(prefix, uri);
          } else {
              doStartPrefixMapping(prefix, uri);
          }
      }
  
      /**
       * By default do nothing.
       *
       * @param prefix String
       * @param uri String
       * @throws SAXException API told me so
       */
      protected void doStartPrefixMapping(String prefix, String uri) throws SAXException {
      }
  
      @Override
      public final void endPrefixMapping(String prefix) throws SAXException {
          if (skip) {
              return;
          }
          if (delegate != null) {
              delegate.endPrefixMapping(prefix);
          } else {
              doEndPrefixMapping(prefix);
          }
      }
  
      /**
       * By default do nothing.
       *
       * @param prefix String
       * @throws SAXException API told me so
       */
      protected void doEndPrefixMapping(String prefix) throws SAXException {
      }
  
      @Override
      public final void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
          if (skip) {
              return;
          }
          if (delegate != null) {
              delegate.ignorableWhitespace(ch, start, length);
          } else {
              doIgnorableWhitespace(ch, start, length);
          }
      }
  
      /**
       * By default do nothing
       *
       * @param ch char[]
       * @param start int
       * @param length int
       * @throws SAXException API told me so
       */
      protected void doIgnorableWhitespace(char[] ch, int start, int length) throws SAXException {
      }
  
      @Override
      public final void notationDecl(String name, String publicId, String systemId)
              throws SAXException {
          if (skip) {
              return;
          }
          if (delegate != null) {
              delegate.notationDecl(name, publicId, systemId);
          } else {
              doNotationDecl(name, publicId, systemId);
          }
      }
  
      /**
       * By default do nothing.
       *
       * @param name String
       * @param publicId String
       * @param systemId String
       * @throws SAXException API told me so
       */
      protected void doNotationDecl(String name, String publicId, String systemId)
              throws SAXException {
      }
  
      @Override
      public final void processingInstruction(String target, String data) throws SAXException {
          if (skip) {
              return;
          }
          if (delegate != null) {
              delegate.processingInstruction(target, data);
          } else {
              doProcessingInstruction(target, data);
          }
      }
  
      /**
       * By default do nothing
       *
       * @param target String
       * @param data String
       * @throws SAXException API told me so
       */
      protected void doProcessingInstruction(String target, String data) throws SAXException {
      }
  
      @Override
      public final void skippedEntity(String name) throws SAXException {
          if (skip) {
              return;
          }
          if (delegate != null) {
              delegate.skippedEntity(name);
          } else {
              doSkippedEntity(name);
          }
      }
  
      /**
       * By default do nothing.
       *
       * @param name String
       * @throws SAXException API told me so
       */
      protected void doSkippedEntity(String name) throws SAXException {
      }
  
      @Override
      public final void unparsedEntityDecl(String name, String publicId, String systemId,
              String notationName) throws SAXException {
          if (skip) {
              return;
          }
          if (delegate != null) {
              delegate.unparsedEntityDecl(name, publicId, systemId, notationName);
          } else {
              doUnparsedEntityDecl(name, publicId, systemId, notationName);
          }
      }
  
      /**
       * By default do nothing.
       *
       * @param name String
       * @param publicId String
       * @param systemId String
       * @param notationName String
       * @throws SAXException API told me so
       */
      protected void doUnparsedEntityDecl(String name, String publicId, String systemId,
              String notationName) throws SAXException {
      }
  
      // ERROR HANDLING
  
      @Override
      public final void warning(SAXParseException exception) throws SAXException {
          if (skip) {
              return;
          }
          if (delegate != null) {
              delegate.warning(exception);
          } else {
              doWarning(exception);
          }
      }
  
      /**
       * By default do nothing.
       *
       * @param exception SAXParseException
       * @throws SAXException API told me so
       */
      protected void doWarning(SAXParseException exception) throws SAXException {
      }
  
      @Override
      public final void error(SAXParseException exception) throws SAXException {
          if (skip) {
              return;
          }
          if (delegate != null) {
              delegate.error(exception);
          } else {
              doError(exception);
          }
      }
  
      /**
       * By default do nothing.
       *
       * @param exception SAXParseException
       * @throws SAXException API told me so
       */
      protected void doError(SAXParseException exception) throws SAXException {
      }
  
      @Override
      public final void fatalError(SAXParseException exception) throws SAXException {
          if (skip) {
              return;
          }
          if (delegate != null) {
              delegate.fatalError(exception);
          } else {
              doFatalError(exception);
          }
      }
  
      /**
       * By default do nothing.
       *
       * @param exception SAXParseException
       * @throws SAXException API told me so
       */
      protected void doFatalError(SAXParseException exception) throws SAXException {
      }
  
      // //////////////////////
      // Functions related to error handling
      // //////////////////////
  
      protected void log(int logLevel, String message, Throwable t) {
          Message.debug(t);
          log(logLevel, message);
      }
  
      protected void log(int logLevel, String message) {
          Message.log(logLevel, getLocation(getLocator()) + message);
      }
  
      protected static String getLocation(Locator locator) {
          if (locator == null) {
              return "";
          }
          return "[line " + locator.getLineNumber() + " col. " + locator.getColumnNumber() + "] ";
      }
  
      @SuppressWarnings("unused")
      private void skipOnError(DelegatingHandler currentHandler,
              Class<? extends DelegatingHandler> handlerClassToSkip, String message) {
          DelegatingHandler handlerToSkip = currentHandler;
          while (!(handlerClassToSkip.isAssignableFrom(handlerToSkip.getClass()))) {
              handlerToSkip = handlerToSkip.getParent();
          }
          log(Message.MSG_ERR, message + ". The '" + handlerToSkip.getName() + "' element "
                  + getCurrentElementIdentifier() + " is then ignored.");
          handlerToSkip.skip();
      }
  
      // //////////////////////
      // Helpers to parse the attributes
      // //////////////////////
  
      protected String getRequiredAttribute(Attributes atts, String name) throws SAXParseException {
          String value = atts.getValue(name);
          if (value == null) {
              throw new SAXParseException("Required attribute '" + name + "' not found", getLocator());
          }
          return value;
      }
  
      protected String getOptionalAttribute(Attributes atts, String name, String defaultValue) {
          String value = atts.getValue(name);
          if (value == null) {
              return defaultValue;
          }
          return value;
      }
  
      protected int getRequiredIntAttribute(Attributes atts, String name, Integer logLevel)
              throws SAXParseException {
          return parseInt(name, getRequiredAttribute(atts, name));
      }
  
      protected Integer getOptionalIntAttribute(Attributes atts, String name, Integer defaultValue)
              throws SAXParseException {
          String value = atts.getValue(name);
          if (value == null) {
              return defaultValue;
          }
          return parseInt(name, value);
      }
  
      private int parseInt(String name, String value) throws SAXParseException {
          try {
              return Integer.parseInt(value);
          } catch (NumberFormatException e) {
              throw new SAXParseException("Attribute '" + name
                      + "' is expected to be an integer but was '" + value + "' (" + e.getMessage()
                      + ")", getLocator());
          }
      }
  
      protected long getRequiredLongAttribute(Attributes atts, String name) throws SAXParseException {
          return parseLong(name, getRequiredAttribute(atts, name));
      }
  
      protected Long getOptionalLongAttribute(Attributes atts, String name, Long defaultValue)
              throws SAXParseException {
          String value = atts.getValue(name);
          if (value == null) {
              return defaultValue;
          }
          return parseLong(name, value);
      }
  
      private long parseLong(String name, String value) throws SAXParseException {
          try {
              return Long.parseLong(value);
          } catch (NumberFormatException e) {
              throw new SAXParseException("Attribute '" + name
                      + "' is expected to be an long but was '" + value + "' (" + e.getMessage()
                      + ")", getLocator());
          }
      }
  
      protected boolean getRequiredBooleanAttribute(Attributes atts, String name)
              throws SAXParseException {
          return parseBoolean(name, getRequiredAttribute(atts, name));
      }
  
      protected Boolean getOptionalBooleanAttribute(Attributes atts, String name, Boolean defaultValue)
              throws SAXParseException {
          String value = atts.getValue(name);
          if (value == null) {
              return defaultValue;
          }
          return parseBoolean(name, value);
      }
  
      static final String TRUE = Boolean.TRUE.toString().toLowerCase(Locale.US);
  
      static final String FALSE = Boolean.FALSE.toString().toLowerCase(Locale.US);
  
      private boolean parseBoolean(String name, String value) throws SAXParseException {
          String lowerValue = value.toLowerCase(Locale.US);
          if (lowerValue.equals(TRUE)) {
              return true;
          }
          if (lowerValue.equals(FALSE)) {
              return false;
          }
          throw new SAXParseException("Attribute '" + name
                  + "' is expected to be a boolean but was '" + value + "'", getLocator());
      }
  }
  