/**
* <copyright>
*
* Copyright (c) 2002-2006 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM - Initial API and implementation
*
* </copyright>
*/
package net.enilink.komma.core;
import static net.enilink.komma.core.URIs.*;
import java.io.File;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* An implementation of the {@link URI} interface.
*/
final class URIImpl implements URI {
// Common to all URI types.
private final int hashCode;
private final boolean hierarchical;
private final String scheme; // null -> relative URI reference
private final String authority;
private final String fragment;
private URIImpl cachedNamespace;
private URIImpl cachedTrimFragment;
private String cachedToString;
// private final boolean Iri;
// private URI cachedASCIIURI;
// Applicable only to a hierarchical URI.
private final String device;
private final boolean absolutePath;
private final String[] segments; // empty last segment -> trailing separator
private final String query;
// Package protected constructor for use of static factory methods.
URIImpl(boolean hierarchical, String scheme, String authority,
String device, boolean absolutePath, String[] segments,
String query, String fragment) {
int hashCode = 0;
// boolean Iri = false;
if (hierarchical) {
++hashCode;
}
if (absolutePath) {
hashCode += 2;
}
if (scheme != null) {
hashCode ^= scheme.toLowerCase().hashCode();
}
if (authority != null) {
hashCode ^= authority.hashCode();
// Iri = Iri || containsNonASCII(authority);
}
if (device != null) {
hashCode ^= device.hashCode();
// Iri = Iri || containsNonASCII(device);
}
if (query != null) {
hashCode ^= query.hashCode();
// Iri = Iri || containsNonASCII(query);
}
if (fragment != null) {
hashCode ^= fragment.hashCode();
// Iri = Iri || containsNonASCII(fragment);
}
for (int i = 0, len = segments.length; i < len; i++) {
hashCode ^= segments[i].hashCode();
// Iri = Iri || containsNonASCII(segments[i]);
}
this.hashCode = hashCode;
// this.Iri = Iri;
this.hierarchical = hierarchical;
this.scheme = scheme == null ? null : scheme.intern();
this.authority = authority;
this.device = device;
this.absolutePath = absolutePath;
this.segments = segments;
this.query = query;
this.fragment = fragment;
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#isRelative()
*/
public boolean isRelative() {
return scheme == null;
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#isHierarchical()
*/
public boolean isHierarchical() {
return hierarchical;
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#hasAuthority()
*/
public boolean hasAuthority() {
return hierarchical && authority != null;
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#hasOpaquePart()
*/
public boolean hasOpaquePart() {
// note: hierarchical -> authority != null
return !hierarchical;
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#hasDevice()
*/
public boolean hasDevice() {
// note: device != null -> hierarchical
return device != null;
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#hasPath()
*/
public boolean hasPath() {
// note: (absolutePath || authority == null) -> hierarchical
// (authority == null && device == null && !absolutePath) -> scheme ==
// null
return absolutePath || (authority == null && device == null);
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#hasAbsolutePath()
*/
public boolean hasAbsolutePath() {
// note: absolutePath -> hierarchical
return absolutePath;
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#hasRelativePath()
*/
public boolean hasRelativePath() {
// note: authority == null -> hierarchical
// (authority == null && device == null && !absolutePath) -> scheme ==
// null
return authority == null && device == null && !absolutePath;
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#hasEmptyPath()
*/
public boolean hasEmptyPath() {
// note: authority == null -> hierarchical
// (authority == null && device == null && !absolutePath) -> scheme ==
// null
return authority == null && device == null && !absolutePath
&& segments.length == 0;
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#hasQuery()
*/
public boolean hasQuery() {
// note: query != null -> hierarchical
return query != null;
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#hasFragment()
*/
public boolean hasFragment() {
return fragment != null;
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#isCurrentDocumentReference()
*/
public boolean isCurrentDocumentReference() {
// note: authority == null -> hierarchical
// (authority == null && device == null && !absolutePath) -> scheme ==
// null
return authority == null && device == null && !absolutePath
&& segments.length == 0 && query == null;
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#isEmpty()
*/
public boolean isEmpty() {
// note: authority == null -> hierarchical
// (authority == null && device == null && !absolutePath) -> scheme ==
// null
return authority == null && device == null && !absolutePath
&& segments.length == 0 && query == null && fragment == null;
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#isFile()
*/
public boolean isFile() {
return isHierarchical()
&& ((isRelative() && !hasQuery()) || SCHEME_FILE
.equalsIgnoreCase(scheme));
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#isPlatform()
*/
public boolean isPlatform() {
return isHierarchical() && !hasAuthority() && segmentCount() >= 2
&& SCHEME_PLATFORM.equalsIgnoreCase(scheme);
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#isPlatformResource()
*/
public boolean isPlatformResource() {
return isPlatform() && "resource".equals(segments[0]);
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#isPlatformPlugin()
*/
public boolean isPlatformPlugin() {
return isPlatform() && "plugin".equals(segments[0]);
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#isArchive()
*/
public boolean isArchive() {
return isArchiveScheme(scheme);
}
/**
* Returns the hash code.
*/
@Override
public int hashCode() {
return hashCode;
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#equals(java.lang.Object)
*/
@Override
public boolean equals(Object object) {
if (this == object)
return true;
if (object instanceof IReference) {
object = ((IReference) object).getURI();
}
if (!(object instanceof URIImpl))
return false;
URIImpl uri = (URIImpl) object;
return hashCode == uri.hashCode()
&& hierarchical == uri.isHierarchical()
&& absolutePath == uri.hasAbsolutePath()
&& equals(scheme, uri.scheme(), true)
&& equals(authority,
hierarchical ? uri.authority() : uri.opaquePart())
&& equals(device, uri.device()) && equals(query, uri.query())
&& equals(fragment, uri.fragment()) && segmentsEqual(uri);
}
// Tests whether this URI's path segment array is equal to that of the
// given uri.
private boolean segmentsEqual(URI uri) {
if (segments.length != uri.segmentCount())
return false;
for (int i = 0, len = segments.length; i < len; i++) {
if (!segments[i].equals(uri.segment(i)))
return false;
}
return true;
}
// Tests two objects for equality, tolerating nulls; null is considered
// to be a valid value that is only equal to itself.
private static boolean equals(Object o1, Object o2) {
return o1 == null ? o2 == null : o1.equals(o2);
}
// Tests two strings for equality, tolerating nulls and optionally
// ignoring case.
private static boolean equals(String s1, String s2, boolean ignoreCase) {
return s1 == null ? s2 == null : ignoreCase ? s1.equalsIgnoreCase(s2)
: s1.equals(s2);
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#scheme()
*/
public String scheme() {
return scheme;
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#opaquePart()
*/
public String opaquePart() {
return isHierarchical() ? null : authority;
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#authority()
*/
public String authority() {
return isHierarchical() ? authority : null;
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#userInfo()
*/
public String userInfo() {
if (!hasAuthority())
return null;
int i = authority.indexOf(USER_INFO_SEPARATOR);
return i < 0 ? null : authority.substring(0, i);
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#host()
*/
public String host() {
if (!hasAuthority())
return null;
int i = authority.indexOf(USER_INFO_SEPARATOR);
int j = authority.indexOf(PORT_SEPARATOR);
return j < 0 ? authority.substring(i + 1) : authority.substring(i + 1,
j);
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#port()
*/
public String port() {
if (!hasAuthority())
return null;
int i = authority.indexOf(PORT_SEPARATOR);
return i < 0 ? null : authority.substring(i + 1);
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#device()
*/
public String device() {
return device;
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#segments()
*/
public String[] segments() {
return segments.clone();
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#segmentsList()
*/
public List<String> segmentsList() {
return Collections.unmodifiableList(Arrays.asList(segments));
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#segmentCount()
*/
public int segmentCount() {
return segments.length;
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#segment(int)
*/
public String segment(int i) {
return segments[i];
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#lastSegment()
*/
public String lastSegment() {
int len = segments.length;
if (len == 0)
return null;
return segments[len - 1];
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#path()
*/
public String path() {
if (!hasPath())
return null;
StringBuffer result = new StringBuffer();
if (hasAbsolutePath())
result.append(SEGMENT_SEPARATOR);
for (int i = 0, len = segments.length; i < len; i++) {
if (i != 0)
result.append(SEGMENT_SEPARATOR);
result.append(segments[i]);
}
return result.toString();
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#devicePath()
*/
public String devicePath() {
if (!hasPath())
return null;
StringBuffer result = new StringBuffer();
if (hasAuthority()) {
if (!isArchive())
result.append(AUTHORITY_SEPARATOR);
result.append(authority);
if (hasDevice())
result.append(SEGMENT_SEPARATOR);
}
if (hasDevice())
result.append(device);
if (hasAbsolutePath())
result.append(SEGMENT_SEPARATOR);
for (int i = 0, len = segments.length; i < len; i++) {
if (i != 0)
result.append(SEGMENT_SEPARATOR);
result.append(segments[i]);
}
return result.toString();
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#query()
*/
public String query() {
return query;
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#appendQuery(java.lang.String)
*/
public URI appendQuery(String query) {
if (!validQuery(query)) {
throw new IllegalArgumentException("invalid query portion: "
+ query);
}
return new URIImpl(hierarchical, scheme, authority, device,
absolutePath, segments, query, fragment);
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#trimQuery()
*/
public URI trimQuery() {
if (query == null) {
return this;
} else {
return new URIImpl(hierarchical, scheme, authority, device,
absolutePath, segments, null, fragment);
}
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#fragment()
*/
public String fragment() {
return fragment;
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#appendFragment(java.lang.String)
*/
public URIImpl appendFragment(String fragment) {
if (!validFragment(fragment)) {
throw new IllegalArgumentException("invalid fragment portion: "
+ fragment);
}
URIImpl result = new URIImpl(hierarchical, scheme, authority, device,
absolutePath, segments, query, encodeFragment(fragment, true));
if (!hasFragment()) {
result.cachedTrimFragment = this;
}
return result;
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#trimFragment()
*/
public URIImpl trimFragment() {
if (fragment == null) {
return this;
} else if (cachedTrimFragment == null) {
cachedTrimFragment = new URIImpl(hierarchical, scheme, authority,
device, absolutePath, segments, query, null);
}
return cachedTrimFragment;
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#namespace()
*/
public URIImpl namespace() {
if (cachedNamespace != null) {
return cachedNamespace;
}
if (fragment != null) {
if (fragment.length() == 0) {
cachedNamespace = this;
} else {
cachedNamespace = new URIImpl(hierarchical, scheme, authority,
device, absolutePath, segments, query, "");
}
} else if (opaquePart() != null) {
String opaquePart = opaquePart();
int colonIndex = opaquePart.lastIndexOf(':');
if (colonIndex >= 0) {
cachedNamespace = new URIImpl(false, scheme,
opaquePart.substring(0, colonIndex + 1), null,
absolutePath, NO_SEGMENTS, null, null);
} else {
cachedNamespace = this;
}
} else {
cachedNamespace = trimSegments(1).appendSegment("");
}
return cachedNamespace;
}
public URI appendLocalPart(String localPart) {
if (!isHierarchical() && toString().endsWith(":")) {
return createURI(toString() + localPart, true);
}
String last = lastSegment();
if (last == null && !hasAbsolutePath() || last != null
&& last.length() > 0) {
return appendFragment(localPart);
}
return trimSegments(1).appendSegment(encodeSegment(localPart, true));
}
@Override
public String localPart() {
String localName = fragment();
if (localName != null) {
return localName;
}
localName = lastSegment();
if (localName != null) {
return localName;
}
localName = toString();
int separatorIndex = localName.lastIndexOf(':');
if (separatorIndex >= 0) {
localName = localName.substring(separatorIndex + 1);
}
return localName;
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#resolve(net.enilink.komma
* .common.util.URI)
*/
public URIImpl resolve(URI base) {
return resolve(base, true);
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#resolve(net.enilink.komma
* .common.util.URI, boolean)
*/
public URIImpl resolve(URI base, boolean preserveRootParents) {
if (!base.isHierarchical() || base.isRelative()) {
throw new IllegalArgumentException(
"resolve against non-hierarchical or relative base");
}
// an absolute URI needs no resolving
if (!isRelative())
return this;
// note: isRelative() -> hierarchical
String newAuthority = authority;
String newDevice = device;
boolean newAbsolutePath = absolutePath;
String[] newSegments = segments;
String newQuery = query;
// note: it's okay for two URIs to share a segments array, since
// neither will ever modify it
if (authority == null) {
// no authority: use base's
newAuthority = base.authority();
if (device == null) {
// no device: use base's
newDevice = base.device();
if (hasEmptyPath() && query == null) {
// current document reference: use base path and query
newAbsolutePath = base.hasAbsolutePath();
newSegments = base.segments();
newQuery = base.query();
} else if (hasRelativePath()) {
// relative path: merge with base and keep query (note: if
// the
// base has no path and this a non-empty relative path,
// there is
// an implied root in the resulting path)
newAbsolutePath = base.hasAbsolutePath() || !hasEmptyPath();
newSegments = newAbsolutePath ? mergePath(base,
preserveRootParents) : NO_SEGMENTS;
}
// else absolute path: keep it and query
}
// else keep device, path, and query
}
// else keep authority, device, path, and query
// always keep fragment, even if null, and use scheme from base;
// no validation needed since all components are from existing URIs
return new URIImpl(true, base.scheme(), newAuthority, newDevice,
newAbsolutePath, newSegments, newQuery, fragment);
}
// Merges this URI's relative path with the base non-relative path. If
// base has no path, treat it as the root absolute path, unless this has
// no path either.
private String[] mergePath(URI base, boolean preserveRootParents) {
if (base.hasRelativePath()) {
throw new IllegalArgumentException("merge against relative path");
}
if (!hasRelativePath()) {
throw new IllegalStateException("merge non-relative path");
}
int baseSegmentCount = base.segmentCount();
int segmentCount = segments.length;
String[] stack = new String[baseSegmentCount + segmentCount];
int sp = 0;
// use a stack to accumulate segments of base, except for the last
// (i.e. skip trailing separator and anything following it), and of
// relative path
for (int i = 0; i < baseSegmentCount - 1; i++) {
sp = accumulate(stack, sp, base.segment(i), preserveRootParents);
}
for (int i = 0; i < segmentCount; i++) {
sp = accumulate(stack, sp, segments[i], preserveRootParents);
}
// if the relative path is empty or ends in an empty segment, a parent
// reference, or a self reference, add a trailing separator to a
// non-empty path
if (sp > 0
&& (segmentCount == 0
|| SEGMENT_EMPTY.equals(segments[segmentCount - 1])
|| SEGMENT_PARENT.equals(segments[segmentCount - 1]) || SEGMENT_SELF
.equals(segments[segmentCount - 1]))) {
stack[sp++] = SEGMENT_EMPTY;
}
// return a correctly sized result
String[] result = new String[sp];
System.arraycopy(stack, 0, result, 0, sp);
return result;
}
// Adds a segment to a stack, skipping empty segments and self references,
// and interpreting parent references.
private static int accumulate(String[] stack, int sp, String segment,
boolean preserveRootParents) {
if (SEGMENT_PARENT.equals(segment)) {
if (sp == 0) {
// special care must be taken for a root's parent reference: it
// is
// either ignored or the symbolic reference itself is pushed
if (preserveRootParents)
stack[sp++] = segment;
} else {
// unless we're already accumulating root parent references,
// parent references simply pop the last segment descended
if (SEGMENT_PARENT.equals(stack[sp - 1]))
stack[sp++] = segment;
else
sp--;
}
} else if (!SEGMENT_EMPTY.equals(segment)
&& !SEGMENT_SELF.equals(segment)) {
// skip empty segments and self references; push everything else
stack[sp++] = segment;
}
return sp;
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#deresolve(net.enilink.komma
* .common.util.URIImpl)
*/
public URI deresolve(URI base) {
return deresolve(base, true, false, true);
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#deresolve(net.enilink.komma
* .common.util.URIImpl, boolean, boolean, boolean)
*/
public URI deresolve(URI base, boolean preserveRootParents,
boolean anyRelPath, boolean shorterRelPath) {
if (!base.isHierarchical() || base.isRelative()) {
throw new IllegalArgumentException(
"deresolve against non-hierarchical or relative base");
}
if (isRelative()) {
throw new IllegalStateException("deresolve relative URI");
}
// note: these assertions imply that neither this nor the base URI has a
// relative path; thus, both have either an absolute path or no path
// different scheme: need complete, absolute URI
if (!scheme.equalsIgnoreCase(base.scheme()))
return this;
// since base must be hierarchical, and since a non-hierarchical URI
// must have both scheme and opaque part, the complete absolute URI is
// needed to resolve to a non-hierarchical URI
if (!isHierarchical())
return this;
String newAuthority = authority;
String newDevice = device;
boolean newAbsolutePath = absolutePath;
String[] newSegments = segments;
String newQuery = query;
if (equals(authority, base.authority())
&& (hasDevice() || hasPath() || (!base.hasDevice() && !base
.hasPath()))) {
// matching authorities and no device or path removal
newAuthority = null;
if (equals(device, base.device()) && (hasPath() || !base.hasPath())) {
// matching devices and no path removal
newDevice = null;
// exception if (!hasPath() && base.hasPath())
if (!anyRelPath && !shorterRelPath) {
// user rejects a relative path: keep absolute or no path
} else if (hasPath() == base.hasPath() && segmentsEqual(base)
&& equals(query, base.query())) {
// current document reference: keep no path or query
newAbsolutePath = false;
newSegments = NO_SEGMENTS;
newQuery = null;
} else if (!hasPath() && !base.hasPath()) {
// no paths: keep query only
newAbsolutePath = false;
newSegments = NO_SEGMENTS;
}
// exception if (!hasAbsolutePath())
else if (hasCollapsableSegments(preserveRootParents)) {
// path form demands an absolute path: keep it and query
} else {
// keep query and select relative or absolute path based on
// length
String[] rel = findRelativePath(base, preserveRootParents);
if (anyRelPath || segments.length > rel.length) {
// user demands a relative path or the absolute path is
// longer
newAbsolutePath = false;
newSegments = rel;
}
// else keep shorter absolute path
}
}
// else keep device, path, and query
}
// else keep authority, device, path, and query
// always include fragment, even if null;
// no validation needed since all components are from existing URIs
return new URIImpl(true, null, newAuthority, newDevice,
newAbsolutePath, newSegments, newQuery, fragment);
}
// Returns true if the non-relative path includes segments that would be
// collapsed when resolving; false otherwise. If preserveRootParents is
// true, collapsible segments include any empty segments, except for the
// last segment, as well as and parent and self references. If
// preserveRootsParents is false, parent references are not collapsible if
// they are the first segment or preceded only by other parent
// references.
private boolean hasCollapsableSegments(boolean preserveRootParents) {
if (hasRelativePath()) {
throw new IllegalStateException(
"test collapsability of relative path");
}
for (int i = 0, len = segments.length; i < len; i++) {
String segment = segments[i];
if ((i < len - 1 && SEGMENT_EMPTY.equals(segment))
|| SEGMENT_SELF.equals(segment)
|| SEGMENT_PARENT.equals(segment)
&& (!preserveRootParents || (i != 0 && !SEGMENT_PARENT
.equals(segments[i - 1])))) {
return true;
}
}
return false;
}
// Returns the shortest relative path between the the non-relative path of
// the given base and this absolute path. If the base has no path, it is
// treated as the root absolute path.
private String[] findRelativePath(URI base, boolean preserveRootParents) {
if (base.hasRelativePath()) {
throw new IllegalArgumentException(
"find relative path against base with relative path");
}
if (!hasAbsolutePath()) {
throw new IllegalArgumentException(
"find relative path of non-absolute path");
}
// treat an empty base path as the root absolute path
String[] startPath = ((URIImpl) base)
.collapseSegments(preserveRootParents);
String[] endPath = segments;
// drop last segment from base, as in resolving
int startCount = startPath.length > 0 ? startPath.length - 1 : 0;
int endCount = endPath.length;
// index of first segment that is different between endPath and
// startPath
int diff = 0;
// if endPath is shorter than startPath, the last segment of endPath may
// not be compared: because startPath has been collapsed and had its
// last segment removed, all preceding segments can be considered non-
// empty and followed by a separator, while the last segment of endPath
// will either be non-empty and not followed by a separator, or just
// empty
for (int count = startCount < endCount ? startCount : endCount - 1; diff < count
&& startPath[diff].equals(endPath[diff]); diff++) {
// Empty statement.
}
int upCount = startCount - diff;
int downCount = endCount - diff;
// a single separator, possibly preceded by some parent reference
// segments, is redundant
if (downCount == 1 && SEGMENT_EMPTY.equals(endPath[endCount - 1])) {
downCount = 0;
}
// an empty path needs to be replaced by a single "." if there is no
// query, to distinguish it from a current document reference
if (upCount + downCount == 0) {
if (query == null)
return new String[] { SEGMENT_SELF };
return NO_SEGMENTS;
}
// return a correctly sized result
String[] result = new String[upCount + downCount];
Arrays.fill(result, 0, upCount, SEGMENT_PARENT);
System.arraycopy(endPath, diff, result, upCount, downCount);
return result;
}
// Collapses non-ending empty segments, parent references, and self
// references in a non-relative path, returning the same path that would
// be produced from the base hierarchical URI as part of a resolve.
String[] collapseSegments(boolean preserveRootParents) {
if (hasRelativePath()) {
throw new IllegalStateException("collapse relative path");
}
if (!hasCollapsableSegments(preserveRootParents))
return segments();
// use a stack to accumulate segments
int segmentCount = segments.length;
String[] stack = new String[segmentCount];
int sp = 0;
for (int i = 0; i < segmentCount; i++) {
sp = accumulate(stack, sp, segments[i], preserveRootParents);
}
// if the path is non-empty and originally ended in an empty segment, a
// parent reference, or a self reference, add a trailing separator
if (sp > 0
&& (SEGMENT_EMPTY.equals(segments[segmentCount - 1])
|| SEGMENT_PARENT.equals(segments[segmentCount - 1]) || SEGMENT_SELF
.equals(segments[segmentCount - 1]))) {
stack[sp++] = SEGMENT_EMPTY;
}
// return a correctly sized result
String[] result = new String[sp];
System.arraycopy(stack, 0, result, 0, sp);
return result;
}
/**
* Returns the string representation of this URI. For a generic,
* non-hierarchical URI, this looks like:
*
* <pre>
* scheme:opaquePart#fragment
* </pre>
*
* <p>
* For a hierarchical URI, it looks like:
*
* <pre>
* scheme://authority/device/pathSegment1/pathSegment2...?query#fragment
* </pre>
*
* <p>
* For an <a href="#archive_explanation">archive URI</a>, it's just:
*
* <pre>
* scheme:authority/pathSegment1/pathSegment2...?query#fragment
* </pre>
* <p>
* Of course, absent components and their separators will be omitted.
*/
@Override
public String toString() {
if (cachedToString == null) {
StringBuffer result = new StringBuffer();
if (!isRelative()) {
result.append(scheme);
result.append(SCHEME_SEPARATOR);
}
if (isHierarchical()) {
if (hasAuthority()) {
if (!isArchive())
result.append(AUTHORITY_SEPARATOR);
result.append(authority);
}
if (hasDevice()) {
result.append(SEGMENT_SEPARATOR);
result.append(device);
}
if (hasAbsolutePath())
result.append(SEGMENT_SEPARATOR);
for (int i = 0, len = segments.length; i < len; i++) {
if (i != 0)
result.append(SEGMENT_SEPARATOR);
result.append(segments[i]);
}
if (hasQuery()) {
result.append(QUERY_SEPARATOR);
result.append(query);
}
} else {
result.append(authority);
}
if (hasFragment()) {
result.append(FRAGMENT_SEPARATOR);
result.append(fragment);
}
cachedToString = result.toString();
}
return cachedToString;
}
// Returns a string representation of this URI for debugging, explicitly
// showing each of the components.
String toString(boolean includeSimpleForm) {
StringBuffer result = new StringBuffer();
if (includeSimpleForm)
result.append(toString());
result.append("\n hierarchical: ");
result.append(hierarchical);
result.append("\n scheme: ");
result.append(scheme);
result.append("\n authority: ");
result.append(authority);
result.append("\n device: ");
result.append(device);
result.append("\n absolutePath: ");
result.append(absolutePath);
result.append("\n segments: ");
if (segments.length == 0)
result.append("<empty>");
for (int i = 0, len = segments.length; i < len; i++) {
if (i > 0)
result.append("\n ");
result.append(segments[i]);
}
result.append("\n query: ");
result.append(query);
result.append("\n fragment: ");
result.append(fragment);
return result.toString();
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#toFileString()
*/
public String toFileString() {
if (!isFile())
return null;
StringBuffer result = new StringBuffer();
char separator = File.separatorChar;
if (hasAuthority()) {
result.append(separator);
result.append(separator);
result.append(authority);
if (hasDevice())
result.append(separator);
}
if (hasDevice())
result.append(device);
if (hasAbsolutePath())
result.append(separator);
for (int i = 0, len = segments.length; i < len; i++) {
if (i != 0)
result.append(separator);
result.append(segments[i]);
}
return decode(result.toString());
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#toPlatformString(boolean)
*/
public String toPlatformString(boolean decode) {
if (isPlatform()) {
StringBuffer result = new StringBuffer();
for (int i = 1, len = segments.length; i < len; i++) {
result.append('/').append(
decode ? decode(segments[i]) : segments[i]);
}
return result.toString();
}
return null;
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#appendSegment(java.lang.String)
*/
public URIImpl appendSegment(String segment) {
if (!validSegment(segment)) {
throw new IllegalArgumentException("invalid segment: " + segment);
}
if (!isHierarchical())
return this;
// absolute path or no path -> absolute path
boolean newAbsolutePath = !hasRelativePath();
int len = segments.length;
String[] newSegments = new String[len + 1];
System.arraycopy(segments, 0, newSegments, 0, len);
newSegments[len] = segment;
return new URIImpl(true, scheme, authority, device, newAbsolutePath,
newSegments, query, fragment);
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#appendSegments(java.lang.String [])
*/
public URIImpl appendSegments(String[] segments) {
if (!validSegments(segments)) {
String s = segments == null ? "invalid segments: null"
: "invalid segment: " + firstInvalidSegment(segments);
throw new IllegalArgumentException(s);
}
if (!isHierarchical())
return this;
// absolute path or no path -> absolute path
boolean newAbsolutePath = !hasRelativePath();
int len = this.segments.length;
int segmentsCount = segments.length;
String[] newSegments = new String[len + segmentsCount];
System.arraycopy(this.segments, 0, newSegments, 0, len);
System.arraycopy(segments, 0, newSegments, len, segmentsCount);
return new URIImpl(true, scheme, authority, device, newAbsolutePath,
newSegments, query, fragment);
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#trimSegments(int)
*/
public URIImpl trimSegments(int i) {
if (!isHierarchical() || i < 1)
return this;
String[] newSegments = NO_SEGMENTS;
int len = segments.length - i;
if (len > 0) {
newSegments = new String[len];
System.arraycopy(segments, 0, newSegments, 0, len);
}
return new URIImpl(true, scheme, authority, device, absolutePath,
newSegments, query, fragment);
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#hasTrailingPathSeparator()
*/
public boolean hasTrailingPathSeparator() {
return segments.length > 0
&& SEGMENT_EMPTY.equals(segments[segments.length - 1]);
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#fileExtension()
*/
public String fileExtension() {
int len = segments.length;
if (len == 0)
return null;
String lastSegment = segments[len - 1];
int i = lastSegment.lastIndexOf(FILE_EXTENSION_SEPARATOR);
return i < 0 ? null : lastSegment.substring(i + 1);
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#appendFileExtension(java.lang .String)
*/
public URI appendFileExtension(String fileExtension) {
if (!validSegment(fileExtension)) {
throw new IllegalArgumentException("invalid segment portion: "
+ fileExtension);
}
int len = segments.length;
if (len == 0)
return this;
String lastSegment = segments[len - 1];
if (SEGMENT_EMPTY.equals(lastSegment))
return this;
StringBuffer newLastSegment = new StringBuffer(lastSegment);
newLastSegment.append(FILE_EXTENSION_SEPARATOR);
newLastSegment.append(fileExtension);
String[] newSegments = new String[len];
System.arraycopy(segments, 0, newSegments, 0, len - 1);
newSegments[len - 1] = newLastSegment.toString();
// note: segments.length > 0 -> hierarchical
return new URIImpl(true, scheme, authority, device, absolutePath,
newSegments, query, fragment);
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#trimFileExtension()
*/
public URI trimFileExtension() {
int len = segments.length;
if (len == 0)
return this;
String lastSegment = segments[len - 1];
int i = lastSegment.lastIndexOf(FILE_EXTENSION_SEPARATOR);
if (i < 0)
return this;
String newLastSegment = lastSegment.substring(0, i);
String[] newSegments = new String[len];
System.arraycopy(segments, 0, newSegments, 0, len - 1);
newSegments[len - 1] = newLastSegment;
// note: segments.length > 0 -> hierarchical
return new URIImpl(true, scheme, authority, device, absolutePath,
newSegments, query, fragment);
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#isPrefix()
*/
public boolean isPrefix() {
return hierarchical
&& query == null
&& fragment == null
&& (hasTrailingPathSeparator() || (absolutePath && segments.length == 0));
}
/*
* (non-Javadoc)
*
* @see net.enilink.komma.core.URI#replacePrefix(net.enilink.core.URI,
* net.enilink.komma.core.URI)
*/
public URI replacePrefix(URI oldPrefix, URI newPrefix) {
if (!oldPrefix.isPrefix() || !newPrefix.isPrefix()) {
String which = oldPrefix.isPrefix() ? "new" : "old";
throw new IllegalArgumentException("non-prefix " + which + " value");
}
// Get what's left of the segments after trimming the prefix.
String[] tailSegments = getTailSegments(oldPrefix);
if (tailSegments == null)
return null;
// If the new prefix has segments, it is not the root absolute path,
// and we need to drop the trailing empty segment and append the tail
// segments.
String[] mergedSegments = tailSegments;
if (newPrefix.segmentCount() != 0) {
int segmentsToKeep = newPrefix.segmentCount() - 1;
mergedSegments = new String[segmentsToKeep + tailSegments.length];
System.arraycopy(newPrefix.segments(), 0, mergedSegments, 0,
segmentsToKeep);
if (tailSegments.length != 0) {
System.arraycopy(tailSegments, 0, mergedSegments,
segmentsToKeep, tailSegments.length);
}
}
// no validation needed since all components are from existing URIs
return new URIImpl(true, newPrefix.scheme(), newPrefix.authority(),
newPrefix.device(), newPrefix.hasAbsolutePath(),
mergedSegments, query, fragment);
}
// If this is a hierarchical URI reference and prefix is a prefix of it,
// returns the portion of the path remaining after that prefix has been
// trimmed; null otherwise.
private String[] getTailSegments(URI prefix) {
if (!prefix.isPrefix()) {
throw new IllegalArgumentException("non-prefix trim");
}
// Don't even consider it unless this is hierarchical and has scheme,
// authority, device and path absoluteness equal to those of the prefix.
if (!hierarchical || !equals(scheme, prefix.scheme(), true)
|| !equals(authority, prefix.authority())
|| !equals(device, prefix.device())
|| absolutePath != prefix.hasAbsolutePath()) {
return null;
}
// If the prefix has no segments, then it is the root absolute path, and
// we know this is an absolute path, too.
if (prefix.segmentCount() == 0)
return segments;
// This must have no fewer segments than the prefix. Since the prefix
// is not the root absolute path, its last segment is empty; all others
// must match.
int i = 0;
int segmentsToCompare = prefix.segmentCount() - 1;
if (segments.length <= segmentsToCompare)
return null;
for (; i < segmentsToCompare; i++) {
if (!segments[i].equals(prefix.segment(i)))
return null;
}
// The prefix really is a prefix of this. If this has just one more,
// empty segment, the paths are the same.
if (i == segments.length - 1 && SEGMENT_EMPTY.equals(segments[i])) {
return NO_SEGMENTS;
}
// Otherwise, the path needs only the remaining segments.
String[] newSegments = new String[segments.length - i];
System.arraycopy(segments, i, newSegments, 0, newSegments.length);
return newSegments;
}
/*
* Returns <code>true</code> if this URI contains non-ASCII characters;
* <code>false</code> otherwise.
*
* This unused code is included for possible future use...
*/
/*
* public boolean isIRI() { return Iri; }
*
* // Returns true if the given string contains any non-ASCII characters; //
* false otherwise. private static boolean containsNonASCII(String value) {
* for (int i = 0, length = value.length(); i < length; i++) { if
* (value.charAt(i) > 127) return true; } return false; }
*/
/*
* If this is an {@link #isIRI IRI}, converts it to a strict ASCII URI,
* using the procedure described in Section 3.1 of the <a
* href="http://www.w3.org/International/Iri-edit/draft-duerst-Iri-09.txt"
* >IRI Draft RFC</a>. Otherwise, this URI, itself, is returned.
*
* This unused code is included for possible future use...
*/
/*
* public URI toASCIIURI() { if (!Iri) return this;
*
* if (cachedASCIIURI == null) { String eAuthority =
* encodeAsASCII(authority); String eDevice = encodeAsASCII(device); String
* eQuery = encodeAsASCII(query); String eFragment =
* encodeAsASCII(fragment); String[] eSegments = new
* String[segments.length]; for (int i = 0; i < segments.length; i++) {
* eSegments[i] = encodeAsASCII(segments[i]); } cachedASCIIURI = new
* URI(hierarchical, scheme, eAuthority, eDevice, absolutePath, eSegments,
* eQuery, eFragment);
*
* } return cachedASCIIURI; }
*
* // Returns a strict ASCII encoding of the given value. Each non-ASCII //
* character is converted to bytes using UTF-8 encoding, which are then //
* represented using % escaping. private String encodeAsASCII(String value)
* { if (value == null) return null;
*
* StringBuffer result = null;
*
* for (int i = 0, length = value.length(); i < length; i++) { char c =
* value.charAt(i);
*
* if (c >= 128) { if (result == null) { result = new
* StringBuffer(value.substring(0, i)); }
*
* try { byte[] encoded = (new String(new char[] { c })).getBytes("UTF-8");
* for (int j = 0, encLen = encoded.length; j < encLen; j++) {
* appendEscaped(result, encoded[j]); } } catch
* (UnsupportedEncodingException e) { throw new WrappedException(e); } }
* else if (result != null) { result.append(c); }
*
* } return result == null ? value : result.toString(); }
*
* // Returns the number of valid, consecutive, three-character escape //
* sequences in the given string, starting at index i. private static int
* countEscaped(String s, int i) { int result = 0;
*
* for (int length = s.length(); i < length; i += 3) { if (isEscaped(s, i))
* result++; } return result; }
*/
@Override
public URI getURI() {
return this;
}
}