blob: 3c12958f1a1b87a737d5dbec1dfb1424d59d3dfc [file] [log] [blame]
/*
* 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
}
}