net.sourceforge.urin.Segment 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.List;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static net.sourceforge.urin.CharacterSetMembershipFunction.P_CHAR;
import static net.sourceforge.urin.PercentEncodingPartial.PercentEncoding.percentEncodingString;
import static net.sourceforge.urin.PercentEncodingPartial.PercentEncoding.specifiedValueEncoding;
/**
* A segment of a URI's path.
*
* Note that the special segments "{@code .}" and "{@code ..}" are obtained via the factory methods {@link #dot()} and {@link #dotDot()}
* respectively. Passing "{@code .}" or "{@code ..}" as an argument to the factory method {@link #segment(String)} is not equivalent,
* as the argument to this method is a literal string, i.e. subject to encoding where necessary.
*
* Immutable and thread safe.
*
* @param The type of value represented by the segment - {@code String} in the general case.
* @see RFC 3986 - Path
*/
public abstract class Segment {
/**
* The {@code MakingDecoder} used by standard segments.
*/
public static final MakingDecoder, String, String> STRING_SEGMENT_MAKING_DECODER = new MakingDecoder, String, String>(PercentEncodingPartial.noOp()) {
@Override
protected Segment makeOne(final String value) {
return segment(value);
}
};
private static final PercentEncodingPartial.PercentEncoding PERCENT_ENCODING = specifiedValueEncoding(".",
specifiedValueEncoding("..",
percentEncodingString(new PercentEncoder(P_CHAR))));
private Segment() {
}
/**
* The segment "{@code .}", referring to the current location in the path name hierarchy.
*
* @param The type of value represented by the segment - {@code String} in the general case.
* @return The segment "{@code .}", referring to the current location in the path name hierarchy
*/
public static Segment dot() {
return new Segment() {
@Override
public boolean hasValue() {
return false;
}
@Override
public ENCODES value() {
throw new UnsupportedOperationException("Attempt to get value of . segment");
}
@Override
List> incorporate(final Segment next) {
return singletonList(next);
}
@Override
String asString() {
return ".";
}
@Override
boolean isEmpty() {
return false;
}
@Override
public boolean equals(final Object object) {
return this == object || !(object == null || getClass() != object.getClass());
}
@Override
public int hashCode() {
return 37;
}
@Override
public String toString() {
return "Segment{.}";
}
};
}
/**
* The segment "{@code ..}", referring to the parent location in the path name hierarchy.
*
* @param The type of value represented by the segment - {@code String} in the general case.
* @return The segment "{@code ..}", referring to the current location in the path name hierarchy
*/
public static Segment dotDot() {
return new Segment() {
@Override
public boolean hasValue() {
return false;
}
@Override
public ENCODES value() {
throw new UnsupportedOperationException("Attempt to get value of .. segment");
}
@Override
List> incorporate(final Segment next) {
return asList(this, next);
}
@Override
String asString() {
return "..";
}
@Override
boolean isEmpty() {
return false;
}
@Override
public boolean equals(final Object object) {
return this == object || !(object == null || getClass() != object.getClass());
}
@Override
public int hashCode() {
return 17;
}
@Override
public String toString() {
return "Segment{..}";
}
};
}
/**
* An empty segment - one that is encoded as "" in a URI.
*
* @param The type of value represented by the segment - {@code String} in the general case.
* @return The empty segment - one that is encoded as "" in a URI
*/
public static Segment empty() {
return new Segment() {
@Override
public boolean hasValue() {
return false;
}
@Override
public ENCODES value() {
throw new UnsupportedOperationException("Attempt to get value of empty segment");
}
@Override
List> incorporate(final Segment next) {
if (dotDot().equals(next)) {
return singletonList(dot());
} else {
return asList(this, next);
}
}
@Override
String asString() {
return "";
}
@Override
boolean isEmpty() {
return true;
}
@Override
public boolean equals(final Object object) {
return this == object || !(object == null || getClass() != object.getClass());
}
@Override
public int hashCode() {
return 19;
}
@Override
public String toString() {
return "Segment{empty}";
}
};
}
/**
* Factory method for creating {@code Segment}s.
*
* @param segment any {@code String} to represent as a {@code Segment}.
* @return a {@code Segment} representing the given {@code String}.
*/
public static Segment segment(final String segment) {
return segment(segment, PercentEncodingPartial.noOp());
}
/**
* Factory method for creating non-{@code String} {@code Segment}s.
*
* @param the type of {@code Object} the {@code Segment} encodes.
* @param segment any {@code Object} to represent as a {@code Segment}.
* @param percentEncodingPartial an encoding from {@code T} to {@code String}.
* @return a {@code Segment} representing the given {@code Object}.
*/
public static Segment segment(final T segment, final PercentEncodingPartial percentEncodingPartial) {
final ValueSegment result = new ValueSegment<>(segment, percentEncodingPartial);
return result.isEmpty() ? empty() : result; // TODO I think this is unhelpful except for a trailing segment as e.g. .//b/c is hard to get values from
}
static Segment parse(final String encodedSegment, final MakingDecoder, ?, String> segmentMakingDecoder) throws ParseException {
switch (encodedSegment) {
case "":
return empty();
case ".":
return dot();
case "..":
return dotDot();
default:
return segmentMakingDecoder.toMaker(PERCENT_ENCODING).make(encodedSegment);
}
}
final boolean containsColon() {
return asString().indexOf(':') != -1;
}
abstract String asString();
abstract boolean isEmpty();
/**
* Returns true if {@code value()} can be called on this {@code Segment}. This method
* returns false for empty, {@code .} and {@code ..} segments.
*
* @return true if {@code value()} can be called on this {@code Segment}.
*/
public abstract boolean hasValue();
/**
* Gets the (non-encoded) value of this segment, if it is a type that has a value, or throws {@code UnsupportedOperationException} otherwise.
*
* Dot segments ({@code .} and {@code ..}) and the empty segment do not have values, and will throw {@code UnsupportedOperationException}.
* This can be tested by equality with the objects returned by {@link #dot()}, {@link #dotDot()}, and {@link #empty()} methods, or by calling {@code hasValue()}.
*
* @return the (non-encoded) value of this segment.
* @throws UnsupportedOperationException if this is a segment that does not represent a value.
*/
public abstract ENCODES value();
abstract List> incorporate(Segment next);
private static final class ValueSegment extends Segment { // TODO could this extend empty? Or a common superclass? They share an implementation of incorporate
private final PercentEncodingUnaryValue delegate;
private ValueSegment(final ENCODES value, final PercentEncodingPartial percentEncodingPartial) {
final PercentEncodingPartial.PercentEncoding apply = percentEncodingPartial.apply(PERCENT_ENCODING);
this.delegate = new SegmentEncodingUnaryValue<>(value, apply);
}
@Override
public boolean hasValue() {
return true;
}
@Override
public ENCODES value() {
return delegate.value;
}
@Override
List> incorporate(final Segment next) {
if (dotDot().equals(next)) {
return singletonList(dot());
} else {
return asList(this, next);
}
}
@Override
String asString() {
return delegate.asString();
}
@Override
boolean isEmpty() {
return "".equals(asString());
}
@Override
public boolean equals(final Object object) {
if (this == object) {
return true;
} else if (object == null || getClass() != object.getClass()) {
return false;
}
final ValueSegment> that = (ValueSegment>) object;
return delegate.equals(that.delegate);
}
@Override
public int hashCode() {
return delegate.hashCode();
}
@Override
public String toString() {
return "Segment{value='" + delegate + "'}";
}
}
private static final class SegmentEncodingUnaryValue extends PercentEncodingUnaryValue {
SegmentEncodingUnaryValue(final ENCODES value, final PercentEncodingPartial.PercentEncoding percentEncoding) {
super(value, percentEncoding);
}
}
}