/*
* Copyright (c) 2012, the Dart project authors.
*
* Licensed under the Eclipse Public License v1.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.eclipse.org/legal/epl-v10.html
*
* 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 com.google.dart.tools.core.utilities.yaml;
import com.google.dart.tools.core.DartCore;
import com.google.dart.tools.core.pub.PubspecConstants;
import com.google.dart.tools.core.utilities.resource.IFileUtilities;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.constructor.SafeConstructor;
import org.yaml.snakeyaml.introspector.BeanAccess;
import org.yaml.snakeyaml.introspector.Property;
import org.yaml.snakeyaml.introspector.PropertyUtils;
import org.yaml.snakeyaml.nodes.CollectionNode;
import org.yaml.snakeyaml.nodes.MappingNode;
import org.yaml.snakeyaml.nodes.Node;
import org.yaml.snakeyaml.nodes.NodeTuple;
import org.yaml.snakeyaml.nodes.SequenceNode;
import org.yaml.snakeyaml.nodes.Tag;
import org.yaml.snakeyaml.representer.Representer;
import org.yaml.snakeyaml.resolver.Resolver;
import org.yaml.snakeyaml.scanner.ScannerException;
import java.beans.IntrospectionException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Utility class for using Snake YAML parser and other yaml utility methods.
*
* @coverage dart.tools.core.utilities
*/
public class PubYamlUtils {
/**
* Resolver to avoid parsing to implicit types, instead parse as string
*/
private static class CustomResolver extends Resolver {
/*
* do not resolve float and timestamp, boolean and int
*/
@Override
protected void addImplicitResolvers() {
addImplicitResolver(Tag.BOOL, BOOL, "yYnNtTfFoO");
addImplicitResolver(Tag.NULL, NULL, "~nN\0");
addImplicitResolver(Tag.NULL, EMPTY, null);
addImplicitResolver(Tag.VALUE, VALUE, "=");
addImplicitResolver(Tag.MERGE, MERGE, "<");
}
}
/**
* To skip empty and null values in the {@link PubYamlObject} while writing out the yaml string
*/
private static class SkipEmptyRepresenter extends Representer {
@Override
protected NodeTuple representJavaBeanProperty(Object javaBean, Property property,
Object propertyValue, Tag customTag) {
NodeTuple tuple = super.representJavaBeanProperty(
javaBean,
property,
propertyValue,
customTag);
Node valueNode = tuple.getValueNode();
if (Tag.NULL.equals(valueNode.getTag())) {
return null;// skip 'null' values
}
if (valueNode instanceof CollectionNode) {
if (Tag.SEQ.equals(valueNode.getTag())) {
SequenceNode seq = (SequenceNode) valueNode;
if (seq.getValue().isEmpty()) {
return null;// skip empty lists
}
}
if (Tag.MAP.equals(valueNode.getTag())) {
MappingNode seq = (MappingNode) valueNode;
if (seq.getValue().isEmpty()) {
return null;// skip empty maps
}
}
}
return tuple;
}
}
/**
* Class to preserve the order of fields as declared in {@link PubYamlObject} while writing out
* the yaml string
*/
private static class UnsortedPropertyUtils extends PropertyUtils {
@Override
protected Set<Property> createPropertySet(Class<? extends Object> type, BeanAccess bAccess)
throws IntrospectionException {
Set<Property> result = new LinkedHashSet<Property>(
getPropertiesMap(type, BeanAccess.FIELD).values());
return result;
}
}
/**
* Represents a pub package semantic version. A version is of the form major.minor.patch and can
* be followed by a prerelease or build string eg. 0.4.8+2, 2.34.56-hotfix.issue, 0.54.9+build.2,
* 1.23.5
*/
private static class Version implements Comparable<Version> {
int major;
int minor;
int patch;
String preRelease;
String build;
public Version(String string) {
String[] strings = string.split("\\.");
major = Integer.parseInt(strings[0]);
minor = Integer.parseInt(strings[1]);
if (strings[2].contains("-")) {
String[] s = strings[2].split("-");
patch = Integer.parseInt(s[0]);
preRelease = s[1];
if (strings.length > 3) {
for (int i = 3; i < strings.length; i++) {
preRelease += "." + strings[i];
}
}
} else if (strings[2].contains("+")) {
String[] s = strings[2].split("\\+");
patch = Integer.parseInt(s[0]);
build = s[1];
if (strings.length > 3) {
for (int i = 3; i <= strings.length; i++) {
build += "." + strings[i];
}
}
} else {
patch = Integer.parseInt(strings[2]);
}
}
@Override
public int compareTo(Version other) {
if (major != other.major) {
return new Integer(major).compareTo(new Integer(other.major));
}
if (minor != other.minor) {
return new Integer(minor).compareTo(new Integer(other.minor));
}
if (patch != other.patch) {
return new Integer(patch).compareTo(new Integer(other.patch));
}
if (preRelease != other.preRelease) {
// Pre-releases always come before no pre-release string.
if (preRelease == null) {
return 1;
}
if (other.preRelease == null) {
return -1;
}
return compareStrings(preRelease, other.preRelease);
}
if (build != other.build) {
// Builds always come after no build string.
if (build == null) {
return -1;
}
if (other.build == null) {
return 1;
}
return compareStrings(build, other.build);
}
return 0;
}
@Override
public String toString() {
StringBuffer buffer = new StringBuffer();
buffer.append(major).append(".");
buffer.append(minor).append(".");
buffer.append(patch);
if (preRelease != null) {
buffer.append("-").append(preRelease);
}
if (build != null) {
buffer.append("+").append(build);
}
return buffer.toString();
}
/// Compares the string part of two versions. This is used for the pre-release
/// and build version parts. This follows Rule 12. of the Semantic Versioning
/// spec.
int compareStrings(String a, String b) {
Object[] aParts = splitParts(a);
Object[] bParts = splitParts(b);
for (int i = 0; i < Math.max(aParts.length, bParts.length); i++) {
Object aPart = (i < aParts.length) ? aParts[i] : null;
Object bPart = (i < bParts.length) ? bParts[i] : null;
if (aPart != bPart) {
// Missing parts come before present ones.
if (aPart == null) {
return -1;
}
if (bPart == null) {
return 1;
}
if (aPart instanceof Integer) {
if (bPart instanceof Integer) {
// Compare two numbers.
return ((Integer) aPart).compareTo((Integer) bPart);
} else {
// Numbers come before strings.
return -1;
}
} else {
if (bPart instanceof Integer) {
// Strings come after numbers.
return 1;
} else {
// Compare two strings.
return ((String) aPart).compareTo((String) bPart);
}
}
}
}
return 0;
}
/// Splits a string of dot-delimited identifiers into their component parts.
/// Identifiers that are numeric are converted to numbers.
Object[] splitParts(String text) {
List<Object> list = new ArrayList<Object>();
String[] objects = text.split("\\.");
for (String o : objects) {
try {
Integer i = Integer.parseInt(o);
list.add(i);
} catch (NumberFormatException e) {
list.add(o);
}
}
return list.toArray(new Object[list.size()]);
}
}
public static String PACKAGE_VERSION_EXPRESSION = "(\\d+\\.){2}\\d+([\\+-]([\\.a-zA-Z0-9-\\+])*)?";
public static String PATTERN_PUBSPEC_NAME_LINE = "(?m)^(?:(?!--|').|'(?:''|[^'])*')*(name:.*)$";
public static String VERSION_CONTSTRAINTS_EXPRESSION = "([=]{0,1}[<>]?)|([<>]?[=]{0,1})(\\d+\\.){2}\\d+([\\+-]([\\.a-zA-Z0-9-])*)?";
/**
* Return a yaml string for the given Map
*
* @param pubYamlObject bean for pubspec
* @return String
*/
public static String buildYamlString(Map<String, Object> yamlMap) {
try {
SkipEmptyRepresenter repr = new SkipEmptyRepresenter();
repr.setPropertyUtils(new UnsortedPropertyUtils());
Yaml yaml = new Yaml(repr);
String yamlString = yaml.dumpAsMap(yamlMap);
return yamlString;
} catch (Exception e) {
DartCore.logError(e);
return null;
}
}
/**
* Return a yaml string for the given {@link PubYamlObject}
*
* @param pubYamlObject bean for pubspec
* @return String
*/
public static String buildYamlString(PubYamlObject pubYamlObject) {
try {
SkipEmptyRepresenter repr = new SkipEmptyRepresenter();
repr.setPropertyUtils(new UnsortedPropertyUtils());
Yaml yaml = new Yaml(repr);
String yamlString = yaml.dumpAsMap(pubYamlObject);
return yamlString;
} catch (Exception e) {
DartCore.logError(e);
return null;
}
}
/**
* Return a list of names of the dependencies specified in the pubspec
*
* @param contents String contents of pubspec.yaml
* @return List<String> names of the packages specified as dependencies
*/
@SuppressWarnings("unchecked")
public static List<String> getNamesOfDependencies(String contents) {
Map<String, Object> map = null;
try {
map = parsePubspecYamlToMap(contents);
} catch (ScannerException e) {
DartCore.logError(e);
}
if (map != null) {
Map<String, Object> dependecies = (Map<String, Object>) map.get(PubspecConstants.DEPENDENCIES);
if (dependecies != null && !dependecies.isEmpty()) {
return new ArrayList<String>(dependecies.keySet());
}
}
return null;
}
/**
* Returns a map of installed packages to the respective version number.
*
* @param lockFile the pubspec.lock file
* @return Map<String,String> Map<packageName,versionNumber>
*/
public static Map<String, String> getPackageVersionMap(IResource lockFile) {
try {
return getPackageVersionMap(IFileUtilities.getContents((IFile) lockFile));
} catch (CoreException exception) {
DartCore.logError(exception);
} catch (IOException exception) {
DartCore.logError(exception);
}
return null;
}
/**
* Returns a map of installed packages to the respective version number.
*
* @param lockFileContents string contents of pubspec.lock file
* @return Map<String,String> Map<packageName,versionNumber>
*/
@SuppressWarnings("unchecked")
public static Map<String, String> getPackageVersionMap(String lockFileContents) {
Map<String, String> versionMap = new HashMap<String, String>();
Map<String, Object> map = null;
try {
map = PubYamlUtils.parsePubspecYamlToMap(lockFileContents);
} catch (ScannerException e) {
DartCore.logError(e);
}
if (map != null) {
Map<String, Object> packagesMap = (Map<String, Object>) map.get("packages");
if (packagesMap != null) {
for (String key : packagesMap.keySet()) {
Map<String, Object> attrMap = (Map<String, Object>) packagesMap.get(key);
String version = (String) attrMap.get(PubspecConstants.VERSION);
if (version != null) {
versionMap.put(key, version);
}
}
}
}
return versionMap;
}
/**
* Return the name of the package as specified in pubspec.yaml (name: sample)
*
* @param contents string contents of the pubspec.yaml file
* @return String package name
*/
public static String getPubspecName(String contents) {
Matcher m = Pattern.compile(PubYamlUtils.PATTERN_PUBSPEC_NAME_LINE).matcher(contents);
if (m.find()) {
String[] strings = m.group(1).split(":");
if (strings.length == 2) {
return strings[1].replaceAll(" ", "");
}
}
return null;
}
/**
* Checks if the package dependency version constraint is valid for SDK >= 1.8.5, contstraints can
* be specified as ^1.2.3
*/
public static boolean isValidDependencyConstraintString(String version) {
if (version.isEmpty()) {
return false;
}
// TODO(keertip): add check for specified SDK version
if (version.startsWith("^")) {
return version.substring(1).matches(PACKAGE_VERSION_EXPRESSION);
}
return isValidVersionConstraintString(version);
}
/**
* Checks if the string has a valid version constraint format ">=1.2.3 <2.0.0", "1.0.0", "<1.5.0"
*/
public static boolean isValidVersionConstraintString(String version) {
if (!version.equals(PubspecConstants.ANY) && !version.isEmpty()) {
String[] versions = version.split(" ");
if (versions.length > 2) {
return false;
} else {
for (String ver : versions) {
if (!isValidVersionConstraint(ver)) {
return false;
}
}
}
}
return true;
}
/**
* Parse the pubspec.yaml string contents to an Map
*/
@SuppressWarnings("unchecked")
public static Map<String, Object> parsePubspecYamlToMap(String contents) throws ScannerException {
Yaml yaml = new Yaml(
new SafeConstructor(),
new Representer(),
new DumperOptions(),
new CustomResolver());
// https://code.google.com/p/dart/issues/detail?id=20712
//
// [org.json.JSONObject] escapes "</" to "<\\/" which breaks [yaml.load], because
// escaped forward slashes are part of JSON but not part of YAML:
// http://en.wikipedia.org/wiki/JSON#YAML .
//
// Experiments with [JSONObject] indicate that only "/" after "<" is escaped
// by [JSONObject]; a lone "/" is not escaped, and it would be dangerous to
// instead do [contents.replace("\\/", "/")] here, since then e.g.
//
// \\/ --JSONObject--> \\\\/ --here--> \\/ --SnakeYAML--> Error
//
// would be wrong.
Object o = yaml.load(contents.replace("<\\/", "</"));
Map<String, Object> map = new HashMap<String, Object>();
if (o instanceof Map) {
map.putAll((Map<String, Object>) o);
}
return map;
}
public static String[] sortVersionArray(String[] versionList) {
List<Version> versions = new ArrayList<PubYamlUtils.Version>();
for (Object o : versionList) {
versions.add(new Version(o.toString()));
}
Collections.sort(versions);
List<String> strings = new ArrayList<String>();
for (Version version : versions) {
strings.add(version.toString());
}
return strings.toArray(new String[strings.size()]);
}
private static boolean isValidVersionConstraint(String string) {
int index = 0;
while (index + 1 <= string.length() && !string.substring(index, index + 1).matches("[0-9]")) {
index++;
}
if (index == string.length()) {
return false;
}
if (index > 2) { // can be single [<>] or two char length [=][<>]
return false;
}
if (index == 1) {
String substring = string.substring(0, 1);
if (!substring.equals("<") && !substring.equals(">")) {
return false;
}
}
if (index == 2) {
String substring = string.substring(0, 2);
if (!substring.equals(">=") && !substring.equals("<=") && !substring.equals("=>")
&& !substring.equals("=<")) {
return false;
}
}
if (string.substring(index).matches(PACKAGE_VERSION_EXPRESSION)) {
return true;
}
return false;
}
}