blob: 80b4b3afe0d4f52312cf9b0cd9add97ed7efaed4 [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.vault.packaging;
import org.apache.jackrabbit.util.XMLChar;
import org.apache.jackrabbit.vault.util.Text;
/**
* <code>PackageId</code> provides the basic metrics for identifying a package.
* A package id consists of a group id, a name and a version.
* the group is a relative path, eg: "company/project/subgroup", the name and the version
* can be of any format.
*/
public class PackageId implements Comparable<PackageId> {
public static final String ETC_PACKAGES = "/etc/packages";
public static final String ETC_PACKAGES_PREFIX = "/etc/packages/";
public static final PackageId[] EMPTY = new PackageId[0];
private final String group;
private final String name;
private final Version version;
private final String str;
private final boolean fromPath;
/**
* Creates a new package id
* @param path path of the package
*/
public PackageId(String path) {
fromPath = true;
path = path.trim();
int idx = path.lastIndexOf('.');
if (idx > 0) {
String ext = path.substring(idx);
if (ext.equalsIgnoreCase(".zip") || ext.equalsIgnoreCase(".jar")) {
path = path.substring(0, idx);
}
}
idx = path.lastIndexOf('/');
String name;
if (idx < 0) {
name = path;
this.group = "";
} else {
name = path.substring(idx + 1);
String grp = path.substring(0, idx);
if (grp.equals(ETC_PACKAGES)) {
grp = "";
} else if (grp.startsWith(ETC_PACKAGES_PREFIX)) {
grp = grp.substring(ETC_PACKAGES_PREFIX.length());
} else if (grp.startsWith("/")) {
grp = grp.substring(1);
}
this.group = grp;
}
// check if name contains a version
String[] segs = Text.explode(name, '-');
int i=segs.length-1;
while (i>0) {
try {
// accept numbers < 1000 (hotfix case)
if (Integer.parseInt(segs[i]) >= 1000) {
break;
}
} catch (NumberFormatException e) {
// ignore
}
// check if starts with a letter'
if (Character.isJavaIdentifierStart(segs[i].charAt(0))) {
// then need a digit
if (segs[i].length() == 1 || !Character.isDigit(segs[i].charAt(1)) && !segs[i].equals("SNAPSHOT")) {
break;
}
}
i--;
}
if (i == segs.length-1) {
this.name = name;
version = Version.EMPTY;
} else {
StringBuilder str = new StringBuilder();
for (int j = 0; j<= i; j++) {
if (j > 0) {
str.append('-');
}
str.append(segs[j]);
}
this.name = str.toString();
str.setLength(0);
for (int j = i+1; j<segs.length; j++) {
if (j > i+1) {
str.append('-');
}
str.append(segs[j]);
}
this.version = Version.create(str.toString());
}
this.str = getString(group, this.name, version);
}
/**
* Creates a new package id
* @param path path of the package
* @param version version of the package
*/
public PackageId(String path, String version) {
this(path, Version.create(version));
}
/**
* Creates a new package id
* @param path path of the package
* @param version version of the package
*/
public PackageId(String path, Version version) {
fromPath = true;
path = path.trim();
int idx = path.lastIndexOf('.');
if (idx > 0) {
String ext = path.substring(idx);
if (ext.equalsIgnoreCase(".zip") || ext.equalsIgnoreCase(".jar")) {
path = path.substring(0, idx);
}
}
if (version != null && path.endsWith('-'+version.toString())) {
path = path.substring(0, path.length() - version.toString().length() - 1);
}
idx = path.lastIndexOf('/');
if (idx < 0) {
this.name = path;
this.group = "";
} else {
this.name = path.substring(idx + 1);
String grp = path.substring(0, idx);
if (grp.equals(ETC_PACKAGES)) {
grp = "";
} else if (grp.startsWith(ETC_PACKAGES_PREFIX)) {
grp = grp.substring(ETC_PACKAGES_PREFIX.length());
} else if (grp.startsWith("/")) {
grp = grp.substring(1);
}
this.group = grp;
}
// sanitize version
if (version == null || version.toString().length() == 0) {
version = Version.EMPTY;
}
this.version = version;
this.str = getString(group, name, version);
}
/**
* Creates a new package id
* @param group group id
* @param name name
* @param version version
*/
public PackageId(String group, String name, String version) {
this(group, name, Version.create(version));
}
/**
* Creates a new package id
* @param group group id
* @param name name
* @param version version
*/
public PackageId(String group, String name, Version version) {
fromPath = false;
// validate group
if (group.equals(ETC_PACKAGES)) {
group = "";
} else if (group.startsWith(ETC_PACKAGES_PREFIX)) {
group = group.substring(ETC_PACKAGES_PREFIX.length());
} else if (group.startsWith("/")) {
group = group.substring(1);
}
this.group = group;
this.name = name;
this.version = version == null ? Version.EMPTY : version;
this.str = getString(this.group, name, this.version);
}
/**
* Returns a package id from a id string. if the given id is null or an
* empty string, <code>null</code> is returned.
* @param str the string
* @return the package id
*/
public static PackageId fromString(String str) {
if (str == null || str.length() == 0) {
return null;
}
String[] segs = str.split(":");
if (segs.length == 1) {
return new PackageId("", segs[0], "");
} else if (segs.length == 2) {
return new PackageId(segs[0], segs[1], "");
} else {
return new PackageId(segs[0], segs[1], segs[2]);
}
}
/**
* Returns an array of package id from strings
* @param str the strings
* @return the array of package ids
*/
public static PackageId[] fromString(String ... str) {
PackageId[] ret = new PackageId[str.length];
for (int i=0; i<str.length; i++) {
ret[i] = PackageId.fromString(str[i]);
}
return ret;
}
/**
* Creates a comma separated list of id strings.
* @param packs the ids
* @return the string
*/
public static String toString(PackageId ... packs) {
String delim = "";
StringBuilder b = new StringBuilder();
for (PackageId pack: packs) {
b.append(delim).append(pack);
delim=",";
}
return b.toString();
}
/**
* Checks if this definition was constructed from a path, rather from a
* group and name.
* @return <code>true</code> if constructed from path.
*
* @since 2.2.26
*/
public boolean isFromPath() {
return fromPath;
}
/**
* Returns the path of this package. please note that since 2.3 this also
* includes the version, but never the extension (.zip).
*
* @return the path of this package
* @since 2.2
*/
public String getInstallationPath() {
StringBuilder b = new StringBuilder(ETC_PACKAGES_PREFIX);
if (group.length() > 0) {
b.append(group);
b.append("/");
}
b.append(name);
if (version.toString().length() > 0) {
b.append("-").append(version);
}
return b.toString();
}
/**
* Returns the group id of this package
* @return the group id;
* @since 2.2
*/
public String getGroup() {
return group;
}
/**
* Returns the name of this package which is the last segment of the path.
* @return the name of this package.
*/
public String getName() {
return name;
}
/**
* Returns the version of this package or and empty string if n/a.
* @return the version of this package
* @since 2.0
*/
public String getVersionString() {
return version.toString();
}
/**
* Returns a download name in the form
* <code>name [ "-" version ] ".zip"</code>
* @return the download name
* @since 2.0
*/
public String getDownloadName() {
StringBuilder str = new StringBuilder(name);
if (version.toString().length() > 0) {
str.append("-").append(version);
}
str.append(".zip");
return str.toString();
}
/**
* Returns the version of this package or <code>null</code> if n/a.
* @return the version of this package
*/
public Version getVersion() {
return version;
}
/**
* Returns a string representation of this id
*/
@Override
public String toString() {
return str;
}
@Override
public boolean equals(Object o) {
return this == o ||
o instanceof PackageId && str.equals(o.toString());
}
@Override
public int hashCode() {
return str.hashCode();
}
/**
* {@inheritDoc}
*
* Compares this id with the given one.
*/
public int compareTo(PackageId o) {
int comp = group.compareTo(o.getGroup());
if (comp != 0) {
return comp;
}
comp = name.compareTo(o.getName());
if (comp != 0) {
return comp;
}
return version.compareTo(o.getVersion());
}
/**
* Internally get the string representation, colon separated.
* @param group group name
* @param name name
* @param version version
* @return string version
*/
private static String getString(String group, String name, Version version) {
return getString(group, name, version == null ? "" : version.toString());
}
/**
* Internally get the string representation, colon separated.
* @param group group name
* @param name name
* @param version version
* @return string version
*/
private static String getString(String group, String name, String version) {
StringBuilder b = new StringBuilder();
b.append(group).append(':');
b.append(name);
if (version.length() > 0) {
b.append(':').append(version);
}
return b.toString();
}
/**
* Checks if this package id is valid in respect to JCR names.
* @return {@code true} if the names are valid
*/
public boolean isValid() {
return PackageId.isValid(group, name, version == null ? null : version.toString());
}
/**
* Checks if the package id is valid in respect to JCR names.
* @param group the package group name
* @param name the package name
* @param version the (optional) version
* @return {@code true} if the names are valid
*/
public static boolean isValid(String group, String name, String version) {
try {
assertValidJcrName(name);
if (version != null && !version.isEmpty()) {
assertValidJcrName(version);
}
for (String groupSegment: Text.explode(group, '/')) {
assertValidJcrName(groupSegment);
}
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
// the code below is copied from org.apache.jackrabbit.spi.commons.conversion.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 validates it.
* @throws java.lang.IllegalArgumentException if the name is not valid
*/
private static void assertValidJcrName(String jcrName) throws IllegalArgumentException {
// trivial check
int len = jcrName == null ? 0 : jcrName.length();
if (len == 0) {
throw new IllegalArgumentException("empty name");
}
if (".".equals(jcrName) || "..".equals(jcrName)) {
throw new IllegalArgumentException(jcrName);
}
// parse the name
String prefix;
int nameStart = 0;
int state = STATE_PREFIX_START;
boolean trailingSpaces = false;
for (int i = 0; i < len; i++) {
char c = jcrName.charAt(i);
if (c == ':') {
if (state == STATE_PREFIX_START) {
throw new IllegalArgumentException("Prefix must not be empty");
} else if (state == STATE_PREFIX) {
if (trailingSpaces) {
throw new IllegalArgumentException("Trailing spaces not allowed");
}
prefix = jcrName.substring(0, i);
if (!XMLChar.isValidNCName(prefix)) {
throw new IllegalArgumentException("Invalid name prefix: "+ prefix);
}
state = STATE_NAME_START;
} else if (state == STATE_URI) {
// ignore -> validation of uri later on.
} else {
throw new IllegalArgumentException("'" + c + "' not allowed in name");
}
trailingSpaces = false;
} else if (c == ' ') {
if (state == STATE_PREFIX_START || state == STATE_NAME_START) {
throw new IllegalArgumentException("'" + c + "' not valid name start");
}
trailingSpaces = true;
} else if (Character.isWhitespace(c) || c == '[' || c == ']' || c == '*' || c == '|') {
throw new IllegalArgumentException("'" + c + "' not allowed in name");
} else if (c == '/') {
if (state == STATE_URI_START) {
state = STATE_URI;
} else if (state != STATE_URI) {
throw new IllegalArgumentException("'" + 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.
state = STATE_NAME_START;
} else if (tmp.equals("internal")) {
// As a special Jackrabbit backwards compatibility
// feature, support {internal} as a valid URI prefix
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 IllegalArgumentException(
"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 IllegalArgumentException("Local name may not contain ':' nor '/'");
}
if (nameStart == len || state == STATE_NAME_START) {
throw new IllegalArgumentException("Local name must not be empty");
}
if (trailingSpaces) {
throw new IllegalArgumentException("Trailing spaces not allowed");
}
}
}