/*
* The MIT License
*
* Copyright 2013 Tim Boudreau.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.mastfrog.url;
import com.mastfrog.util.AbstractBuilder;
import com.mastfrog.util.Checks;
import com.mastfrog.util.Exceptions;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Pattern;
import org.openide.util.NbBundle;
/**
* The path portion of a URL.
*
* @author Tim Boudreau
*/
public final class Path implements URLComponent, Iterable<PathElement> {
private static final long serialVersionUID = 1L;
private final PathElement[] elements;
private final boolean illegal;
public Path (PathElement... elements) {
Checks.notNull("elements", elements);
this.elements = new PathElement[elements.length];
System.arraycopy(elements, 0, this.elements, 0, elements.length);
illegal = normalizePath().illegal;
}
Path (NormalizeResult n) {
Checks.notNull("n", n);
illegal = n.illegal;
this.elements = illegal ? n.original : n.elements.toArray(new PathElement[n.elements.size()]);
}
static final Pattern PATH_PATTERN = Pattern.compile (URLBuilder.PATH_ELEMENT_DELIMITER + "([$.]*?)");
/**
* Parse a path in the format <code>element1/element2/element3</code>
* @param path
* @return
*/
public static Path parse(String path) {
return Path.parse(path, false);
}
public static Path parse(String path, boolean decode) {
Checks.notNull("path", path);
//XXX handle relative paths
List<PathElement> l = new ArrayList<>(12);
char[] ch = path.toCharArray();
StringBuilder sb = new StringBuilder();
// http://foo.com/relative/path/../../stuff = http://foo.com/stuff
// http://foo.com/./,/stuff = http://foo.com/stuff
try {
for (int i = 0; i < ch.length; i++) {
char c = ch[i];
switch (c) {
case '/':
if (i == 0) {
continue;
}
if (i == ch.length - 1) {
if (decode) {
l.add(new PathElement(URLDecoder.decode(sb.toString(), "UTF-8"), true, decode));
} else {
l.add(new PathElement(sb.toString(), true));
}
} else {
if (decode) {
l.add(new PathElement(URLDecoder.decode(sb.toString(), "UTF-8"), false, decode));
} else {
l.add(new PathElement(sb.toString(), false));
}
}
sb.setLength(0);
break;
default:
sb.append(c);
}
}
if (sb.length() > 0) {
if (decode) {
l.add(new PathElement(URLDecoder.decode(sb.toString()), false, decode));
} else {
l.add(new PathElement(sb.toString(), false));
}
}
if (!l.isEmpty() && path.endsWith("/")) {
PathElement el = l.get(l.size() - 1);
l.set(l.size() - 1, el.toTrailingSlashElement());
}
} catch (UnsupportedEncodingException e) {
return Exceptions.chuck(e);
}
return new Path(l.toArray(new PathElement[l.size()]));
}
public Path toURLDecodedPath() {
List<PathElement> el = new ArrayList<>(size());
for (PathElement p : this) {
try {
el.add(new PathElement(URLDecoder.decode(p.toString(), "UTF-8"), true));
} catch (UnsupportedEncodingException ex) {
return Exceptions.chuck(ex);
}
}
Path result = new Path(el.toArray(new PathElement[size()]));
return result.equals(this) ? this : result;
}
public Iterator<PathElement> iterator() {
return Arrays.asList(elements).iterator();
}
public Path normalize() {
return new Path (normalizePath());
}
public Path replace (String old, String nue) {
PathElement[] els = this.getElements();
for (int i = 0; i < els.length; i++) {
if (els[i].toNonTrailingSlashElement().toString().equals(old)) {
boolean hadTrailingSlash = !els[i].toString().equals(els[i].toNonTrailingSlashElement().toString());
PathElement nu = new PathElement(nue, hadTrailingSlash);
els[i] = nu;
}
}
return new Path(els);
}
NormalizeResult normalizePath() {
List<PathElement> result = new ArrayList<>();
boolean illegal = false;
for (PathElement e : elements) {
if (".".equals(e.rawText())) {
continue;
}
if ("..".equals(e.rawText())) {
if (result.size() > 0) {
result.remove(result.size() - 1);
if (result.size() > 0) {
result.set(result.size() - 1, result.get(result.size() - 1).toTrailingSlashElement());
}
continue;
} else {
illegal = true;
}
}
result.add(e);
}
NormalizeResult res = new NormalizeResult (result, this.elements, illegal);
return res;
}
static final class NormalizeResult {
private final List<PathElement> elements;
private final boolean illegal;
private final PathElement[] original;
public NormalizeResult(List<PathElement> elements, PathElement[] original, boolean illegal) {
Checks.notNull("original", original);
Checks.notNull("elements", elements);
this.original = new PathElement[original.length];
System.arraycopy(original, 0, this.original, 0, original.length);
this.elements = elements;
this.illegal = illegal;
}
}
public Path prepend (String part) {
return merge (Path.parse(part), this);
}
public Path append (String part) {
return merge (this, Path.parse(part));
}
/**
* Merge an array of paths together
* @param paths An array of paths
* @return A merged path which appends all elements of all passed paths
*/
public static Path merge(Path... paths) {
Checks.notEmptyOrNull("paths", paths);
List<PathElement> l = new ArrayList<>(paths.length * 10);
for (Path p : paths) {
l.addAll (Arrays.asList(p.getElements()));
}
return new Path (l.toArray(new PathElement[l.size()]));
}
/**
* Determine if this path is a path to a parent of the passed path. E.g.
* <pre>
* assert Path.parse ("com/foo").isParentOf(Path.parse("com/foo/bar")) == true;
* </pre>
*
* @param path A path
* @return whether or not this path is a parent of the passed path.
*/
public boolean isParentOf (Path path) {
Checks.notNull("path", path);
return path.toString().startsWith(toString());
}
/**
* Determine if this path is a path to a child of the passed path. E.g.
* <pre>
* assert Path.parse ("com/foo/bar").isChildOf(Path.parse("com/foo")) == true;
* </pre>
*
* @param path a path
* @return whether or not this Path is a child of the passed Path
*/
public boolean isChildOf (Path path) {
Checks.notNull("path", path);
return path.isParentOf(this);
}
/**
* Get the number of elements in this path.
* @return
*/
public int size() {
return elements.length;
}
/**
* Get the individual elements of this path.
* @return An array of elements
*/
public PathElement[] getElements() {
PathElement[] result = new PathElement[elements.length];
System.arraycopy(elements, 0, result, 0, elements.length);
return result;
}
/**
* Get a Path which does not include the first element of this path.
* E.g. the child path of <code>com/foo/bar</code> is <code>foo/bar</code>.
*
* @return A child path
*/
public Path getChildPath() {
if (elements.length > 1) {
PathElement[] els = new PathElement[elements.length - 1];
System.arraycopy(elements, 1, els, 0, els.length);
return new Path(els);
}
return null;
}
public String toStringWithLeadingSlash() {
StringBuilder result = new StringBuilder();
appendTo(result);
if (result.charAt(0) != '/') {
result.insert(0, '/');
}
return result.toString();
}
/**
* Get the parent of this path. E.g. the parent of
* <code>com/foo/bar</code> is <code>com/foo</code>.
* @return A path
*/
public Path getParentPath() {
if (elements.length > 1) {
PathElement[] els = new PathElement[elements.length - 1];
System.arraycopy(elements, 0, els, 0, els.length);
return new Path(els);
}
return null;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
appendTo(sb);
return sb.toString();
}
public static AbstractBuilder<PathElement, Path> builder() {
return new PathBuilder();
}
boolean isIllegal() {
return illegal;
}
@Override
public boolean isValid() {
if (illegal) {
return false;
}
for (PathElement e : elements) {
if (!e.isValid()) {
return false;
}
}
return true;
}
@Override
public String getComponentName() {
return NbBundle.getMessage(Path.class, "path");
}
/**
* Determine if this path is probably a reference to a file (last element
* contains a . character). May be used to decide whether or not to append
* a '/' character.
* @return true if this is a probable file.
*/
public boolean isProbableFileReference() {
return elements.length == 0 ? false : elements[elements.length - 1].isProbableFileReference();
}
public void appendTo(StringBuilder sb) {
Checks.notNull("sb", sb);
for (int i = 0; i < elements.length; i++) {
if (i > 0) {
sb.append (URLBuilder.PATH_ELEMENT_DELIMITER);
}
elements[i].appendTo(sb, i == elements.length - 1);
}
}
public PathElement getElement(int ix) {
Checks.nonNegative("ix", ix);
return elements[ix];
}
public PathElement getLastElement() {
if (elements.length > 0) {
PathElement last = elements[elements.length - 1];
return last.toNonTrailingSlashElement();
} else {
return new PathElement("", true);
}
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Path other = (Path) obj;
return Arrays.equals(this.elements, other.elements);
}
@Override
public int hashCode() {
int hash = 7;
hash = 67 * hash + Arrays.deepHashCode(this.elements);
return hash;
}
public URI toURI() {
try {
return new URI(toString());
} catch (URISyntaxException ex) {
return Exceptions.chuck(ex);
}
}
public URI toURIWithLeadingSlash() {
try {
return new URI(toStringWithLeadingSlash());
} catch (URISyntaxException ex) {
return Exceptions.chuck(ex);
}
}
public String[] toStringArray() {
String[] result = new String[this.size()];
for (int i = 0; i < result.length; i++) {
result[i] = getElement(i).toString();
}
return result;
}
private static final class PathBuilder extends AbstractBuilder<PathElement, Path> {
@Override
public Path create() {
PathElement[] elements = new PathElement[size()];
elements = elements().toArray(elements);
return new Path (elements);
}
@Override
protected PathElement createElement(String element) {
Checks.notNull("string", element);
return new PathElement(element, false);
}
protected PathElement createElementWithTrailingSlash(String element) {
Checks.notNull("string", element);
return new PathElement(element, true);
}
}
}