/******************************************************************************* * Copyright (c) 2006-2013, Cloudsmith Inc. * The code, documentation and other materials contained herein have been * licensed under the Eclipse Public License - v 1.0 by the copyright holder * listed above, as the Initial Contributor under such license. The text of * such license is available at www.eclipse.org. ******************************************************************************/ package org.eclipse.buckminster.pde.tasks; import java.util.HashMap; import java.util.List; import java.util.Map; import org.eclipse.buckminster.pde.IPDEConstants; import org.eclipse.core.runtime.CoreException; import org.eclipse.equinox.p2.metadata.IVersionedId; import org.eclipse.equinox.p2.metadata.Version; import org.eclipse.pde.internal.build.IBuildPropertiesConstants; /** * @author Thomas Hallgren */ @SuppressWarnings("restriction") public class FeatureVersionSuffixGenerator implements IPDEConstants, IBuildPropertiesConstants { private static void appendEncodedCharacter(StringBuilder buffer, int c) { while (c > 62) { buffer.append('z'); c -= 63; } buffer.append(base64Character(c)); } // Integer to character conversion in our base-64 encoding scheme. If the // input is out of range, an illegal character will be returned. // private static char base64Character(int number) { return (number < 0 || number > 63) ? ' ' : BASE_64_ENCODING.charAt(number); } private static int charValue(char c) { int index = BASE_64_ENCODING.indexOf(c); // The "+ 1" is very intentional. For a blank (or anything else that // is not a legal character), we want to return 0. For legal // characters, we want to return one greater than their position, so // that a blank is correctly distinguished from '-'. return index + 1; } // Encode a non-negative number as a variable length string, with the // property that if X > Y then the encoding of X is lexicographically // greater than the enocding of Y. This is accomplished by encoding the // length of the string at the beginning of the string. The string is a // series of base 64 (6-bit) characters. The first three bits of the first // character indicate the number of additional characters in the string. // The last three bits of the first character and all of the rest of the // characters encode the actual value of the number. Examples: // 0 --> 000 000 --> "-" // 7 --> 000 111 --> "6" // 8 --> 001 000 001000 --> "77" // 63 --> 001 000 111111 --> "7z" // 64 --> 001 001 000000 --> "8-" // 511 --> 001 111 111111 --> "Dz" // 512 --> 010 000 001000 000000 --> "E7-" // 2^32 - 1 --> 101 011 111111 ... 111111 --> "fzzzzz" // 2^45 - 1 --> 111 111 111111 ... 111111 --> "zzzzzzzz" // (There are some wasted values in this encoding. For example, // "7-" through "76" and "E--" through "E6z" are not legal encodings of // any number. But the benefit of filling in those wasted ranges would not // be worth the added complexity.) private static String lengthPrefixBase64(long number) { int length = 7; for (int i = 0; i < 7; ++i) { if (number < (1L << ((i * 6) + 3))) { length = i; break; } } StringBuilder result = new StringBuilder(length + 1); result.append(base64Character((length << 3) + (int) ((number >> (6 * length)) & 0x7))); while (--length >= 0) { result.append(base64Character((int) ((number >> (6 * length)) & 0x3f))); } return result.toString(); } private Map<String, Integer> contextQualifierLengths; private static final int QUALIFIER_SUFFIX_VERSION = 1; // The 64 characters that are legal in a version qualifier, in // lexicographical order. private static final String BASE_64_ENCODING = "-0123456789_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; //$NON-NLS-1$ private static int computeNameSum(String name) { int sum = 0; int top = name.length(); int lshift = 20; for (int idx = 0; idx < top; ++idx) { int c = name.charAt(idx) & 0xffff; if (c == '.' && lshift > 0) lshift -= 4; else sum += c << lshift; } return sum; } private final int maxVersionSuffixLength; private final int significantDigits; public FeatureVersionSuffixGenerator() { this(-1, -1); } public FeatureVersionSuffixGenerator(int maxVersionSuffixLenght, int significantDigits) { this.maxVersionSuffixLength = maxVersionSuffixLenght < 0 ? 28 : maxVersionSuffixLenght; this.significantDigits = significantDigits < 0 ? Integer.MAX_VALUE : significantDigits; } public void addContextQualifierLength(String context, int length) { if (contextQualifierLengths == null) contextQualifierLengths = new HashMap<String, Integer>(); contextQualifierLengths.put(context, Integer.valueOf(length)); } /** * Version suffix generation. Modeled after * {@link org.eclipse.pde.internal.build.builder.FeatureBuildScriptGenerator#generateFeatureVersionSuffix(org.eclipse.pde.internal.build.site.BuildTimeFeature buildFeature)} * * @return The generated suffix or <code>null</code> * @throws CoreException */ public String generateSuffix(List<IVersionedId> features, List<IVersionedId> bundles) { if (maxVersionSuffixLength <= 0) return null; // do nothing long majorSum = 0L; long minorSum = 0L; long serviceSum = 0L; long nameCharsSum = 0L; // Include the version of this algorithm as part of the suffix, so that // we have a way to make sure all suffixes increase when the algorithm // changes. // majorSum += QUALIFIER_SUFFIX_VERSION; int numElements = features.size() + bundles.size(); if (numElements == 0) // // This feature is empty so there will be no suffix // return null; String[] qualifiers = new String[numElements]; // Loop through the included features, adding the version number parts // to the running totals and storing the qualifier suffixes. // int idx = 0; for (IVersionedId refFeature : features) { Version v = refFeature.getVersion(); org.osgi.framework.Version version = v == null ? null : new org.osgi.framework.Version(v.toString()); majorSum += version.getMajor(); minorSum += version.getMinor(); serviceSum += version.getMicro(); String qualifier = version.getQualifier(); Integer ctxLen = contextQualifierLengths == null ? null : contextQualifierLengths.get(refFeature.getId()); int contextLength = (ctxLen == null) ? -1 : ctxLen.intValue(); ++contextLength; // account for the '-' separating the context // qualifier and suffix // The entire qualifier of the nested feature is often too long to // include in the suffix computation for the containing feature, // and using it would result in extremely long qualifiers for // umbrella features. So instead we want to use just the suffix // part of the qualifier, or just the context part (if there is no // suffix part). See bug #162022. // if (qualifier != null && contextLength > 0 && qualifier.length() > contextLength) qualifier = qualifier.substring(contextLength); qualifiers[idx++] = qualifier; nameCharsSum = computeNameSum(refFeature.getId()); } // Loop through the included plug-ins and fragments, adding the version // number parts to the running totals and storing the qualifiers. // for (IVersionedId refBundle : bundles) { Version v = refBundle.getVersion(); org.osgi.framework.Version version = v == null ? null : new org.osgi.framework.Version(v.toString()); majorSum += version.getMajor(); minorSum += version.getMinor(); serviceSum += version.getMicro(); String qualifier = version.getQualifier(); if (qualifier != null && qualifier.endsWith(PROPERTY_QUALIFIER)) { int resultingLength = qualifier.length() - PROPERTY_QUALIFIER.length(); if (resultingLength > 0) { if (qualifier.charAt(resultingLength - 1) == '.') resultingLength--; qualifier = resultingLength > 0 ? qualifier.substring(0, resultingLength) : null; } else qualifier = null; } qualifiers[idx++] = qualifier; } // Limit the qualifiers to the specified number of significant digits, // and figure out what the longest qualifier is. // int longestQualifier = 0; while (--idx >= 0) { String qualifier = qualifiers[idx]; if (qualifier == null) continue; if (qualifier.length() > significantDigits) { qualifier = qualifier.substring(0, significantDigits); qualifiers[idx] = qualifier; } if (qualifier.length() > longestQualifier) longestQualifier = qualifier.length(); } StringBuilder result = new StringBuilder(); // Encode the sums of the first three parts of the version numbers. result.append(lengthPrefixBase64(majorSum)); result.append(lengthPrefixBase64(minorSum)); result.append(lengthPrefixBase64(serviceSum)); result.append(lengthPrefixBase64(nameCharsSum)); if (longestQualifier > 0) { // Calculate the sum at each position of the qualifiers. int[] qualifierSums = new int[longestQualifier]; for (idx = 0; idx < numElements; ++idx) { String qualifier = qualifiers[idx]; if (qualifier == null) continue; int top = qualifier.length(); for (int j = 0; j < top; ++j) qualifierSums[j] += charValue(qualifier.charAt(j)); } // Normalize the sums to be base 65. int carry = 0; for (int k = longestQualifier - 1; k >= 1; --k) { qualifierSums[k] += carry; carry = qualifierSums[k] / 65; qualifierSums[k] = qualifierSums[k] % 65; } qualifierSums[0] += carry; // Always use one character for overflow. This will be handled // correctly even when the overflow character itself overflows. result.append(lengthPrefixBase64(qualifierSums[0])); for (int m = 1; m < longestQualifier; ++m) appendEncodedCharacter(result, qualifierSums[m]); } // If the resulting suffix is too long, shorten it to the designed // length. // if (result.length() > maxVersionSuffixLength) result.setLength(maxVersionSuffixLength); // It is safe to strip any '-' characters from the end of the suffix. // (This won't happen very often, but it will save us a character or // two when it does.) // int len = result.length(); while (len > 0 && result.charAt(len - 1) == '-') result.setLength(--len); return result.toString(); } }