TemplateVariable.java
/*
* Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package org.glassfish.jersey.uri.internal;
import org.glassfish.jersey.uri.UriComponent;
import java.util.Collection;
import java.util.Map;
/**
* The Reserved Expansion template variable representation as per RFC6570.
*/
/* package */ class TemplateVariable extends UriPart {
protected final Position position;
protected int len = -1; // unlimited
protected boolean star = false;
TemplateVariable(String part, Position position) {
super(part);
this.position = position;
}
/**
* Choose the template variable type. The
* @param type Type of the template
* @param part the template content
* @param position the position of the variable in the template.
* @return Subclass of Templatevariable to represent the variable and allowing expansion based on the type of the variable
*/
static TemplateVariable createTemplateVariable(char type, String part, Position position) {
TemplateVariable newType;
switch (type) {
case '+':
newType = new TemplateVariable(part, position);
break;
case '-': // Not supported by RFC
newType = new MinusTemplateVariable(part, position);
break;
case '#':
newType = new HashTemplateVariable(part, position);
break;
case '.':
newType = new DotTemplateVariable(part, position);
break;
case '/':
newType = new SlashTemplateVariable(part, position);
break;
case ';':
newType = new MatrixTemplateVariable(part, position);
break;
case '?':
newType = new QueryTemplateVariable(part, position);
break;
case '&':
newType = new QueryContinuationTemplateVariable(part, position);
break;
default:
//'p'
newType = new PathTemplateVariable(part, position);
break;
}
return newType;
}
@Override
public boolean isTemplate() {
return true;
}
@Override
public String getGroup() {
StringBuilder sb = new StringBuilder();
if (position.isFirst()) {
sb.append('{');
} else {
sb.append(',');
}
sb.append(getPart());
if (position.isLast()) {
sb.append('}');
}
return sb.toString();
}
@Override
public String resolve(Object value, UriComponent.Type type, boolean encode) {
if (value == null) {
return "";
}
return position.isFirst()
? plainResolve(value, type, encode)
: separator() + plainResolve(value, type, encode);
}
protected char separator() {
return ',';
}
protected char keyValueSeparator() {
return star ? '=' : ',';
}
protected String plainResolve(Object value, UriComponent.Type componentType, boolean encode) {
if (Collection.class.isInstance(value)) {
return ((Collection<Object>) value).stream()
.map(a -> plainResolve(a, componentType, encode))
.reduce("", (a, b) -> a + (a.isEmpty() ? b : separator() + b));
} else if (Map.class.isInstance(value)) {
return ((Map<?, ?>) value).entrySet().stream()
.map(e -> plainResolve(e.getKey(), componentType, encode)
+ keyValueSeparator()
+ plainResolve(e.getValue(), componentType, encode))
.reduce("", (a, b) -> a + (a.isEmpty() ? b : separator() + b));
} else {
return plainResolve(value.toString(), componentType, encode);
}
}
protected String plainResolve(String value, UriComponent.Type componentType, boolean encode) {
String val = len == -1 ? value : value.substring(0, Math.min(value.length(), len));
return encode(val, componentType, encode);
}
protected String encode(String toEncode, UriComponent.Type componentType, boolean encode) {
if (componentType == null) {
componentType = getDefaultType();
}
return UriPart.percentEncode(toEncode, componentType, encode);
}
protected UriComponent.Type getDefaultType() {
return UriComponent.Type.PATH;
}
void setLength(int len) {
this.len = len;
}
void setStar(boolean b) {
star = b;
}
/**
* The default UriBuilder template
*/
private static class PathTemplateVariable extends TemplateVariable {
protected PathTemplateVariable(String part, Position position) {
super(part, position);
}
@Override
public boolean throwWhenNoTemplateArg() {
return true; // The default UriBuilder behaviour
}
@Override
protected UriComponent.Type getDefaultType() {
return UriComponent.Type.PATH;
}
}
/**
* The template that works according to RFC 6570, Section 3.2.2.
* The default Path works as described in Section 3.2.3, as described by RFC 3986.
*/
private static class MinusTemplateVariable extends TemplateVariable {
protected MinusTemplateVariable(String part, Position position) {
super(part, position);
}
@Override
protected String encode(String toEncode, UriComponent.Type componentType, boolean encode) {
return super.encode(toEncode, UriComponent.Type.QUERY, encode); //Query has the same encoding as Section 3.2.3
}
@Override
protected UriComponent.Type getDefaultType() {
return UriComponent.Type.QUERY;
}
}
/**
* Section 3.2.5
*/
private static class DotTemplateVariable extends MinusTemplateVariable {
protected DotTemplateVariable(String part, Position position) {
super(part, position);
}
@Override
public String resolve(Object value, UriComponent.Type type, boolean encode) {
if (value == null) {
return "";
}
return '.' + plainResolve(value, type, encode);
}
@Override
protected char separator() {
return star ? '.' : super.separator();
}
}
/**
* Section 3.2.6
*/
private static class SlashTemplateVariable extends MinusTemplateVariable {
protected SlashTemplateVariable(String part, Position position) {
super(part, position);
}
@Override
public String resolve(Object value, UriComponent.Type type, boolean encode) {
if (value == null) {
return "";
}
return '/' + plainResolve(value, type, encode);
}
@Override
protected char separator() {
return star ? '/' : super.separator();
}
}
/**
* Section 3.2.4
*/
private static class HashTemplateVariable extends TemplateVariable {
protected HashTemplateVariable(String part, Position position) {
super(part, position);
}
@Override
public String resolve(Object value, UriComponent.Type type, boolean encode) {
return (value == null || !position.isFirst() ? "" : "#") + super.resolve(value, type, encode);
}
@Override
protected UriComponent.Type getDefaultType() {
return UriComponent.Type.PATH;
}
}
private abstract static class ExtendedVariable extends TemplateVariable {
private final Character firstSymbol;
private final char separator;
protected final boolean appendEmpty;
protected ExtendedVariable(String part, Position position, Character firstSymbol, char separator, boolean appendEmpty) {
super(part, position);
this.firstSymbol = firstSymbol;
this.separator = separator;
this.appendEmpty = appendEmpty;
}
@Override
public String resolve(Object value, UriComponent.Type componentType, boolean encode) {
if (value == null) { // RFC 6570
return "";
}
String sValue = super.plainResolve(value, componentType, encode);
StringBuilder sb = new StringBuilder();
if (position.isFirst()) {
sb.append(firstSymbol);
} else {
sb.append(separator);
}
if (!star) {
sb.append(getPart());
if (appendEmpty || !sValue.isEmpty()) {
sb.append('=').append(sValue);
}
} else if (!Map.class.isInstance(value)) {
String[] split = sValue.split(String.valueOf(separator()));
for (int i = 0; i != split.length; i++) {
sb.append(getPart());
sb.append('=').append(split[i]);
if (i != split.length - 1) {
sb.append(separator);
}
}
} else if (Map.class.isInstance(value)) {
sb.append(sValue);
}
return sb.toString();
}
@Override
protected char separator() {
return star ? separator : super.separator();
}
}
/**
* Section 3.2.7
*/
private static class MatrixTemplateVariable extends ExtendedVariable {
protected MatrixTemplateVariable(String part, Position position) {
super(part, position, ';', ';', false);
}
@Override
protected UriComponent.Type getDefaultType() {
return UriComponent.Type.QUERY; // For matrix, use query encoding per 6570
}
@Override
public String resolve(Object value, UriComponent.Type componentType, boolean encode) {
return super.resolve(value, getDefaultType(), encode);
}
}
/**
* Section 3.2.8
*/
private static class QueryTemplateVariable extends ExtendedVariable {
protected QueryTemplateVariable(String part, Position position) {
super(part, position, '?', '&', true);
}
}
/**
* Section 3.2.9
*/
private static class QueryContinuationTemplateVariable extends ExtendedVariable {
protected QueryContinuationTemplateVariable(String part, Position position) {
super(part, position, '&', '&', true);
}
@Override
protected UriComponent.Type getDefaultType() {
return UriComponent.Type.QUERY;
}
@Override
public String resolve(Object value, UriComponent.Type componentType, boolean encode) {
return super.resolve(value, getDefaultType(), encode);
}
}
/**
* <p>
* Position of the template variable. For instance, template {@code {first, middle, last}} would have three arguments, on
* {@link Position#FIRST}, {@link Position#MIDDLE}, and {@link Position#LAST} positions.
* If only a single argument is in template (most common) e.g. {@code {single}}, the position is {@link Position#SINGLE}.
* </p>
* <p>
* {@link Position#SINGLE} is first (see {@link Position#isFirst()}) and last (see {@link Position#isLast()}) at the same time.
* </p>
*/
/* package */ static enum Position {
FIRST((byte) 0b1100),
MIDDLE((byte) 0b1010),
LAST((byte) 0b1001),
SINGLE((byte) 0b1111);
final byte val;
Position(byte val) {
this.val = val;
}
/**
* Informs whether the position of the argument is the last in the argument group.
* @return true when the argument is the last.
*/
boolean isLast() {
return (val & LAST.val) == LAST.val;
}
/**
* Informs whether the position of the argument is the first in the argument group.
* @return true when the argument is the first.
*/
boolean isFirst() {
return (val & FIRST.val) == FIRST.val;
}
}
}