001/*
002 * Copyright (c) 1998, 2022, Oracle and/or its affiliates. All rights reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation.  Oracle designates this
008 * particular file as subject to the "Classpath" exception as provided
009 * by Oracle in the LICENSE file that accompanied this code.
010 *
011 * This code is distributed in the hope that it will be useful, but WITHOUT
012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
014 * version 2 for more details (a copy is included in the LICENSE file that
015 * accompanied this code).
016 *
017 * You should have received a copy of the GNU General Public License version
018 * 2 along with this work; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020 *
021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
022 * or visit www.oracle.com if you need additional information or have any
023 * questions.
024 */
025
026package org.jdrupes.mdoclet.internal.doclets.toolkit.util;
027
028import java.util.ArrayList;
029import java.util.Arrays;
030import java.util.Collections;
031import java.util.List;
032
033/**
034 * Abstraction for immutable relative paths.
035 * Paths always use '/' as a separator, and never begin or end with '/'.
036 */
037public class DocPath {
038    private final String path;
039
040    /** The empty path. */
041    public static final DocPath empty = new DocPath("");
042
043    /** The empty path. */
044    public static final DocPath parent = new DocPath("..");
045
046    /**
047     * Creates a path from a string.
048     * @param p the string
049     * @return the path
050     */
051    public static DocPath create(String p) {
052        return (p == null) || p.isEmpty() ? empty : new DocPath(p);
053    }
054
055    protected DocPath(String p) {
056        path = (p.endsWith("/") ? p.substring(0, p.length() - 1) : p);
057    }
058
059    @Override
060    public boolean equals(Object other) {
061        return (other instanceof DocPath dp) && path.equals(dp.path);
062    }
063
064    @Override
065    public int hashCode() {
066        return path.hashCode();
067    }
068
069    public DocPath basename() {
070        int sep = path.lastIndexOf("/");
071        return (sep == -1) ? this : new DocPath(path.substring(sep + 1));
072    }
073
074    public DocPath parent() {
075        int sep = path.lastIndexOf("/");
076        return (sep == -1) ? empty : new DocPath(path.substring(0, sep));
077    }
078
079    /**
080     * Returns the path formed by appending the specified string to the current path.
081     * @param p the string
082     * @return the path
083     */
084    public DocPath resolve(String p) {
085        if (p == null || p.isEmpty())
086            return this;
087        if (path.isEmpty())
088            return new DocPath(p);
089        return new DocPath(path + "/" + p);
090    }
091
092    /**
093     * Returns the path by appending the specified path to the current path.
094     * @param p the path
095     * @return the path
096     */
097    public DocPath resolve(DocPath p) {
098        if (p == null || p.isEmpty())
099            return this;
100        if (path.isEmpty())
101            return p;
102        return new DocPath(path + "/" + p.getPath());
103    }
104
105    /**
106     * Return the inverse path for this path.
107     * For example, if the path is a/b/c, the inverse path is ../../..
108     * @return the path
109     */
110    public DocPath invert() {
111        return new DocPath(path.replaceAll("[^/]+", ".."));
112    }
113
114    /**
115     * Returns the path formed by eliminating empty components,
116     * '.' components, and redundant name/.. components.
117     * @return the path
118     */
119    public DocPath normalize() {
120        return path.isEmpty()
121            ? this
122            : new DocPath(String.join("/", normalize(path)));
123    }
124
125    private static List<String> normalize(String path) {
126        return normalize(Arrays.asList(path.split("/")));
127    }
128
129    private static List<String> normalize(List<String> parts) {
130        if (parts.stream()
131            .noneMatch(s -> s.isEmpty() || s.equals(".") || s.equals(".."))) {
132            return parts;
133        }
134        List<String> normalized = new ArrayList<>();
135        for (String part : parts) {
136            switch (part) {
137            case "":
138            case ".":
139                break;
140            case "..":
141                int n = normalized.size();
142                if (n > 0 && !normalized.get(n - 1).equals("..")) {
143                    normalized.remove(n - 1);
144                } else {
145                    normalized.add(part);
146                }
147                break;
148            default:
149                normalized.add(part);
150            }
151        }
152        return normalized;
153    }
154
155    /**
156     * Normalize and relativize a path against this path,
157     * assuming that this path is for a file (not a directory),
158     * in which the other path will appear.
159     *
160     * @param other the path to be relativized.
161     * @return the simplified path
162     */
163    public DocPath relativize(DocPath other) {
164        if (other == null || other.path.isEmpty()) {
165            return this;
166        }
167
168        if (path.isEmpty()) {
169            return other;
170        }
171
172        List<String> originParts = normalize(path);
173        int sep = path.lastIndexOf("/");
174        List<String> destParts = sep == -1
175            ? normalize(other.path)
176            : normalize(path.substring(0, sep + 1) + other.path);
177        int common = 0;
178        while (common < originParts.size()
179            && common < destParts.size()
180            && originParts.get(common).equals(destParts.get(common))) {
181            common++;
182        }
183
184        List<String> newParts;
185        if (common == originParts.size()) {
186            newParts = destParts.subList(common, destParts.size());
187        } else {
188            newParts = new ArrayList<>();
189            newParts.addAll(
190                Collections.nCopies(originParts.size() - common - 1, ".."));
191            newParts.addAll(destParts.subList(common, destParts.size()));
192        }
193        return new DocPath(String.join("/", newParts));
194    }
195
196    /**
197     * Return true if this path is empty.
198     * @return true if this path is empty
199     */
200    public boolean isEmpty() {
201        return path.isEmpty();
202    }
203
204    /**
205     * Creates a DocLink formed from this path and a fragment identifier.
206     * @param fragment the fragment
207     * @return the link
208     */
209    public DocLink fragment(String fragment) {
210        return new DocLink(path, fragment);
211    }
212
213    /**
214     * Returns this path as a string.
215     * @return the path
216     */
217    // This is provided instead of using toString() to help catch
218    // unintended use of toString() in string concatenation sequences.
219    public String getPath() {
220        return path;
221    }
222}