net.sourceforge.urin.Path Maven / Gradle / Ivy
Show all versions of urin Show documentation
/*
* Copyright 2024 Mark Slater
*
* Licensed 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
*
* http://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 net.sourceforge.urin;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static net.sourceforge.urin.Segment.*;
/**
* An iterable of {@code Segment}s.
* {@code Path}s can either be absolute (starting with '/'), or rootless (not starting with '/').
*
* Immutable and thread safe.
*
* @see RFC 3986 - Path
*/
public abstract class Path implements Iterable> {
Path() {
}
/**
* Factory method for creating rootless {@code Path}s from {@code String}s.
*
* @param firstSegment a {@code String} representing the first segment.
* @param segments any further segments.
* @return a {@code Path} representing the given {@code String}s.
*/
public static Path rootlessPath(final String firstSegment, final String... segments) {
final List> segmentList = new ArrayList<>(segments.length + 1);
segmentList.add(segment(firstSegment));
for (final String segment : segments) {
segmentList.add(segment(segment));
}
return rootlessPath(segmentList);
}
/**
* Factory method for creating empty rootless {@code Path}s.
*
* @param The type of value represented by the path segments - {@code String} in the general case.
* @return an empty {@code Path}.
*/
public static Path rootlessPath() {
return rootlessPath(emptyList());
}
/**
* Factory method for creating rootless {@code Path}s from {@code Segment}s.
*
* @param segments {@code Segment}s that will be represented by this {@code Path}.
* @param The type of value represented by the path segments - {@code String} in the general case.
* @return a {@code Path} representing the given {@code Segment}s.
*/
@SafeVarargs
public static Path rootlessPath(final Segment... segments) {
return rootlessPath(asList(segments));
}
/**
* Factory method for creating rootless {@code Path}s from an {@code Iterable} of {@code Segment}s.
*
* @param segments {@code Iterable} of {@code Segment}s that will be represented by this {@code Path}.
* @param The type of value represented by the path segments - {@code String} in the general case.
* @return a {@code Path} representing the given {@code Segment}s.
*/
public static Path rootlessPath(final Iterable> segments) {
if (segments.iterator().hasNext()) {
return new RootlessPath<>(segments);
} else {
return new EmptyPath<>();
}
}
/**
* Factory method for creating {@code AbsolutePath}s from {@code String}s.
*
* @param firstSegment a {@code String} representing the first segment.
* @param segments any further segments.
* @return a {@code AbsolutePath} representing the given {@code String}s.
*/
public static AbsolutePath path(final String firstSegment, final String... segments) {
final List> segmentList = new ArrayList<>(segments.length + 1);
segmentList.add(segment(firstSegment));
for (final String segment : segments) {
segmentList.add(segment(segment));
}
return path(segmentList);
}
/**
* Factory method for creating an empty {@code AbsolutePath}.
*
* @param The type of value represented by the path segments - {@code String} in the general case.
* @return an empty {@code AbsolutePath}.
*/
public static AbsolutePath path() {
return path(emptyList());
}
/**
* Factory method for creating {@code AbsolutePath}s from {@code Segment}s.
*
* @param segments {@code Segment}s that will be represented by this {@code AbsolutePath}.
* @param The type of value represented by the path segments - {@code String} in the general case.
* @return a {@code AbsolutePath} representing the given {@code Segment}s.
*/
@SafeVarargs
public static AbsolutePath path(final Segment... segments) {
return path(asList(segments));
}
/**
* Factory method for creating {@code AbsolutePath}s from an {@code Iterable} of {@code Segment}s.
*
* @param segments {@code Iterable} of {@code Segment}s that will be represented by this {@code AbsolutePath}.
* @param The type of value represented by the path segments - {@code String} in the general case.
* @return a {@code AbsolutePath} representing the given {@code Segment}s.
*/
public static AbsolutePath path(final Iterable> segments) {
return new AbsolutePath<>(segments);
}
static Path parseRootlessPath(final String rawPath, final MakingDecoder, ?, String> segmentMakingDecoder) throws ParseException {
if (rawPath == null || rawPath.isEmpty()) {
return rootlessPath(emptyList());
} else {
final String[] segmentStrings = rawPath.split("/");
final List> result = new ArrayList<>(segmentStrings.length);
for (final String segmentString : segmentStrings) {
result.add(parse(segmentString, segmentMakingDecoder));
}
return rootlessPath(result);
}
}
static AbsolutePath parsePath(final String rawPath, final MakingDecoder, ?, String> segmentMakingDecoder) throws ParseException {
if ("/".equals(rawPath)) {
return path(emptyList());
} else {
final String[] segmentStrings = rawPath.split("/", -1);
final List> result = new ArrayList<>(segmentStrings.length - 1);
boolean isFirst = true;
for (final String segmentString : segmentStrings) {
if (!isFirst) {
result.add(parse(segmentString, segmentMakingDecoder));
}
isFirst = false;
}
return path(result);
}
}
static Deque> normaliseRootless(final Iterable> segments) { // TODO consider whether we should normalise by effect, e.g. the relative references . and ./ are equivalent, as are .. and ../ (we'd call the latter of each "empty")
final Deque> normalisedSegments = new LinkedList<>();
for (final Segment next : segments) {
if (normalisedSegments.isEmpty()) {
normalisedSegments.add(next);
} else {
Segment current = normalisedSegments.removeLast();
if (dot().equals(current) && !normalisedSegments.isEmpty()) {
current = normalisedSegments.removeLast();
}
normalisedSegments.addAll(current.incorporate(next));
}
}
if (normalisedSegments.size() > 1 && dot().equals(normalisedSegments.getLast())) {
normalisedSegments.removeLast();
normalisedSegments.add(empty());
}
return normalisedSegments;
}
abstract boolean firstPartIsSuppliedButIsEmpty();
abstract String asString(PrefixWithDotSegmentCriteria prefixWithDotSegmentCriteria);
abstract boolean isEmpty();
abstract boolean firstPartIsSuppliedButContainsColon();
abstract Path resolveRelativeTo(Path basePath);
abstract Path replaceLastSegmentWith(Iterable> segments);
/**
* Indicates whether this path is absolute (begins with '/') or not.
*
* @return whether this path is absolute (begins with '/') or not.
*/
public abstract boolean isAbsolute();
/**
* Returns the list of {@code Segment}s that this path represents.
* Note that this {@code List} contains the same elements as returned by calling {@code iterator()} on {@code this}.
*
* @return the list of {@code Segment}s that this path represents.
*/
public abstract List> segments();
enum PrefixWithDotSegmentCriteria {
NEVER_PREFIX_WITH_DOT_SEGMENT {
@Override
boolean matches(final Path> path) {
return false;
}
},
PREFIX_WITH_DOT_SEGMENT_IF_FIRST_CONTAINS_COLON {
@Override
boolean matches(final Path> path) {
return path.firstPartIsSuppliedButContainsColon();
}
},
PREFIX_WITH_DOT_SEGMENT_IF_FIRST_IS_EMPTY {
@Override
boolean matches(final Path> path) {
return path.firstPartIsSuppliedButIsEmpty();
}
},
PREFIX_WITH_DOT_SEGMENT_IF_FIRST_IS_EMPTY_OR_CONTAINS_COLON {
@Override
boolean matches(final Path> path) {
return path.firstPartIsSuppliedButIsEmpty() || path.firstPartIsSuppliedButContainsColon();
}
};
abstract boolean matches(Path> path);
}
}