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.core;
  
  import java.io.PrintStream;
  import java.text.ParseException;
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.List;
  
  /**
   * Parse a header of a manifest. The manifest header is composed with the following rules:
   *
   * <pre>
   * header ::= header-element (',' header-element)*
   * header-element ::= values (';' (attribute | directive) )*
   * values ::= value (';' value)*
   * value ::= &lt;any string value that does not have ';' or ','&gt;
   * attribute ::= key '=' value
   * directive ::= key '=' value
   * key ::= token
   * value ::= token | quoted-string | double-quoted-string
   * </pre>
   */
  public class ManifestHeaderValue {
  
      private List<ManifestHeaderElement> elements = new ArrayList<>();
  
      ManifestHeaderValue() {
          // just for unit testing
      }
  
      public ManifestHeaderValue(String header) throws ParseException {
          if (header != null) {
              new ManifestHeaderParser(header).parse();
          }
      }
  
      public List<ManifestHeaderElement> getElements() {
          return elements;
      }
  
      public String getSingleValue() {
          if (elements.isEmpty()) {
              return null;
          }
          List<String> values = getElements().iterator().next().getValues();
          if (values.isEmpty()) {
              return null;
          }
          return values.iterator().next();
      }
  
      public List<String> getValues() {
          if (elements.isEmpty()) {
              return Collections.emptyList();
          }
          List<String> list = new ArrayList<>();
          for (ManifestHeaderElement element : getElements()) {
              list.addAll(element.getValues());
          }
          return list;
      }
  
      void addElement(ManifestHeaderElement element) {
          this.elements.add(element);
      }
  
      class ManifestHeaderParser {
  
          /**
           * header to parse
           */
          private final String header;
  
          /**
           * the length of the source
           */
          private int length;
  
          /**
           * buffer
           */
          private StringBuilder buffer = new StringBuilder();
  
          /**
           * position in the source
           */
          private int pos = 0;
  
          /**
           * last read character
           */
          private char c;
  
          /**
           * the header element being build
           */
          private ManifestHeaderElement elem = new ManifestHeaderElement();
  
          /**
           * Once at true (at the first attribute parsed), only parameters are allowed
           */
          private boolean valuesParsed;
  
          /**
           * the last parsed parameter name
           */
          private String paramName;
  
          /**
           * true if the last parsed parameter is a directive (assigned via :=)
           */
          private boolean isDirective;
  
          /**
           * Default constructor
           *
           * @param header
           *            the header to parse
           */
          ManifestHeaderParser(String header) {
              this.header = header;
              this.length = header.length();
          }
  
          /**
           * Do the parsing
           *
           * @throws ParseException if something goes wrong
           */
          void parse() throws ParseException {
              do {
                  elem = new ManifestHeaderElement();
                  int posElement = pos;
                  parseElement();
                  if (elem.getValues().isEmpty()) {
                      error("No defined value", posElement);
                      // try to recover: ignore that element
                      continue;
                  }
                  addElement(elem);
              } while (pos < length);
          }
  
          private char readNext() {
              if (pos == length) {
                  c = '\0';
              } else {
                  c = header.charAt(pos++);
              }
              return c;
          }
  
          private void error(String message) throws ParseException {
              error(message, pos - 1);
          }
  
          private void error(String message, int p) throws ParseException {
              throw new ParseException(message, p);
          }
  
          private void parseElement() throws ParseException {
              valuesParsed = false;
              do {
                  parseValueOrParameter();
              } while (c == ';' && pos < length);
          }
  
          private void parseValueOrParameter() throws ParseException {
              // true if the value/parameter parsing has started, white spaces skipped
              boolean start = false;
              do {
                  switch (readNext()) {
                      case '\0':
                          break;
                      case ';':
                      case ',':
                          endValue();
                          return;
                      case ':':
                      case '=':
                          endParameterName();
                          parseSeparator();
                          parseParameterValue();
                          return;
                      case ' ':
                      case '\t':
                      case '\n':
                      case '\r':
                          if (start) {
                              buffer.append(c);
                          }
                          break;
                      default:
                          start = true;
                          buffer.append(c);
                  }
              } while (pos < length);
              endValue();
          }
  
          private void endValue() throws ParseException {
              if (valuesParsed) {
                  error("Early end of a parameter");
                  // try to recover: ignore it
                  buffer.setLength(0);
                  return;
              }
              if (buffer.length() == 0) {
                  error("Empty value");
                  // try to recover: just ignore the error
              }
              elem.addValue(buffer.toString());
              buffer.setLength(0);
          }
  
          private void endParameterName() throws ParseException {
              if (buffer.length() == 0) {
                  error("Empty parameter name");
                  // try to recover: won't store the value
                  paramName = null;
              }
              paramName = buffer.toString();
              buffer.setLength(0);
          }
  
          private void parseSeparator() throws ParseException {
              if (c == '=') {
                  isDirective = false;
                  return;
              }
              if (readNext() != '=') {
                  error("Expecting '='");
                  // try to recover: will ignore this parameter
                  pos--;
                  paramName = null;
              }
              isDirective = true;
          }
  
          private void parseParameterValue() throws ParseException {
              // true if the value parsing has started, white spaces skipped
              boolean start = false;
              // true if the value parsing is ended, then only white spaces are allowed
              boolean end = false;
              boolean doubleQuoted = false;
              do {
                  switch (readNext()) {
                      case '\0':
                          break;
                      case ',':
                      case ';':
                          endParameterValue();
                          return;
                      case '=':
                      case ':':
                          error("Illegal character '" + c + "' in parameter value of " + paramName);
                          // try to recover: ignore that parameter
                          paramName = null;
                          break;
                      case '\"':
                          doubleQuoted = true;
                      case '\'':
                          if (end && paramName != null) {
                              error("Expecting the end of a parameter value");
                              // try to recover: ignore that parameter
                              paramName = null;
                          }
                          if (start) {
                              // quote in the middle of the value, just add it as a quote
                              buffer.append(c);
                          } else {
                              start = true;
                              appendQuoted(doubleQuoted);
                              end = true;
                          }
                          break;
                      case '\\':
                          if (end && paramName != null) {
                              error("Expecting the end of a parameter value");
                              // try to recover: ignore that parameter
                              paramName = null;
                          }
                          start = true;
                          appendEscaped();
                          break;
                      case ' ':
                      case '\t':
                      case '\n':
                      case '\r':
                          if (start) {
                              end = true;
                          }
                          break;
                      default:
                          if (end && paramName != null) {
                              error("Expecting the end of a parameter value");
                              // try to recover: ignore that parameter
                              paramName = null;
                          }
                          start = true;
                          buffer.append(c);
                  }
              } while (pos < length);
              endParameterValue();
          }
  
          private void endParameterValue() throws ParseException {
              if (paramName == null) {
                  // recovering from an incorrect parameter: skip the value
                  return;
              }
              if (buffer.length() == 0) {
                  error("Empty parameter value");
                  // try to recover: do not store the parameter
                  return;
              }
              String value = buffer.toString();
              if (isDirective) {
                  elem.addDirective(paramName, value);
              } else {
                  elem.addAttribute(paramName, value);
              }
              valuesParsed = true;
              buffer.setLength(0);
          }
  
          private void appendQuoted(boolean doubleQuoted) {
              do {
                  switch (readNext()) {
                      case '\0':
                          break;
                      case '\"':
                          if (doubleQuoted) {
                              return;
                          }
                          buffer.append(c);
                          break;
                      case '\'':
                          if (!doubleQuoted) {
                              return;
                          }
                          buffer.append(c);
                          break;
                      case '\\':
                          break;
                      default:
                          buffer.append(c);
                  }
              } while (pos < length);
          }
  
          private void appendEscaped() {
              if (pos < length) {
                  buffer.append(readNext());
              } else {
                  buffer.append(c);
              }
          }
      }
  
      public boolean equals(Object obj) {
          if (!(obj instanceof ManifestHeaderValue)) {
              return false;
          }
          ManifestHeaderValue other = (ManifestHeaderValue) obj;
          if (other.elements.size() != elements.size()) {
              return false;
          }
          for (ManifestHeaderElement element : elements) {
              if (!other.elements.contains(element)) {
                  return false;
              }
          }
          return true;
      }
  
      public String toString() {
          StringBuilder sb = new StringBuilder();
          for (ManifestHeaderElement element : elements) {
              if (sb.length() > 0) {
                  sb.append(",");
              }
              sb.append(element.toString());
          }
          return sb.toString();
      }
  
      public static void writeParseException(PrintStream out, String source, ParseException e) {
          out.println(e.getMessage());
          out.print("   " + source + "\n   ");
          for (int i = 0; i < e.getErrorOffset(); i++) {
              out.print(' ');
          }
          out.println('^');
      }
  }
  