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.core.module.id;
  
  import java.lang.ref.WeakReference;
  import java.util.HashMap;
  import java.util.Map;
  import java.util.WeakHashMap;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  
  import org.apache.ivy.Ivy;
  import org.apache.ivy.core.IvyContext;
  import org.apache.ivy.core.IvyPatternHelper;
  import org.apache.ivy.util.extendable.UnmodifiableExtendableItem;
  
  /**
   * Identifies a module in a particular version
   *
   * @see <a href="package-summary.html">org.apache.ivy.core.module.id</a>
   */
  public class ModuleRevisionId extends UnmodifiableExtendableItem {
  
      private static final String ENCODE_SEPARATOR = ModuleId.ENCODE_SEPARATOR;
  
      private static final String ENCODE_PREFIX = "+";
  
      private static final String NULL_ENCODE = "@#:NULL:#@";
  
      static final String STRICT_CHARS_PATTERN = "[a-zA-Z0-9\\-/\\._+=]";
  
      private static final String REV_STRICT_CHARS_PATTERN = "[a-zA-Z0-9\\-/\\._+=,\\[\\]\\{\\}\\(\\):@]";
  
      private static final Map<ModuleRevisionId, WeakReference<ModuleRevisionId>> CACHE = new WeakHashMap<>();
  
      /**
       * Pattern to use to matched mrid text representation.
       *
       * @see #parse(String)
       */
      public static final Pattern MRID_PATTERN = Pattern.compile("(" + STRICT_CHARS_PATTERN + "*)"
              + "#(" + STRICT_CHARS_PATTERN + "+)" + "(?:#(" + STRICT_CHARS_PATTERN + "+))?" + ";("
              + REV_STRICT_CHARS_PATTERN + "+)");
  
      /**
       * Same as MRID_PATTERN but using non capturing groups, useful to build larger regexp
       */
      public static final Pattern NON_CAPTURING_PATTERN = Pattern.compile("(?:"
              + STRICT_CHARS_PATTERN + "*)" + "#(?:" + STRICT_CHARS_PATTERN + "+)" + "(?:#(?:"
              + STRICT_CHARS_PATTERN + "+))?" + ";(?:" + REV_STRICT_CHARS_PATTERN + "+)");
  
      /**
       * Parses a module revision id text representation and returns a new {@link ModuleRevisionId}
       * instance corresponding to the parsed String.
       * <p>
       * The result is unspecified if the module doesn't respect strict name conventions.
       * </p>
       *
       * @param mrid
       *            the text representation of the module (as returned by {@link #toString()}). Must
       *            not be <code>null</code>.
       * @return a {@link ModuleRevisionId} corresponding to the given text representation
       * @throws IllegalArgumentException
       *             if the given text representation does not match the {@link ModuleRevisionId} text
       *             representation rules.
       */
      public static ModuleRevisionId parse(String mrid) {
          Matcher m = MRID_PATTERN.matcher(mrid.trim());
          if (!m.matches()) {
              throw new IllegalArgumentException(
                      "module revision text representation do not match expected pattern."
                              + " given mrid='" + mrid + "' expected form=" + MRID_PATTERN.pattern());
          }
  
          // CheckStyle:MagicNumber| OFF
          return newInstance(m.group(1), m.group(2), m.group(3), m.group(4));
          // CheckStyle:MagicNumber| ON
      }
  
      public static ModuleRevisionId newInstance(String organisation, String name, String revision) {
          return intern(new ModuleRevisionId(ModuleId.newInstance(organisation, name), revision));
      }
  
      public static ModuleRevisionId newInstance(String organisation, String name, String revision,
              Map<String, String> extraAttributes) {
          return intern(new ModuleRevisionId(ModuleId.newInstance(organisation, name), revision,
                  extraAttributes));
      }
  
      public static ModuleRevisionId newInstance(String organisation, String name, String branch,
              String revision) {
          return intern(new ModuleRevisionId(ModuleId.newInstance(organisation, name), branch,
                  revision));
      }
  
      public static ModuleRevisionId newInstance(String organisation, String name, String branch,
              String revision, Map<String, String> extraAttributes) {
          return intern(new ModuleRevisionId(ModuleId.newInstance(organisation, name), branch,
                  revision, extraAttributes));
      }
  
      public static ModuleRevisionId newInstance(String organisation, String name, String branch,
              String revision, Map<String, String> extraAttributes,
              boolean replaceNullBranchWithDefault) {
          return intern(new ModuleRevisionId(ModuleId.newInstance(organisation, name), branch,
                  revision, extraAttributes, replaceNullBranchWithDefault));
      }
  
      public static ModuleRevisionId newInstance(ModuleRevisionId mrid, String rev) {
          return intern(new ModuleRevisionId(mrid.getModuleId(), mrid.getBranch(), rev,
                  mrid.getQualifiedExtraAttributes()));
      }
  
      public static ModuleRevisionId newInstance(ModuleRevisionId mrid, String branch, String rev) {
          return intern(new ModuleRevisionId(mrid.getModuleId(), branch, rev,
                  mrid.getQualifiedExtraAttributes()));
      }
  
      /**
       * Returns an intern instance of the given ModuleRevisionId if any, or put the given
       * ModuleRevisionId in a cache of intern instances and returns it.
       * <p>
       * This method should be called on ModuleRevisionId created with one of the constructor to
       * decrease memory footprint.
       * </p>
       * <p>
       * When using static newInstances methods, this method is already called.
       * </p>
       *
       * @param moduleRevisionId
       *            the module revision id to intern
       * @return an interned ModuleRevisionId
       */
      public static ModuleRevisionId intern(ModuleRevisionId moduleRevisionId) {
          ModuleRevisionId r = null;
  
          synchronized (CACHE) {
              WeakReference<ModuleRevisionId> ref = CACHE.get(moduleRevisionId);
              if (ref != null) {
                  r = ref.get();
              }
              if (r == null) {
                  r = moduleRevisionId;
                  CACHE.put(r, new WeakReference<>(r));
              }
          }
  
          return r;
      }
  
      private final ModuleId moduleId;
  
      private final String branch;
  
      private final String revision;
  
      private int hash;
  
      // TODO: make these constructors private and use only static factory methods
  
      public ModuleRevisionId(ModuleId moduleId, String revision) {
          this(moduleId, null, revision, null);
      }
  
      public ModuleRevisionId(ModuleId moduleId, String branch, String revision) {
          this(moduleId, branch, revision, null);
      }
  
      private ModuleRevisionId(ModuleId moduleId, String revision, Map<String, String> extraAttributes) {
          this(moduleId, null, revision, extraAttributes);
      }
  
      private ModuleRevisionId(ModuleId moduleId, String branch, String revision,
              Map<String, String> extraAttributes) {
          this(moduleId, branch, revision, extraAttributes, true);
      }
  
      private ModuleRevisionId(ModuleId moduleId, String branch, String revision,
              Map<String, String> extraAttributes, boolean replaceNullBranchWithDefault) {
          super(null, extraAttributes);
          this.moduleId = moduleId;
          IvyContext context = IvyContext.getContext();
          this.branch = (replaceNullBranchWithDefault && branch == null)
          // we test if there's already an Ivy instance loaded, to avoid loading a default one
          // just to get the default branch
          ? (context.peekIvy() == null ? null : context.getSettings().getDefaultBranch(moduleId))
                  : branch;
          this.revision = revision == null ? Ivy.getWorkingRevision() : normalizeRevision(revision);
          setStandardAttribute(IvyPatternHelper.ORGANISATION_KEY, this.moduleId.getOrganisation());
          setStandardAttribute(IvyPatternHelper.MODULE_KEY, this.moduleId.getName());
          setStandardAttribute(IvyPatternHelper.BRANCH_KEY, this.branch);
          setStandardAttribute(IvyPatternHelper.REVISION_KEY, this.revision);
      }
  
      public ModuleId getModuleId() {
          return moduleId;
      }
  
      public String getName() {
          return getModuleId().getName();
      }
  
      public String getOrganisation() {
          return getModuleId().getOrganisation();
      }
  
      public String getRevision() {
          return revision;
      }
  
      @Override
      public boolean equals(Object obj) {
          if (!(obj instanceof ModuleRevisionId)) {
              return false;
          }
          ModuleRevisionId other = (ModuleRevisionId) obj;
  
          return other.getRevision().equals(getRevision())
                  && !(other.getBranch() == null && getBranch() != null)
                  && !(other.getBranch() != null && !other.getBranch().equals(getBranch()))
                  && other.getModuleId().equals(getModuleId())
                  && other.getQualifiedExtraAttributes().equals(getQualifiedExtraAttributes());
      }
  
      @Override
      public int hashCode() {
          if (hash == 0) {
              // CheckStyle:MagicNumber| OFF
              hash = 31;
              hash = hash * 13 + (getBranch() == null ? 0 : getBranch().hashCode());
              hash = hash * 13 + getRevision().hashCode();
              hash = hash * 13 + getModuleId().hashCode();
              hash = hash * 13 + getQualifiedExtraAttributes().hashCode();
              // CheckStyle:MagicNumber| ON
          }
          return hash;
      }
  
      @Override
      public String toString() {
          return moduleId + (branch == null || branch.length() == 0 ? "" : "#" + branch) + ";"
                  + (revision == null ? "NONE" : revision);
      }
  
      public String encodeToString() {
          StringBuilder buf = new StringBuilder();
          Map<String, String> attributes = new HashMap<>(getAttributes());
          attributes.keySet().removeAll(getExtraAttributes().keySet());
          attributes.putAll(getQualifiedExtraAttributes());
  
          for (Map.Entry<String, String> att : attributes.entrySet()) {
              String value = att.getValue();
              value = (value == null) ? NULL_ENCODE : value;
              buf.append(ENCODE_PREFIX).append(att.getKey()).append(ENCODE_SEPARATOR)
                      .append(ENCODE_PREFIX).append(value).append(ENCODE_SEPARATOR);
          }
          return buf.toString();
      }
  
      public static ModuleRevisionId decode(String encoded) {
          String[] parts = encoded.split(ENCODE_SEPARATOR);
          if (parts.length % 2 != 0) {
              throw new IllegalArgumentException("badly encoded module revision id: '" + encoded
                      + "'");
          }
          Map<String, String> attributes = new HashMap<>();
          for (int i = 0; i < parts.length; i += 2) {
              String attName = parts[i];
              if (!attName.startsWith(ENCODE_PREFIX)) {
                  throw new IllegalArgumentException("badly encoded module revision id: '" + encoded
                          + "': " + attName + " doesn't start with " + ENCODE_PREFIX);
              } else {
                  attName = attName.substring(1);
              }
              String attValue = parts[i + 1];
              if (!attValue.startsWith(ENCODE_PREFIX)) {
                  throw new IllegalArgumentException("badly encoded module revision id: '" + encoded
                          + "': " + attValue + " doesn't start with " + ENCODE_PREFIX);
              } else {
                  attValue = attValue.substring(1);
              }
              if (NULL_ENCODE.equals(attValue)) {
                  attValue = null;
              }
              attributes.put(attName, attValue);
          }
          String org = attributes.remove(IvyPatternHelper.ORGANISATION_KEY);
          String mod = attributes.remove(IvyPatternHelper.MODULE_KEY);
          String rev = attributes.remove(IvyPatternHelper.REVISION_KEY);
          String branch = attributes.remove(IvyPatternHelper.BRANCH_KEY);
          if (org == null) {
              throw new IllegalArgumentException("badly encoded module revision id: '" + encoded
                      + "': no organisation");
          }
          if (mod == null) {
              throw new IllegalArgumentException("badly encoded module revision id: '" + encoded
                      + "': no module name");
          }
          if (rev == null) {
              throw new IllegalArgumentException("badly encoded module revision id: '" + encoded
                      + "': no revision");
          }
          return newInstance(org, mod, branch, rev, attributes);
      }
  
      public String getBranch() {
          return branch;
      }
  
      /**
       * [revision] is a valid revision in maven. This method strips the '[' and ']' characters. Cfr.
       * http://docs.codehaus.org/x/IGU
       */
      private static String normalizeRevision(String revision) {
          if (revision.startsWith("[") && revision.endsWith("]") && revision.indexOf(',') == -1) {
              if (IvyPatternHelper.getTokenString(IvyPatternHelper.REVISION_KEY).equals(revision)) {
                  // this is the case when listing dynamic revisions
                  return revision;
              }
  
              return revision.substring(1, revision.length() - 1);
          } else {
              return revision;
          }
      }
  }
  