/*
* 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.pub;
import com.google.common.collect.Lists;
import com.google.dart.tools.core.DartCore;
import com.google.dart.tools.core.pub.DependencyObject.Type;
import com.google.dart.tools.core.utilities.yaml.PubYamlUtils;
import org.eclipse.core.resources.IFile;
import org.yaml.snakeyaml.error.Mark;
import org.yaml.snakeyaml.error.MarkedYAMLException;
import java.io.ByteArrayInputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Model representing the pubspec.
*
* @coverage dart.tools.core.pub
*/
public class PubspecModel {
public static final String PUBSPEC_MARKER = DartCore.PLUGIN_ID + ".pubspecIssue";
private static String EMPTY_STRING = "";
private static final Comparator<String> fieldComparator = new Comparator<String>() {
Random random = new Random();
List<String> fieldNames = Arrays.asList(
"name",
"version",
"author",
"authors",
"description",
"homepage",
"environment",
"sdk",
"documentation",
"dependencies",
"dev_dependencies",
"dependency_overrides",
"transformers");
@Override
public int compare(String o1, String o2) {
// make sure unknown fields are written at the end
// TODO(keertip): sort unknowns
int index1 = fieldNames.indexOf(o1) != -1 ? fieldNames.indexOf(o1) : random.nextInt(50) + 11;
int index2 = fieldNames.indexOf(o2) != -1 ? fieldNames.indexOf(o2) : random.nextInt(50) + 11;
return index1 - index2;
}
};
private ArrayList<IModelListener> modelListeners;
private String name = EMPTY_STRING;
private String version = EMPTY_STRING;
private String description = EMPTY_STRING;
private String author = EMPTY_STRING;
private String homepage = EMPTY_STRING;
private String sdkVersion = EMPTY_STRING;
private String documentation = EMPTY_STRING;
private boolean errorOnParse = false;
private ArrayList<DependencyObject> dependencies;
private ArrayList<Object> transformers;
private String comments;
private boolean isDirty = false;
private IFile file;
private Map<String, Object> yamlMap = new HashMap<String, Object>();
private final List<PubspecException> exceptions = Lists.newArrayList();
public PubspecModel(IFile file, String contents) {
this.file = file;
initModel(contents);
}
public PubspecModel(String contents) {
initModel(contents);
}
public void add(DependencyObject[] objs, String eventType) {
if (objs.length > 0) {
for (int i = 0; i < objs.length; i++) {
if (objs[i] != null) {
dependencies.add(objs[i]);
objs[i].setModel(this);
}
}
fireModelChanged(objs, eventType);
}
}
public void addModelListener(IModelListener listener) {
if (!modelListeners.contains(listener)) {
modelListeners.add(listener);
}
}
public void fireModelChanged(Object[] objects, String type) {
for (int i = 0; i < modelListeners.size(); i++) {
modelListeners.get(i).modelChanged(objects, type);
}
}
public String getAuthor() {
return author;
}
/**
* Return the model contents as a yaml string
*/
public String getContents() {
// append comments at end of pubspec
updateMapValues();
TreeMap<String, Object> map = new TreeMap<String, Object>(fieldComparator);
map.putAll(yamlMap);
return PubYamlUtils.buildYamlString(map) + comments;
}
public Object[] getDependecies() {
return dependencies.toArray();
}
public String getDescription() {
return description;
}
public String getDocumentation() {
return documentation;
}
public List<PubspecException> getExceptions() {
return exceptions;
}
public String getHomepage() {
return homepage;
}
public String getName() {
return name;
}
public String getSdkVersion() {
return sdkVersion;
}
public ArrayList<Object> getTransformers() {
return transformers;
}
public String getVersion() {
return version;
}
public boolean isDirty() {
return isDirty;
}
public boolean isErrorOnParse() {
return errorOnParse;
}
public void remove(DependencyObject[] dependencyObjects, boolean notify) {
for (int i = 0; i < dependencyObjects.length; i++) {
dependencies.remove(dependencyObjects[i]);
dependencyObjects[i].setModel(null);
}
if (notify) {
fireModelChanged(dependencyObjects, IModelListener.REMOVED);
}
}
public void removeModelListener(IModelListener listener) {
modelListeners.remove(listener);
}
/**
* Saves the content of this model to its original {@link IFile}.
*/
public void save() throws Exception {
if (file == null) {
new IllegalStateException("This model doesn't have a file.");
}
String contents = getContents();
byte[] contentBytes = contents.getBytes("UTF-8");
file.setContents(new ByteArrayInputStream(contentBytes), true, true, null);
}
public void setAuthor(String author) {
this.author = author;
}
public void setDescription(String description) {
this.description = description;
}
public void setDirty(boolean isDirty) {
this.isDirty = isDirty;
}
public void setDocumentation(String documentation) {
this.documentation = documentation;
}
public void setHomepage(String homepage) {
this.homepage = homepage;
}
public void setName(String name) {
this.name = name;
}
public void setSdkVersion(String sdkVersion) {
this.sdkVersion = sdkVersion;
}
/**
* Initialize the model with the values in the yaml string
*/
public void setValuesFromString(String yamlString) {
if (yamlString != null) {
comments = getComments(yamlString);
if (!yamlString.isEmpty()) {
try {
exceptions.clear();
errorOnParse = false;
yamlMap = PubYamlUtils.parsePubspecYamlToMap(yamlString);
setValuesFromMap(yamlMap);
} catch (MarkedYAMLException exception) {
errorOnParse = true;
Mark mark = exception.getProblemMark();
exceptions.add(new PubspecException(
"Invalid syntax: " + exception.getProblem(),
mark.getLine(),
mark.getIndex(),
mark.getIndex() + 2));
}
}
}
}
public void setVersion(String version) {
this.version = version;
}
private void clearModelFields() {
isDirty = false;
name = version = description = homepage = author = sdkVersion = comments = documentation = EMPTY_STRING;
dependencies.clear();
}
// search for comments and store them so that we don't lose it altogether
// TODO(keertip): remove when we can do micro edits
private String getComments(String yamlString) {
Matcher m = Pattern.compile("(?m)^(?:(?!--|').|'(?:''|[^'])*')*(#.*)$").matcher(yamlString);
StringBuilder builder = new StringBuilder();
while (m.find()) {
builder.append("\n");
builder.append(m.group(1));
}
return builder.toString();
}
private void initModel(String contents) {
modelListeners = new ArrayList<IModelListener>();
dependencies = new ArrayList<DependencyObject>();
setValuesFromString(contents);
}
// Support for dependencies hosted on pub.dartlang.org and git.
// TODO(keertip): Add support for hosted on other than pub.dartlang.org
@SuppressWarnings("unchecked")
private DependencyObject[] processDependencies(Map<String, Object> yamlDep, boolean isDev) {
List<DependencyObject> deps = new ArrayList<DependencyObject>();
if (yamlDep != null) {
for (String name : yamlDep.keySet()) {
DependencyObject d = new DependencyObject(name);
d.setForDevelopment(isDev);
Object value = yamlDep.get(name);
if (value instanceof String) {
d.setVersion((String) value);
} else if (value instanceof Map) {
Map<String, Object> values = (Map<String, Object>) value;
for (String key : values.keySet()) {
if (key.equals(PubspecConstants.VERSION)) {
d.setVersion((String) values.get(key));
}
if (key.equals(PubspecConstants.PATH)) {
d.setPath((String) values.get(key));
d.setType(Type.PATH);
}
if (key.equals(PubspecConstants.GIT)) {
d.setType(Type.GIT);
Object fields = values.get(key);
if (fields instanceof String) {
d.setPath((String) fields);
}
if (fields instanceof Map) {
Map<String, Object> map = (Map<String, Object>) fields;
for (String mapKey : map.keySet()) {
if (mapKey.equals(PubspecConstants.URL)) {
d.setPath((String) map.get(mapKey));
}
if (mapKey.equals(PubspecConstants.REF)) {
d.setGitRef((String) map.get(mapKey));
}
}
}
}
if (key.endsWith(PubspecConstants.HOSTED)) {
Object fields = values.get(key);
if (fields instanceof Map) {
Map<String, Object> map = (Map<String, Object>) fields;
for (String mapKey : map.keySet()) {
if (mapKey.equals(PubspecConstants.URL)) {
d.setPath((String) map.get(mapKey));
}
}
}
}
}
}
deps.add(d);
}
}
return deps.toArray(new DependencyObject[deps.size()]);
}
@SuppressWarnings("unchecked")
private ArrayList<Object> safelyAsArray(Map<String, Object> map, String key) {
Object value = map.get(key);
if (value == null) {
return new ArrayList<Object>();
}
if (value instanceof ArrayList) {
return (ArrayList<Object>) value;
}
return new ArrayList<Object>();
}
@SuppressWarnings("unchecked")
private Map<String, Object> safelyAsMap(Map<String, Object> map, String key) {
Object value = map.get(key);
if (value == null) {
return new HashMap<String, Object>();
}
if (value instanceof Map) {
return (Map<String, Object>) value;
}
return new HashMap<String, Object>();
}
private String safelyAsString(Map<String, Object> map, String key) {
Object value = map.get(key);
if (value == null) {
return EMPTY_STRING;
}
if (value instanceof String) {
return (String) value;
}
return EMPTY_STRING;
}
@SuppressWarnings("unchecked")
private void setValuesFromMap(Map<String, Object> pubspecMap) {
if (pubspecMap != null) {
clearModelFields();
name = safelyAsString(pubspecMap, PubspecConstants.NAME);
version = safelyAsString(pubspecMap, PubspecConstants.VERSION);
author = safelyAsString(pubspecMap, PubspecConstants.AUTHOR);
if (pubspecMap.get(PubspecConstants.AUTHORS) != null) {
Object o = pubspecMap.get(PubspecConstants.AUTHORS);
if (o instanceof List) {
List<String> authors = (List<String>) pubspecMap.get(PubspecConstants.AUTHORS);
author = authors.get(0);
for (int i = 1; i < authors.size(); i++) {
author += ", " + authors.get(i);
}
} else {
author = (String) o;
}
}
if (pubspecMap.get(PubspecConstants.ENVIRONMENT) != null) {
Map<String, Object> env = safelyAsMap(pubspecMap, PubspecConstants.ENVIRONMENT);
sdkVersion = safelyAsString(env, PubspecConstants.SDK_VERSION);
} else {
sdkVersion = EMPTY_STRING;
}
description = safelyAsString(pubspecMap, PubspecConstants.DESCRIPTION);
homepage = safelyAsString(pubspecMap, PubspecConstants.HOMEPAGE);
documentation = safelyAsString(pubspecMap, PubspecConstants.DOCUMENTATION);
transformers = safelyAsArray(pubspecMap, PubspecConstants.TRANSFORMERS);
add(
processDependencies(safelyAsMap(pubspecMap, PubspecConstants.DEPENDENCIES), false),
IModelListener.REFRESH);
add(
processDependencies(safelyAsMap(pubspecMap, PubspecConstants.DEV_DEPENDENCIES), true),
IModelListener.REFRESH);
errorOnParse = false;
} else {
errorOnParse = true;
}
}
// update the map of the original contents with the values from the model
private void updateMapValues() {
if (yamlMap != null) {
yamlMap.put(PubspecConstants.NAME, name);
yamlMap.remove(PubspecConstants.VERSION);
if (!version.isEmpty()) {
yamlMap.put(PubspecConstants.VERSION, version);
}
yamlMap.remove(PubspecConstants.DESCRIPTION);
if (!description.isEmpty()) {
yamlMap.put(PubspecConstants.DESCRIPTION, description);
}
yamlMap.remove(PubspecConstants.AUTHOR);
yamlMap.remove(PubspecConstants.AUTHORS);
if (!author.isEmpty()) {
if (author.indexOf(',') != -1) { // comma separated list
String[] strings = author.split(",");
for (int i = 0; i < strings.length; i++) {
strings[i] = strings[i].trim();
}
yamlMap.put(PubspecConstants.AUTHORS, Arrays.asList(strings));
} else {
yamlMap.put(PubspecConstants.AUTHOR, author);
}
}
yamlMap.remove(PubspecConstants.HOMEPAGE);
if (!homepage.isEmpty()) {
yamlMap.put(PubspecConstants.HOMEPAGE, homepage);
}
yamlMap.remove(PubspecConstants.DOCUMENTATION);
if (!documentation.isEmpty()) {
yamlMap.put(PubspecConstants.DOCUMENTATION, documentation);
}
yamlMap.remove(PubspecConstants.ENVIRONMENT);
if (!sdkVersion.isEmpty()) {
Map<String, Object> map = new HashMap<String, Object>();
map.put(PubspecConstants.SDK_VERSION, sdkVersion);
yamlMap.put(PubspecConstants.ENVIRONMENT, map);
}
Map<String, Object> dependenciesMap = new HashMap<String, Object>();
Map<String, Object> devDependenciesMap = new HashMap<String, Object>();
for (DependencyObject dep : dependencies) {
if (dep.getType().equals(Type.HOSTED)) {
if (dep.getPath() != null && !dep.getPath().isEmpty()) {
Map<String, Object> map = new HashMap<String, Object>();
map.put(PubspecConstants.NAME, dep.getName());
map.put(PubspecConstants.URL, dep.getPath());
Map<String, Object> dMap = new HashMap<String, Object>();
dMap.put(PubspecConstants.HOSTED, map);
if (!dep.getVersion().isEmpty()) {
dMap.put(PubspecConstants.VERSION, dep.getVersion());
}
if (dep.isForDevelopment()) {
devDependenciesMap.put(dep.getName(), dMap);
} else {
dependenciesMap.put(dep.getName(), dMap);
}
} else {
if (dep.getVersion().isEmpty()) {
if (dep.isForDevelopment()) {
devDependenciesMap.put(dep.getName(), PubspecConstants.ANY);
} else {
dependenciesMap.put(dep.getName(), PubspecConstants.ANY);
}
} else {
if (dep.isForDevelopment()) {
devDependenciesMap.put(dep.getName(), dep.getVersion());
} else {
dependenciesMap.put(dep.getName(), dep.getVersion());
}
}
}
} else if (dep.getType().equals(Type.GIT)) {
Map<String, Object> gitMap = new HashMap<String, Object>();
if (dep.getGitRef() != null && !dep.getGitRef().isEmpty()) {
Map<String, String> map = new HashMap<String, String>();
map.put(PubspecConstants.REF, dep.getGitRef());
map.put(PubspecConstants.URL, dep.getPath());
gitMap.put(PubspecConstants.GIT, map);
} else {
gitMap.put(PubspecConstants.GIT, dep.getPath());
}
if (!dep.getVersion().equals(PubspecConstants.ANY) && !dep.getVersion().isEmpty()) {
gitMap.put(PubspecConstants.VERSION, dep.getVersion());
}
if (dep.isForDevelopment()) {
devDependenciesMap.put(dep.getName(), gitMap);
} else {
dependenciesMap.put(dep.getName(), gitMap);
}
} else {
Map<String, Object> pathMap = new HashMap<String, Object>();
pathMap.put(PubspecConstants.PATH, dep.getPath());
if (dep.isForDevelopment()) {
devDependenciesMap.put(dep.getName(), pathMap);
} else {
dependenciesMap.put(dep.getName(), pathMap);
}
}
}
yamlMap.remove(PubspecConstants.DEPENDENCIES);
if (!dependenciesMap.isEmpty()) {
yamlMap.put(PubspecConstants.DEPENDENCIES, new TreeMap<String, Object>(dependenciesMap));
}
yamlMap.remove(PubspecConstants.DEV_DEPENDENCIES);
if (!devDependenciesMap.isEmpty()) {
yamlMap.put(PubspecConstants.DEV_DEPENDENCIES, new TreeMap<String, Object>(
devDependenciesMap));
}
yamlMap.remove(PubspecConstants.TRANSFORMERS);
if (transformers != null && !transformers.isEmpty()) {
yamlMap.put(PubspecConstants.TRANSFORMERS, transformers);
}
}
}
}