package org.argeo.api.acr;
-/** When a countent was requested which does not exists, equivalent to HTTP code 404.*/
+/**
+ * When a content was requested which does not exists, equivalent to HTTP code
+ * 404.
+ */
public class ContentNotFoundException extends RuntimeException {
private static final long serialVersionUID = -8629074900713760886L;
- public ContentNotFoundException(String message, Throwable cause) {
- super(message, cause);
+ private final String path;
+
+ public ContentNotFoundException(ContentSession session, String path, Throwable cause) {
+ super(message(session, path), cause);
+ this.path = path;
+ // we don't keep reference to the session for security reasons
+ }
+
+ public ContentNotFoundException(ContentSession session, String path) {
+ this(session, path, (String) null);
}
- public ContentNotFoundException(String message) {
- super(message);
+ public ContentNotFoundException(ContentSession session, String path, String message) {
+ super(message != null ? message : message(session, path));
+ this.path = path;
+ // we don't keep reference to the session for security reasons
}
-
+ private static String message(ContentSession session, String path) {
+ return "Content " + path + "cannot be found.";
+ }
+
+ public String getPath() {
+ return path;
+ }
}
import java.io.IOException;
import java.io.Writer;
-import java.util.ArrayList;
-import java.util.List;
import javax.servlet.http.HttpServletResponse;
import org.argeo.api.cms.CmsLog;
+import org.argeo.util.ExceptionsChain;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
/** Serialisable wrapper of a {@link Throwable}. */
-public class CmsExceptionsChain {
+public class CmsExceptionsChain extends ExceptionsChain {
public final static CmsLog log = CmsLog.getLog(CmsExceptionsChain.class);
- private List<SystemException> exceptions = new ArrayList<>();
-
public CmsExceptionsChain() {
super();
}
public CmsExceptionsChain(Throwable exception) {
- writeException(exception);
+ super(exception);
if (log.isDebugEnabled())
log.error("Exception chain", exception);
}
}
}
- /** recursive */
- protected void writeException(Throwable exception) {
- SystemException systemException = new SystemException(exception);
- exceptions.add(systemException);
- Throwable cause = exception.getCause();
- if (cause != null)
- writeException(cause);
- }
-
- public List<SystemException> getExceptions() {
- return exceptions;
- }
-
- public void setExceptions(List<SystemException> exceptions) {
- this.exceptions = exceptions;
- }
-
- /** An exception in the chain. */
- public static class SystemException {
- private String type;
- private String message;
- private List<String> stackTrace;
-
- public SystemException() {
- }
-
- public SystemException(Throwable exception) {
- this.type = exception.getClass().getName();
- this.message = exception.getMessage();
- this.stackTrace = new ArrayList<>();
- StackTraceElement[] elems = exception.getStackTrace();
- for (int i = 0; i < elems.length; i++)
- stackTrace.add("at " + elems[i].toString());
- }
-
- public String getType() {
- return type;
- }
-
- public void setType(String type) {
- this.type = type;
- }
-
- public String getMessage() {
- return message;
- }
-
- public void setMessage(String message) {
- this.message = message;
- }
-
- public List<String> getStackTrace() {
- return stackTrace;
- }
-
- public void setStackTrace(List<String> stackTrace) {
- this.stackTrace = stackTrace;
- }
-
- @Override
- public String toString() {
- return "System exception: " + type + ", " + message + ", " + stackTrace;
- }
-
- }
-
- @Override
- public String toString() {
- return exceptions.toString();
- }
-
// public static void main(String[] args) throws Exception {
// try {
// try {
// throw new RuntimeException("Top exception", e);
// }
// } catch (Exception e) {
-// CmsExceptionsChain vjeSystemErrors = new CmsExceptionsChain(e);
+// CmsExceptionsChain systemErrors = new CmsExceptionsChain(e);
// ObjectMapper objectMapper = new ObjectMapper();
-// System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(vjeSystemErrors));
+// System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(systemErrors));
// System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(e));
// e.printStackTrace();
// }
}
}
if (userDirectory == null)
- throw new ContentNotFoundException("Cannot find user directory " + userDirectoryName);
+ throw new ContentNotFoundException(session, mountPath + "/" + relativePath,
+ "Cannot find user directory " + userDirectoryName);
if (segments.size() == 1) {
return new DirectoryContent(session, this, userDirectory);
} else {
}
HierarchyUnit hierarchyUnit = userDirectory.getHierarchyUnit(pathWithinUserDirectory);
if (hierarchyUnit == null)
- throw new ContentNotFoundException(
+ throw new ContentNotFoundException(session,
+ mountPath + "/" + relativePath + "/" + pathWithinUserDirectory,
"Cannot find " + pathWithinUserDirectory + " within " + userDirectoryName);
return new HierarchyUnitContent(session, this, hierarchyUnit);
}
if (nodes.getLength() > 1)
throw new IllegalArgumentException("Multiple content found for " + relativePath + " under " + mountPath);
if (nodes.getLength() == 0)
- throw new ContentNotFoundException("Path " + relativePath + " under " + mountPath + " was not found");
+ throw new ContentNotFoundException(session, mountPath + "/" + relativePath,
+ "Path " + relativePath + " under " + mountPath + " was not found");
Element element = (Element) nodes.item(0);
return new DomContent(session, this, element);
}
import java.io.IOException;
import java.io.InputStream;
-import java.net.Authenticator;
-import java.net.PasswordAuthentication;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import javax.xml.namespace.QName;
+import org.argeo.util.http.HttpHeader;
+import org.argeo.util.http.HttpMethod;
import org.argeo.util.http.HttpResponseStatus;
public class DavClient {
""";
System.out.println(body);
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(url)) //
- .header("Depth", "1") //
- .method(DavMethod.PROPPATCH.name(), BodyPublishers.ofString(body)) //
+ .header(HttpHeader.DEPTH.getHeaderName(), DavDepth.DEPTH_1.getValue()) //
+ .method(HttpMethod.PROPPATCH.name(), BodyPublishers.ofString(body)) //
.build();
BodyHandler<String> bodyHandler = BodyHandlers.ofString();
HttpResponse<String> response = httpClient.send(request, bodyHandler);
<D:propname/>
</D:propfind>""";
HttpRequest request = HttpRequest.newBuilder().uri(uri) //
- .header(DavHeader.DEPTH.getHeaderName(), DavDepth.DEPTH_1.getValue()) //
- .method(DavMethod.PROPFIND.name(), BodyPublishers.ofString(body)) //
+ .header(HttpHeader.DEPTH.getHeaderName(), DavDepth.DEPTH_1.getValue()) //
+ .method(HttpMethod.PROPFIND.name(), BodyPublishers.ofString(body)) //
.build();
HttpResponse<String> responseStr = httpClient.send(request, BodyHandlers.ofString());
public boolean exists(URI uri) {
try {
HttpRequest request = HttpRequest.newBuilder().uri(uri) //
- .header(DavHeader.DEPTH.getHeaderName(), DavDepth.DEPTH_0.getValue()) //
- .method(DavMethod.HEAD.name(), BodyPublishers.noBody()) //
+ .header(HttpHeader.DEPTH.getHeaderName(), DavDepth.DEPTH_0.getValue()) //
+ .method(HttpMethod.HEAD.name(), BodyPublishers.noBody()) //
.build();
BodyHandler<String> bodyHandler = BodyHandlers.ofString();
HttpResponse<String> response = httpClient.send(request, bodyHandler);
<D:allprop/>
</D:propfind>""";
HttpRequest request = HttpRequest.newBuilder().uri(uri) //
- .header(DavHeader.DEPTH.getHeaderName(), DavDepth.DEPTH_0.getValue()) //
- .method(DavMethod.PROPFIND.name(), BodyPublishers.ofString(body)) //
+ .header(HttpHeader.DEPTH.getHeaderName(), DavDepth.DEPTH_0.getValue()) //
+ .method(HttpMethod.PROPFIND.name(), BodyPublishers.ofString(body)) //
.build();
// HttpResponse<String> responseStr = httpClient.send(request, BodyHandlers.ofString());
package org.argeo.cms.dav;
+import org.argeo.util.http.HttpHeader;
+
import com.sun.net.httpserver.HttpExchange;
public enum DavDepth {
}
public static DavDepth fromHttpExchange(HttpExchange httpExchange) {
- String value = httpExchange.getRequestHeaders().getFirst(DavHeader.DEPTH.getHeaderName());
+ String value = httpExchange.getRequestHeaders().getFirst(HttpHeader.DEPTH.getHeaderName());
if (value == null)
return null;
DavDepth depth = switch (value) {
+++ /dev/null
-package org.argeo.cms.dav;
-
-/** Standard HTTP headers. */
-public enum DavHeader {
- DEPTH("Depth"), //
- ;
-
- private final String name;
-
- private DavHeader(String headerName) {
- this.name = headerName;
- }
-
- public String getHeaderName() {
- return name;
- }
-
- @Override
- public String toString() {
- return getHeaderName();
- }
-
-}
--- /dev/null
+package org.argeo.cms.dav;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.StringJoiner;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
+
+import javax.xml.namespace.NamespaceContext;
+
+import org.argeo.api.acr.ContentNotFoundException;
+import org.argeo.util.http.HttpHeader;
+import org.argeo.util.http.HttpMethod;
+import org.argeo.util.http.HttpResponseStatus;
+import org.argeo.util.http.HttpServerUtils;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+
+/**
+ * Centralise patterns which are not ACR specific. Not really meant as a
+ * framework for building WebDav servers, but rather to make uppe-level of
+ * ACR-specific code more readable and maintainable.
+ */
+public abstract class DavHttpHandler implements HttpHandler {
+ private NamespaceContext namespaceContext;
+
+ @Override
+ public void handle(HttpExchange exchange) throws IOException {
+ String subPath = HttpServerUtils.subPath(exchange);
+ String method = exchange.getRequestMethod();
+ try {
+ if (HttpMethod.GET.name().equals(method)) {
+ handleGET(exchange, subPath);
+ } else if (HttpMethod.OPTIONS.name().equals(method)) {
+ handleOPTIONS(exchange, subPath);
+ exchange.sendResponseHeaders(HttpResponseStatus.NO_CONTENT.getCode(), -1);
+ } else if (HttpMethod.PROPFIND.name().equals(method)) {
+ DavDepth depth = DavDepth.fromHttpExchange(exchange);
+ if (depth == null) {
+ // default, as per http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND
+ depth = DavDepth.DEPTH_INFINITY;
+ }
+ DavPropfind davPropfind;
+ try (InputStream in = exchange.getRequestBody()) {
+ davPropfind = DavPropfind.load(depth, in);
+ }
+ MultiStatusWriter multiStatusWriter = new MultiStatusWriter();
+ CompletableFuture<Void> published = handlePROPFIND(exchange, subPath, davPropfind, multiStatusWriter);
+ exchange.sendResponseHeaders(HttpResponseStatus.MULTI_STATUS.getCode(), 0l);
+ try (OutputStream out = exchange.getResponseBody()) {
+ multiStatusWriter.process(namespaceContext, out, published.minimalCompletionStage(),
+ davPropfind.isPropname());
+ }
+ } else {
+ throw new IllegalArgumentException("Unsupported method " + method);
+ }
+ } catch (ContentNotFoundException e) {
+ exchange.sendResponseHeaders(HttpResponseStatus.NOT_FOUND.getCode(), -1);
+ }
+ // TODO return a structured error message
+ catch (UnsupportedOperationException e) {
+ exchange.sendResponseHeaders(HttpResponseStatus.NOT_IMPLEMENTED.getCode(), -1);
+ } catch (Exception e) {
+ exchange.sendResponseHeaders(HttpResponseStatus.INTERNAL_SERVER_ERROR.getCode(), -1);
+ }
+
+ }
+
+ protected abstract CompletableFuture<Void> handlePROPFIND(HttpExchange exchange, String path,
+ DavPropfind davPropfind, Consumer<DavResponse> consumer) throws IOException;
+
+ protected abstract void handleGET(HttpExchange exchange, String path) throws IOException;
+
+ protected void handleOPTIONS(HttpExchange exchange, String path) throws IOException {
+ exchange.getResponseHeaders().set(HttpHeader.DAV.getHeaderName(), "1, 3");
+ StringJoiner methods = new StringJoiner(",");
+ methods.add(HttpMethod.OPTIONS.name());
+ methods.add(HttpMethod.HEAD.name());
+ methods.add(HttpMethod.GET.name());
+ methods.add(HttpMethod.POST.name());
+ methods.add(HttpMethod.PUT.name());
+ methods.add(HttpMethod.PROPFIND.name());
+ // TODO :
+ methods.add(HttpMethod.PROPPATCH.name());
+ methods.add(HttpMethod.MKCOL.name());
+ methods.add(HttpMethod.DELETE.name());
+ methods.add(HttpMethod.MOVE.name());
+ methods.add(HttpMethod.COPY.name());
+
+ exchange.getResponseHeaders().add(HttpHeader.ALLOW.getHeaderName(), methods.toString());
+ }
+
+ public void setNamespaceContext(NamespaceContext namespaceContext) {
+ this.namespaceContext = namespaceContext;
+ }
+
+}
+++ /dev/null
-package org.argeo.cms.dav;
-
-public enum DavMethod {
- // Generic HTTP
- HEAD, //
- // WebDav specific
- PROPFIND, //
- PROPPATCH, //
- ;
-}
import org.argeo.api.acr.QNamed;
-public enum DavXmlElement implements QNamed {
+enum DavXmlElement implements QNamed {
response, //
multistatus, //
href, //
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
-public class MultiStatusWriter implements Consumer<DavResponse> {
+class MultiStatusWriter implements Consumer<DavResponse> {
private BlockingQueue<DavResponse> queue = new ArrayBlockingQueue<>(64);
// private OutputStream out;
import java.io.IOException;
import java.io.InputStream;
-import java.io.OutputStream;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import javax.xml.namespace.QName;
import org.argeo.api.acr.Content;
+import org.argeo.api.acr.ContentNotFoundException;
import org.argeo.api.acr.ContentSession;
import org.argeo.api.acr.DName;
import org.argeo.api.acr.spi.ProvidedRepository;
import org.argeo.api.cms.CmsConstants;
-import org.argeo.cms.acr.ContentUtils;
import org.argeo.cms.auth.RemoteAuthUtils;
import org.argeo.cms.dav.DavDepth;
-import org.argeo.cms.dav.DavMethod;
+import org.argeo.cms.dav.DavHttpHandler;
import org.argeo.cms.dav.DavPropfind;
import org.argeo.cms.dav.DavResponse;
-import org.argeo.cms.dav.DavXmlElement;
-import org.argeo.cms.dav.MultiStatusWriter;
import org.argeo.cms.internal.http.RemoteAuthHttpExchange;
import org.argeo.util.StreamUtils;
-import org.argeo.util.http.HttpMethod;
import org.argeo.util.http.HttpResponseStatus;
-import org.argeo.util.http.HttpServerUtils;
import com.sun.net.httpserver.HttpExchange;
-import com.sun.net.httpserver.HttpHandler;
-public class CmsAcrHttpHandler implements HttpHandler {
+/** A partial WebDav implementation based on ACR. */
+public class CmsAcrHttpHandler extends DavHttpHandler {
private ProvidedRepository contentRepository;
@Override
- public void handle(HttpExchange exchange) throws IOException {
- String method = exchange.getRequestMethod();
- if (DavMethod.PROPFIND.name().equals(method)) {
- handlePROPFIND(exchange);
- } else if (HttpMethod.GET.name().equals(method)) {
- handleGET(exchange);
- } else {
- throw new IllegalArgumentException("Unsupported method " + method);
+ protected void handleGET(HttpExchange exchange, String path) throws IOException {
+ ContentSession session = RemoteAuthUtils.doAs(() -> contentRepository.get(),
+ new RemoteAuthHttpExchange(exchange));
+ if (!session.exists(path)) // not found
+ throw new ContentNotFoundException(session, path);
+ Content content = session.get(path);
+ Optional<Long> size = content.get(DName.getcontentlength, Long.class);
+ try (InputStream in = content.open(InputStream.class)) {
+ exchange.sendResponseHeaders(HttpResponseStatus.OK.getCode(), size.orElse(0l));
+ StreamUtils.copy(in, exchange.getResponseBody());
+ } catch (IOException e) {
+ throw new RuntimeException("Cannot process " + path, e);
}
-
}
- protected void handlePROPFIND(HttpExchange exchange) throws IOException {
- String relativePath = HttpServerUtils.relativize(exchange);
-
- DavDepth depth = DavDepth.fromHttpExchange(exchange);
- if (depth == null) {
- // default, as per http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND
- depth = DavDepth.DEPTH_INFINITY;
- }
-
+ @Override
+ protected CompletableFuture<Void> handlePROPFIND(HttpExchange exchange, String path, DavPropfind davPropfind,
+ Consumer<DavResponse> consumer) throws IOException {
ContentSession session = RemoteAuthUtils.doAs(() -> contentRepository.get(),
new RemoteAuthHttpExchange(exchange));
-
- String path = ContentUtils.ROOT_SLASH + relativePath;
- if (!session.exists(path)) {// not found
- exchange.sendResponseHeaders(HttpResponseStatus.NOT_FOUND.getCode(), -1);
- return;
- }
+ if (!session.exists(path)) // not found
+ throw new ContentNotFoundException(session, path);
Content content = session.get(path);
CompletableFuture<Void> published = new CompletableFuture<Void>();
-
- try (InputStream in = exchange.getRequestBody()) {
- DavPropfind davPropfind = DavPropfind.load(depth, in);
- MultiStatusWriter msWriter = new MultiStatusWriter();
- ForkJoinPool.commonPool().execute(() -> {
- publishDavResponses(content, davPropfind, msWriter);
- published.complete(null);
- });
- exchange.sendResponseHeaders(HttpResponseStatus.MULTI_STATUS.getCode(), 0l);
- try (OutputStream out = exchange.getResponseBody()) {
- msWriter.process(session, out, published.minimalCompletionStage(), davPropfind.isPropname());
- }
- }
+ ForkJoinPool.commonPool().execute(() -> {
+ publishDavResponses(content, davPropfind, consumer);
+ published.complete(null);
+ });
+ return published;
}
protected void publishDavResponses(Content content, DavPropfind davPropfind, Consumer<DavResponse> consumer) {
Object value = content.get(key);
processMapEntry(davResponse, key, value);
}
- if (DavXmlElement.resourcetype.qName().equals(key)) {
+ if (DName.resourcetype.qName().equals(key)) {
davResponse.getResourceTypes().addAll(content.getContentClasses());
}
}
}
- protected void handleGET(HttpExchange exchange) {
- String relativePath = HttpServerUtils.relativize(exchange);
- ContentSession session = RemoteAuthUtils.doAs(() -> contentRepository.get(),
- new RemoteAuthHttpExchange(exchange));
- Content content = session.get(ContentUtils.ROOT_SLASH + relativePath);
- Optional<Long> size = content.get(DName.getcontentlength, Long.class);
- try (InputStream in = content.open(InputStream.class)) {
- exchange.sendResponseHeaders(HttpResponseStatus.OK.getCode(), size.orElse(0l));
- StreamUtils.copy(in, exchange.getResponseBody());
- } catch (IOException e) {
- throw new RuntimeException("Cannot process " + relativePath, e);
- }
- }
-
public void setContentRepository(ProvidedRepository contentRepository) {
this.contentRepository = contentRepository;
}
--- /dev/null
+package org.argeo.util;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Serialisable wrapper of a {@link Throwable}. typically to be written as XML
+ * or JSON in a server error response.
+ */
+public class ExceptionsChain {
+ private List<SystemException> exceptions = new ArrayList<>();
+
+ public ExceptionsChain() {
+ }
+
+ public ExceptionsChain(Throwable exception) {
+ writeException(exception);
+ }
+
+ /** recursive */
+ protected void writeException(Throwable exception) {
+ SystemException systemException = new SystemException(exception);
+ exceptions.add(systemException);
+ Throwable cause = exception.getCause();
+ if (cause != null)
+ writeException(cause);
+ }
+
+ public List<SystemException> getExceptions() {
+ return exceptions;
+ }
+
+ public void setExceptions(List<SystemException> exceptions) {
+ this.exceptions = exceptions;
+ }
+
+ /** An exception in the chain. */
+ public static class SystemException {
+ private String type;
+ private String message;
+ private List<String> stackTrace;
+
+ public SystemException() {
+ }
+
+ public SystemException(Throwable exception) {
+ this.type = exception.getClass().getName();
+ this.message = exception.getMessage();
+ this.stackTrace = new ArrayList<>();
+ StackTraceElement[] elems = exception.getStackTrace();
+ for (int i = 0; i < elems.length; i++)
+ stackTrace.add("at " + elems[i].toString());
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+ public List<String> getStackTrace() {
+ return stackTrace;
+ }
+
+ public void setStackTrace(List<String> stackTrace) {
+ this.stackTrace = stackTrace;
+ }
+
+ @Override
+ public String toString() {
+ return "System exception: " + type + ", " + message + ", " + stackTrace;
+ }
+
+ }
+
+ @Override
+ public String toString() {
+ return exceptions.toString();
+ }
+}
package org.argeo.util.http;
-/** HTTP headers which are specific to WebDAV. */
+/** Standard HTTP headers (including WebDav). */
public enum HttpHeader {
AUTHORIZATION("Authorization"), //
WWW_AUTHENTICATE("WWW-Authenticate"), //
+ ALLOW("Allow"), //
+
+ // WebDav
+ DAV("DAV"), //
+ DEPTH("Depth"), //
;
public final static String BASIC = "Basic";
package org.argeo.util.http;
+/** Generic HTTP methods. */
public enum HttpMethod {
- GET,//
+ OPTIONS, //
+ HEAD, //
+ GET, //
+ POST, //
+ PUT, //
+ DELETE, //
+
+ // WebDav
+ PROPFIND, //
+ PROPPATCH, //
+ MKCOL, //
+ MOVE, //
+ COPY, //
;
}
package org.argeo.util.http;
/**
- * Standard HTTP response status codes.
+ * Standard HTTP response status codes (including WebDav ones).
*
* @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
*/
public enum HttpResponseStatus {
// Successful responses (200–299)
OK(200), //
+ NO_CONTENT(204), //
MULTI_STATUS(207), // WebDav
// Client error responses (400–499)
UNAUTHORIZED(401), //
FORBIDDEN(403), //
NOT_FOUND(404), //
+ // Server error responses (500-599)
+ INTERNAL_SERVER_ERROR(500), //
+ NOT_IMPLEMENTED(501), //
;
private final int code;
import com.sun.net.httpserver.HttpExchange;
public class HttpServerUtils {
+ private final static String SLASH = "/";
- public static String relativize(HttpContext httpContext, String path) {
- Objects.requireNonNull(path);
- if (!path.startsWith(httpContext.getPath()))
- throw new IllegalArgumentException(path + " does not belong to context" + httpContext.getPath());
- String relativePath = path.substring(httpContext.getPath().length());
+ private static String extractPathWithingContext(HttpContext httpContext, String fullPath, boolean startWithSlash) {
+ Objects.requireNonNull(fullPath);
+ String contextPath = httpContext.getPath();
+ if (!fullPath.startsWith(contextPath))
+ throw new IllegalArgumentException(fullPath + " does not belong to context" + contextPath);
+ String path = fullPath.substring(contextPath.length());
// TODO optimise?
- if (relativePath.startsWith("/"))
- relativePath = relativePath.substring(1);
- return relativePath;
+ if (!startWithSlash && path.startsWith(SLASH)) {
+ path = path.substring(1);
+ } else if (startWithSlash && !path.startsWith(SLASH)) {
+ path = SLASH + path;
+ }
+ return path;
}
+ /** Path within the context, NOT starting with a slash. */
public static String relativize(HttpExchange exchange) {
URI uri = exchange.getRequestURI();
HttpContext httpContext = exchange.getHttpContext();
- return relativize(httpContext, uri.getPath());
+ return extractPathWithingContext(httpContext, uri.getPath(), false);
+ }
+
+ /** Path within the context, starting with a slash. */
+ public static String subPath(HttpExchange exchange) {
+ URI uri = exchange.getRequestURI();
+ HttpContext httpContext = exchange.getHttpContext();
+ return extractPathWithingContext(httpContext, uri.getPath(), true);
}
/** singleton */