blob: a33909a0cb6bcb1a98ef5ab9e25f042a3d054b59 [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.jackrabbit.spi.commons.conversion;
import org.apache.jackrabbit.spi.NameFactory;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver;
import org.apache.jackrabbit.util.XMLChar;
import javax.jcr.NamespaceException;
/**
* <code>NameParser</code> parses a {@link String jcrName} using a
* {@link NamespaceResolver} and a {@link NameFactory}.
*/
public class NameParser {
// constants for parser
private static final int STATE_PREFIX_START = 0;
private static final int STATE_PREFIX = 1;
private static final int STATE_NAME_START = 2;
private static final int STATE_NAME = 3;
private static final int STATE_URI_START = 4;
private static final int STATE_URI = 5;
/**
* Parses the <code>jcrName</code> (either qualified or expanded) and
* returns a new <code>Name</code>.
*
* @param jcrName the name to be parsed. The jcrName may either be in the
* qualified or in the expanded form.
* @param resolver <code>NamespaceResolver</code> use to retrieve the
* namespace URI from the prefix contained in the given JCR name.
* @return qName the new <code>Name</code>
* @throws IllegalNameException If <code>jcrName</code> is not a valid
* JCR-style name.
* @throws NamespaceException If the jcr name contains an unknown prefix.
*/
public static Name parse(String jcrName, NamespaceResolver resolver, NameFactory factory)
throws IllegalNameException, NamespaceException {
// trivial check
int len = jcrName == null ? 0 : jcrName.length();
if (len == 0) {
throw new IllegalNameException("empty name");
}
if (".".equals(jcrName) || "..".equals(jcrName)) {
throw new IllegalNameException(jcrName);
}
// parse the name
String prefix = "";
String uri = null;
int nameStart = 0;
int state = STATE_PREFIX_START;
boolean trailingSpaces = false;
boolean checkFormat = (resolver == null);
for (int i = 0; i < len; i++) {
char c = jcrName.charAt(i);
if (c == ':') {
if (state == STATE_PREFIX_START) {
throw new IllegalNameException("Prefix must not be empty");
} else if (state == STATE_PREFIX) {
if (trailingSpaces) {
throw new IllegalNameException("Trailing spaces not allowed");
}
prefix = jcrName.substring(0, i);
if (!XMLChar.isValidNCName(prefix)) {
throw new IllegalNameException("Invalid name prefix: " + prefix);
}
state = STATE_NAME_START;
} else if (state == STATE_URI) {
// ignore -> validation of uri later on.
} else {
throw new IllegalNameException(asDisplayableString(c) + " not allowed in name");
}
trailingSpaces = false;
} else if (c == ' ') {
if (state == STATE_PREFIX_START || state == STATE_NAME_START) {
throw new IllegalNameException(asDisplayableString(c) + " not valid name start");
}
trailingSpaces = true;
} else if (c == '[' || c == ']' || c == '*' || c == '|') {
throw new IllegalNameException(asDisplayableString(c) + " not allowed in name");
} else if (Character.isWhitespace(c) && c < 128) {
throw new IllegalNameException("Whitespace character " + asDisplayableString(c) + " not allowed in name");
} else if (c == '/') {
if (state == STATE_URI_START) {
state = STATE_URI;
} else if (state != STATE_URI) {
throw new IllegalNameException(asDisplayableString(c) + " not allowed in name");
}
trailingSpaces = false;
} else if (c == '{') {
if (state == STATE_PREFIX_START) {
state = STATE_URI_START;
} else if (state == STATE_URI_START || state == STATE_URI) {
// second '{' in the uri-part -> no valid expanded jcr-name.
// therefore reset the nameStart and change state.
state = STATE_NAME;
nameStart = 0;
} else if (state == STATE_NAME_START) {
state = STATE_NAME;
nameStart = i;
}
trailingSpaces = false;
} else if (c == '}') {
if (state == STATE_URI_START || state == STATE_URI) {
String tmp = jcrName.substring(1, i);
if (tmp.length() == 0 || tmp.indexOf(':') != -1) {
// The leading "{...}" part is empty or contains
// a colon, so we treat it as a valid namespace URI.
// More detailed validity checks (is it well formed,
// registered, etc.) are not needed here.
uri = tmp;
state = STATE_NAME_START;
} else if (tmp.equals("internal")) {
// As a special Jackrabbit backwards compatibility
// feature, support {internal} as a valid URI prefix
uri = tmp;
state = STATE_NAME_START;
} else if (tmp.indexOf('/') == -1) {
// The leading "{...}" contains neither a colon nor
// a slash, so we can interpret it as a a part of a
// normal local name.
state = STATE_NAME;
nameStart = 0;
} else {
throw new IllegalNameException(
"The URI prefix of the name " + jcrName
+ " is neither a valid URI nor a valid part"
+ " of a local name.");
}
} else if (state == STATE_PREFIX_START) {
state = STATE_PREFIX; // prefix start -> validation later on will fail.
} else if (state == STATE_NAME_START) {
state = STATE_NAME;
nameStart = i;
}
trailingSpaces = false;
} else {
if (state == STATE_PREFIX_START) {
state = STATE_PREFIX; // prefix start
} else if (state == STATE_NAME_START) {
state = STATE_NAME;
nameStart = i;
} else if (state == STATE_URI_START) {
state = STATE_URI;
}
trailingSpaces = false;
}
}
// take care of qualified jcrNames starting with '{' that are not having
// a terminating '}' -> make sure there are no illegal characters present.
if (state == STATE_URI && (jcrName.indexOf(':') > -1 || jcrName.indexOf('/') > -1)) {
throw new IllegalNameException("Local name may not contain ':' nor '/'");
}
if (nameStart == len || state == STATE_NAME_START) {
throw new IllegalNameException("Local name must not be empty");
}
if (trailingSpaces) {
throw new IllegalNameException("Trailing spaces not allowed");
}
// if namespace is null, this is just a check for format. this can only
// happen if invoked internally
if (checkFormat) {
return null;
}
// resolve prefix to uri
if (uri == null) {
uri = resolver.getURI(prefix);
}
String localName = (nameStart == 0 ? jcrName : jcrName.substring(nameStart, len));
return factory.create(uri, localName);
}
private static String asDisplayableString(char c) {
if (c >= ' ' && c < 127) {
return Character.toString(c);
} else if (c == '\b') {
return "\\b";
} else if (c == '\f') {
return "\\f";
} else if (c == '\n') {
return "\\n";
} else if (c == '\r') {
return "\\r";
} else if (c == '\t') {
return "\\t";
} else {
return String.format("\\u%04x", (int) c);
}
}
/**
* Parses an array of <code>jcrName</code> and returns the respective
* array of <code>Name</code>.
*
* @param jcrNames the array of names to be parsed
* @param resolver <code>NamespaceResolver</code> use to retrieve the
* namespace URI from the prefix contained in the given JCR name.
* @param factory
* @return the new array of <code>Name</code>
* @throws IllegalNameException If <code>jcrName</code> is not a valid
* JCR-style name.
* @throws NamespaceException If the jcr name contains an unknown prefix.
*/
public static Name[] parse(String jcrNames[], NamespaceResolver resolver, NameFactory factory)
throws NameException, NamespaceException {
Name[] ret = new Name[jcrNames.length];
for (int i=0; i<ret.length; i++) {
ret[i] = parse(jcrNames[i], resolver, factory);
}
return ret;
}
/**
* Check the format of the jcr name. Note that the prefix is not resolved
* and therefore namespace violations (unknown prefix) will not be detected.
*
* @param jcrName
* @throws IllegalNameException If the jcrName contains an invalid format.
*/
public static void checkFormat(String jcrName) throws IllegalNameException {
try {
parse(jcrName, null, null);
} catch (NamespaceException e) {
// will never occur since the resolver is not passed to the parser
}
}
}