1 /*
   2  * Copyright (c) 1998, 2018, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package jdk.javadoc.internal.doclets.toolkit.util;
  27 
  28 import java.util.ArrayList;
  29 import java.util.Arrays;
  30 import java.util.Collections;
  31 import java.util.List;
  32 
  33 import javax.lang.model.element.ModuleElement;
  34 import javax.lang.model.element.PackageElement;
  35 import javax.lang.model.element.TypeElement;
  36 
  37 /**
  38  * Abstraction for immutable relative paths.
  39  * Paths always use '/' as a separator, and never begin or end with '/'.
  40  *
  41  *  <p><b>This is NOT part of any supported API.
  42  *  If you write code that depends on this, you do so at your own risk.
  43  *  This code and its internal interfaces are subject to change or
  44  *  deletion without notice.</b>
  45  */
  46 public class DocPath {
  47     private final String path;
  48 
  49     /** The empty path. */
  50     public static final DocPath empty = new DocPath("");
  51 
  52     /** The empty path. */
  53     public static final DocPath parent = new DocPath("..");
  54 
  55     /**
  56      * Creates a path from a string.
  57      * @param p the string
  58      * @return the path
  59      */
  60     public static DocPath create(String p) {
  61         return (p == null) || p.isEmpty() ? empty : new DocPath(p);
  62     }
  63 
  64     /**
  65      * Returns the path for a class.
  66      * For example, if the class is java.lang.Object,
  67      * the path is java/lang/Object.html.
  68      * @param utils utility class for handling type elements
  69      * @param typeElement the type element
  70      * @return the path
  71      */
  72     public static DocPath forClass(Utils utils, TypeElement typeElement) {
  73         return (typeElement == null)
  74                 ? empty
  75                 : forPackage(utils.containingPackage(typeElement)).resolve(forName(utils, typeElement));
  76     }
  77 
  78     /**
  79      * Returns the path for the simple name of a class.
  80      * For example, if the class is java.lang.Object,
  81      * the path is Object.html.
  82      * @param utils utility class for handling type elements
  83      * @param typeElement the type element
  84      * @return the path
  85      */
  86     public static DocPath forName(Utils utils, TypeElement typeElement) {
  87         return (typeElement == null) ? empty : new DocPath(utils.getSimpleName(typeElement) + ".html");
  88     }
  89 
  90     /**
  91      * Returns the path for the name of a module.
  92      * For example, if the module is java.base,
  93      * the path is java.base.
  94      * @param mdle the module element
  95      * @return the path
  96      */
  97     public static DocPath forModule(ModuleElement mdle) {
  98         return mdle == null || mdle.isUnnamed()
  99                 ? empty
 100                 : DocPath.create(mdle.getQualifiedName().toString());
 101     }
 102 
 103     /**
 104      * Returns the path for the package of a class.
 105      * For example, if the class is java.lang.Object,
 106      * the path is java/lang.
 107      * @param utils utility class for handling type elements
 108      * @param typeElement the type element
 109      * @return the path
 110      */
 111     public static DocPath forPackage(Utils utils, TypeElement typeElement) {
 112         return (typeElement == null) ? empty : forPackage(utils.containingPackage(typeElement));
 113     }
 114 
 115     /**
 116      * Returns the path for a package.
 117      * For example, if the package is java.lang,
 118      * the path is java/lang.
 119      * @param pkgElement the package element
 120      * @return the path
 121      */
 122     public static DocPath forPackage(PackageElement pkgElement) {
 123         return pkgElement == null || pkgElement.isUnnamed()
 124                 ? empty
 125                 : DocPath.create(pkgElement.getQualifiedName().toString().replace('.', '/'));
 126     }
 127 
 128     /**
 129      * Returns the inverse path for a package.
 130      * For example, if the package is java.lang,
 131      * the inverse path is ../...
 132      * @param pkgElement the package element
 133      * @return the path
 134      */
 135     public static DocPath forRoot(PackageElement pkgElement) {
 136         String name = (pkgElement == null || pkgElement.isUnnamed())
 137                 ? ""
 138                 : pkgElement.getQualifiedName().toString();
 139         return new DocPath(name.replace('.', '/').replaceAll("[^/]+", ".."));
 140     }
 141 
 142     /**
 143      * Returns the relative path from one package to another.
 144      * @param from the initial package
 145      * @param to the target package
 146      * @return the path
 147      */
 148     public static DocPath relativePath(PackageElement from, PackageElement to) {
 149         return forRoot(from).resolve(forPackage(to));
 150     }
 151 
 152     protected DocPath(String p) {
 153         path = (p.endsWith("/") ? p.substring(0, p.length() - 1) : p);
 154     }
 155 
 156     /** {@inheritDoc} */
 157     @Override
 158     public boolean equals(Object other) {
 159         return (other instanceof DocPath) && path.equals(((DocPath)other).path);
 160     }
 161 
 162     /** {@inheritDoc} */
 163     @Override
 164     public int hashCode() {
 165         return path.hashCode();
 166     }
 167 
 168     public DocPath basename() {
 169         int sep = path.lastIndexOf("/");
 170         return (sep == -1) ? this : new DocPath(path.substring(sep + 1));
 171     }
 172 
 173     public DocPath parent() {
 174         int sep = path.lastIndexOf("/");
 175         return (sep == -1) ? empty : new DocPath(path.substring(0, sep));
 176     }
 177 
 178     /**
 179      * Returns the path formed by appending the specified string to the current path.
 180      * @param p the string
 181      * @return the path
 182      */
 183     public DocPath resolve(String p) {
 184         if (p == null || p.isEmpty())
 185             return this;
 186         if (path.isEmpty())
 187             return new DocPath(p);
 188         return new DocPath(path + "/" + p);
 189     }
 190 
 191     /**
 192      * Returns the path by appending the specified path to the current path.
 193      * @param p the path
 194      * @return the path
 195      */
 196     public DocPath resolve(DocPath p) {
 197         if (p == null || p.isEmpty())
 198             return this;
 199         if (path.isEmpty())
 200             return p;
 201         return new DocPath(path + "/" + p.getPath());
 202     }
 203 
 204     /**
 205      * Return the inverse path for this path.
 206      * For example, if the path is a/b/c, the inverse path is ../../..
 207      * @return the path
 208      */
 209     public DocPath invert() {
 210         return new DocPath(path.replaceAll("[^/]+", ".."));
 211     }
 212 
 213     /**
 214      * Returns the path formed by eliminating empty components,
 215      * '.' components, and redundant name/.. components.
 216      * @return the path
 217      */
 218     public DocPath normalize() {
 219         return path.isEmpty()
 220                 ? this
 221                 : new DocPath(String.join("/", normalize(path)));
 222     }
 223 
 224     private static List<String> normalize(String path) {
 225         return normalize(Arrays.asList(path.split("/")));
 226     }
 227 
 228     private static List<String> normalize(List<String> parts) {
 229         if (parts.stream().noneMatch(s -> s.isEmpty() || s.equals(".") || s.equals(".."))) {
 230             return parts;
 231         }
 232         List<String> normalized = new ArrayList<>();
 233         for (String part : parts) {
 234             switch (part) {
 235                 case "":
 236                 case ".":
 237                     break;
 238                 case "..":
 239                     int n = normalized.size();
 240                     if (n > 0 && !normalized.get(n - 1).equals("..")) {
 241                         normalized.remove(n - 1);
 242                     } else {
 243                         normalized.add(part);
 244                     }
 245                     break;
 246                 default:
 247                     normalized.add(part);
 248             }
 249         }
 250         return normalized;
 251     }
 252 
 253     /**
 254      * Normalize and relativize a path against this path,
 255      * assuming that this path is for a file (not a directory),
 256      * in which the other path will appear.
 257      *
 258      * @param other the path to be relativized.
 259      * @return the simplified path
 260      */
 261     public DocPath relativize(DocPath other) {
 262         if (other == null || other.path.isEmpty()) {
 263             return this;
 264         }
 265 
 266         if (path.isEmpty()) {
 267             return other;
 268         }
 269 
 270         List<String> originParts = normalize(path);
 271         int sep = path.lastIndexOf("/");
 272         List<String> destParts = sep == -1
 273                 ? normalize(other.path)
 274                 : normalize(path.substring(0, sep + 1) + other.path);
 275         int common = 0;
 276         while (common < originParts.size()
 277                 && common < destParts.size()
 278                 && originParts.get(common).equals(destParts.get(common))) {
 279             common++;
 280         }
 281 
 282         List<String> newParts;
 283         if (common == originParts.size()) {
 284             newParts = destParts.subList(common, destParts.size());
 285         } else {
 286             newParts = new ArrayList<>();
 287             newParts.addAll(Collections.nCopies(originParts.size() - common - 1, ".."));
 288             newParts.addAll(destParts.subList(common, destParts.size()));
 289         }
 290         return new DocPath(String.join("/", newParts));
 291     }
 292 
 293     /**
 294      * Return true if this path is empty.
 295      * @return true if this path is empty
 296      */
 297     public boolean isEmpty() {
 298         return path.isEmpty();
 299     }
 300 
 301     /**
 302      * Creates a DocLink formed from this path and a fragment identifier.
 303      * @param fragment the fragment
 304      * @return the link
 305      */
 306     public DocLink fragment(String fragment) {
 307         return new DocLink(path, null, fragment);
 308     }
 309 
 310     /**
 311      * Creates a DocLink formed from this path and a query string.
 312      * @param query the query string
 313      * @return the link
 314      */
 315     public DocLink query(String query) {
 316         return new DocLink(path, query, null);
 317     }
 318 
 319     /**
 320      * Returns this path as a string.
 321      * @return the path
 322      */
 323     // This is provided instead of using toString() to help catch
 324     // unintended use of toString() in string concatenation sequences.
 325     public String getPath() {
 326         return path;
 327     }
 328 }