/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.apache.felix.http.base.internal.util; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.nio.charset.CoderResult; import java.nio.charset.CodingErrorAction; /** * Some convenience methods for handling URI(-parts). */ public abstract class UriUtils { private static final String SLASH_STR = "/"; private static final char DOT = '.'; private static final char SLASH = '/'; /** * Concatenates two paths keeping their respective path-parts into consideration. * * @param path1 the first part of the path, can be <code>null</code>; * @param path2 the second part of the path, can be <code>null</code>. * @return the concatenated path, can be <code>null</code> in case both given arguments were <code>null</code>. */ public static String concat(String path1, String path2) { // Handle special cases... if (path1 == null && path2 == null) { return null; } if (path1 == null) { path1 = ""; } if (path2 == null) { path2 = ""; } if (isEmpty(path1) && isEmpty(path2)) { return ""; } StringBuilder sb = new StringBuilder(); int idx = path1.indexOf('?'); if (idx == 0) { // path1 only consists of a query, append it to the second path... return path2.concat(path1); } else if (idx > 0) { // path1 contains of a path + query, append the path first... sb.append(path1.substring(0, idx)); } else { // Plain paths... sb.append(path1); // need a slash? } if (endsWith(sb, SLASH_STR)) { if (path2.startsWith(SLASH_STR)) { sb.append(path2.substring(1)); } else { sb.append(path2); } } else { if (path2.startsWith(SLASH_STR)) { sb.append(path2); } else if (sb.length() > 0 && !isEmpty(path2)) { sb.append(SLASH_STR).append(path2); } else { sb.append(path2); } } if (idx > 0) { // Add the query of path1... sb.append(path1.substring(idx, path1.length())); } return sb.toString(); } /** * Decodes a given URL-encoded path assuming it is UTF-8 encoded. * * @param path the URL-encoded path, can be <code>null</code>. * @return the decoded path, can be <code>null</code> only if the given path was <code>null</code>. */ public static String decodePath(String path) { return decodePath(path, "UTF-8"); } /** * Decodes a given URL-encoded path using a given character encoding. * * @param path the URL-encoded path, can be <code>null</code>; * @param encoding the character encoding to use, cannot be <code>null</code>. * @return the decoded path, can be <code>null</code> only if the given path was <code>null</code>. */ private static String decodePath(String path, String encoding) { // Special cases... if (path == null) { return null; } CharsetDecoder decoder = Charset.forName(encoding).newDecoder(); decoder.onMalformedInput(CodingErrorAction.REPORT); decoder.onUnmappableCharacter(CodingErrorAction.REPORT); int len = path.length(); ByteBuffer buf = ByteBuffer.allocate(len); StringBuilder sb = new StringBuilder(); for (int i = 0; i < len; i++) { char ch = path.charAt(i); if (ch == '%' && (i + 2 < len)) { // URL-encoded char... buf.put((byte) ((16 * hexVal(path, ++i)) + hexVal(path, ++i))); } else { if (buf.position() > 0) { // flush encoded chars first... sb.append(decode(buf, decoder)); buf.clear(); } sb.append(ch); } } // flush trailing encoded characters... if (buf.position() > 0) { sb.append(decode(buf, decoder)); buf.clear(); } return sb.toString(); } /** * Removes all superfluous dot-segments using the algorithm described in RFC-3986 section 5.2.4. * * @param path the path to remove all dot-segments from, can be <code>null</code>. * @return the cleaned path, can be <code>null</code> only if the given path was <code>null</code>. */ public static String removeDotSegments(String path) { // Handle special cases... if (path == null) { return null; } if (isEmpty(path)) { return ""; } StringBuilder scratch = new StringBuilder(path); StringBuilder sb = new StringBuilder(); char l, la = 0, laa = 0, laaa = 0; while (scratch.length() > 0) { l = la(scratch, 0); la = la(scratch, 1); laa = la(scratch, 2); if (l == DOT) { if (la == 0) { // (D) found '.' at the end of the URL break; } else if (la == DOT && laa == SLASH) { // (A) found '../', remove it from the input... scratch.delete(0, 3); continue; } else if (la == DOT && laa == 0) { // (D) found '..' at the end of the URL break; } else if (la == SLASH) { // (A) found './', remove it from the input... scratch.delete(0, 2); continue; } } else if (l == SLASH && la == DOT) { if (laa == SLASH) { // (B) found '/./', remove the leading '/.'... scratch.delete(0, 2); continue; } else if (laa == 0) { // (B) found '/.' as last part of the URL sb.append(SLASH); // we're done... break; } else if (laa == DOT) { laaa = la(scratch, 3); if (laaa == SLASH) { // (C) found '/../', remove the '/..' part from the input... scratch.delete(0, 3); // go back one segment in the output, including the last '/'... sb.setLength(lb(sb, 0)); continue; } else if (laaa == 0) { // (C) found '/..' as last part of the URL, go back one segment in the output, excluding the last '/'... sb.setLength(lb(sb, -1)); // we're done... break; } } } // (E) Copy everything up to (but not including) the next '/'... do { sb.append(l); scratch.delete(0, 1); l = la(scratch, 0); } while (l != SLASH && l != 0); } return sb.toString(); } private static char la(CharSequence sb, int idx) { if (sb.length() > idx) { return sb.charAt(idx); } return 0; } private static int lb(CharSequence sb, int offset) { int pos = sb.length() - 1 - offset; while (pos > 0 && sb.charAt(pos + offset) != SLASH) { pos--; } return pos; } private static String decode(ByteBuffer bb, CharsetDecoder decoder) { CharBuffer cb = CharBuffer.allocate(128); CoderResult result = decoder.decode((ByteBuffer) bb.flip(), cb, true /* endOfInput */); if (result.isError()) { throw new IllegalArgumentException("Malformed UTF-8!"); } return ((CharBuffer) cb.flip()).toString(); } private static boolean endsWith(CharSequence seq, String part) { int len = part.length(); if (seq.length() < len) { return false; } for (int i = 0; i < len; i++) { if (seq.charAt(seq.length() - (i + 1)) != part.charAt(i)) { return false; } } return true; } private static int hexVal(CharSequence seq, int idx) { char ch = seq.charAt(idx); if (ch >= '0' && ch <= '9') { return ch - '0'; } else if (ch >= 'a' && ch <= 'f') { return 10 + (ch - 'a'); } else if (ch >= 'A' && ch <= 'F') { return 10 + (ch - 'A'); } throw new IllegalArgumentException("Invalid hex digit: " + ch); } private static boolean isEmpty(String value) { return value == null || "".equals(value.trim()); } /** * Creates a new {@link UriUtils} instance. */ private UriUtils() { // Nop } }