/*
* The MIT License
*
* Copyright (c) 2013-2014, CloudBees, Inc., Amadeus IT Group
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package org.cloudbees.literate.impl;
import edu.umd.cs.findbugs.annotations.NonNull;
import org.apache.commons.io.IOUtils;
import org.cloudbees.literate.api.v1.ExecutionEnvironment;
import org.cloudbees.literate.api.v1.ProjectModel;
import org.cloudbees.literate.api.v1.ProjectModel.Builder;
import org.cloudbees.literate.api.v1.ProjectModelBuildingException;
import org.cloudbees.literate.api.v1.ProjectModelRequest;
import org.cloudbees.literate.api.v1.vfs.ProjectRepository;
import org.cloudbees.literate.impl.yaml.Language;
import org.cloudbees.literate.impl.yaml.environment.EnvironmentDecorator;
import org.cloudbees.literate.spi.v1.ProjectModelBuilder;
import org.yaml.snakeyaml.Yaml;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.ServiceLoader;
import java.util.Set;
/**
* A {@link ProjectModelBuilder} that uses a YAML file as the source of its
* {@link ProjectModel}
*/
@ProjectModelBuilder.Priority(-1000)
public class YamlProjectModelBuilder implements ProjectModelBuilder {
/**
* {@inheritDoc}
*/
//@Override
public ProjectModel build(ProjectModelRequest request) throws IOException, ProjectModelBuildingException {
for (String name : markerFiles(request.getBaseName())) {
if (request.getRepository().isFile(name)) {
return new Parser(request).parseProjectModel(request.getRepository(), name);
}
}
throw new ProjectModelBuildingException("Not a YAML based literate project");
}
/**
* {@inheritDoc}
*/
@NonNull
public Collection<String> markerFiles(@NonNull String basename) {
return Arrays.asList("." + basename + ".yml", ".travis.yml");
}
private static class Parser {
private final String[] buildIds;
private final String environmentsId;
private final String envvarsId;
private final String languageId;
public Parser(@NonNull ProjectModelRequest request) {
this.buildIds = request.getBuildId().split("[, ]");
this.environmentsId = request.getEnvironmentsId();
this.envvarsId = request.getEnvvarsId();
this.languageId = "language";
}
/**
* Returns the list of commands contained in the object given the
* provided environment
*
* @param value The input model containing the commands
* @param environment The environment we wish to obtain commands for
* @return The list of commands applicable for this environment
*/
@SuppressWarnings("unchecked")
public static List<String> getCommands(Object value, ExecutionEnvironment environment) {
List<String> commands = new ArrayList<String>();
// if a specific environment command is specified and no environment
// exists,
// (value instanceof Map and environment is null)
// the command is ignored
if (value instanceof Map && environment != null) {
Map<String, Object> map = (Map<String, Object>) value;
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (environment.getLabels().contains(entry.getKey())) {
commands.addAll(getCommands(entry.getValue(), environment));
}
}
} else if (value instanceof List) {
for (Object command : (List<Object>) value) {
commands.addAll(getCommands(command, environment));
}
} else if (value instanceof String) {
commands.add((String) value);
}
return commands;
}
/**
* Parses a file into a literate project model
*
* @param repository The repository containing the file
* @param name the name of the file to parse
* @return A project model built from the given file
* @throws IOException in case we encounter I/O issue while accessing
* the repository
* @throws ProjectModelBuildingException in case the file contains an
* invalid model
*/
public ProjectModel parseProjectModel(ProjectRepository repository, String name) throws IOException, ProjectModelBuildingException {
InputStream stream = repository.get(name);
try {
Yaml yaml = new Yaml();
@SuppressWarnings("unchecked")
Map<String, Object> model = (Map<String, Object>) yaml.load(stream);
Map<String, Object> decoratedModel = decorateWithLanguage(model, repository);
return internalBuild(decoratedModel);
} finally {
IOUtils.closeQuietly(stream);
}
}
private Map<String, Object> decorateWithLanguage(Map<String, Object> model, ProjectRepository repository) throws IOException {
String language = (String) model.get(languageId);
for (Language l : ServiceLoader.load(Language.class, getClass().getClassLoader())) {
if (l.supported().contains(language)) {
return l.decorate(model, repository);
}
}
return model;
}
private ProjectModel internalBuild(Map<String, Object> model) throws ProjectModelBuildingException {
Builder builder = ProjectModel.builder();
List<ExecutionEnvironment> environments = consumeEnvironmentSection(model);
environments = decorateWithEnvironmentVariables(environments, model);
builder.addEnvironments(environments);
Map<ExecutionEnvironment, List<String>> build = new HashMap<ExecutionEnvironment, List<String>>();
for (String step : buildIds) {
Object value = model.get(step);
if (value != null) {
for (ExecutionEnvironment environment : environments) {
if (build.containsKey(environment)) {
build.get(environment).addAll(getCommands(value, environment));
} else {
build.put(environment, new ArrayList<String>(getCommands(value, environment)));
}
}
}
}
builder.addBuild(build);
addTasks(builder, model);
return builder.build();
}
private List<ExecutionEnvironment> decorateWithEnvironmentVariables(List<ExecutionEnvironment> environments, Map<String, Object> model)
throws ProjectModelBuildingException {
List<ExecutionEnvironment> result = new ArrayList<ExecutionEnvironment>(environments);
Object object = model.get(envvarsId);
if (object instanceof String) {
result = applyDecorators(result, Collections.singleton((String) object), "matrix");
} else if (object instanceof Map) {
Map<String, Object> map = checkMap((Map) object);
for (Entry<String, Object> entry : map.entrySet()) {
Collection<String> variables;
String key = entry.getKey();
Object value = entry.getValue();
if (value instanceof Collection) {
variables = checkCollection((Collection) value);
} else if (value instanceof String) {
variables = Collections.singleton((String) value);
} else {
throw invalidEnvironmentModel(map);
}
result = applyDecorators(result, variables, key);
}
}
return result;
}
private List<ExecutionEnvironment> applyDecorators(List<ExecutionEnvironment> envs, Collection<String> variables, String sectionName) {
List<ExecutionEnvironment> input = new ArrayList<ExecutionEnvironment>(envs);
List<ExecutionEnvironment> output = new ArrayList<ExecutionEnvironment>();
ServiceLoader<EnvironmentDecorator> decorators = ServiceLoader.load(EnvironmentDecorator.class, getClass().getClassLoader());
for (EnvironmentDecorator decorator : decorators) {
if (decorator.acceptSection(sectionName)) {
for (ExecutionEnvironment e : input) {
output.addAll(decorator.decorate(e, variables));
}
input = output;
}
}
return output;
}
private Collection<String> checkCollection(Collection raw) throws ProjectModelBuildingException {
for (Object object : raw) {
if (!(object instanceof String)) {
throw invalidEnvironmentModel(raw);
}
}
return raw;
}
private Map<String, Object> checkMap(Map rawMap) throws ProjectModelBuildingException {
Set entrySet = rawMap.entrySet();
for (Object object : entrySet) {
Entry e = (Entry) object;
if (!(e.getKey() instanceof String)) {
throw invalidEnvironmentModel(rawMap);
}
}
return rawMap;
}
private ProjectModelBuildingException invalidEnvironmentModel(Collection rawCollection) {
// TODO: Make a better error message
return new ProjectModelBuildingException("Specified environment variables are invalid.");
}
private ProjectModelBuildingException invalidEnvironmentModel(Map rawMap) {
// TODO: Make a better error message
return new ProjectModelBuildingException("Specified environment variables are invalid.");
}
/**
* @param model The input model
* @return a list of {@link ExecutionEnvironment} parsed from the input
* model
*/
private List<ExecutionEnvironment> consumeEnvironmentSection(Map<String, Object> model) {
Object value = model.get(environmentsId);
return parseEnvironment(value, 0);
}
/**
* Parse a part of the environment section
*
* @param value The current node in the raw Yaml model
* @param depth The current depth in the model
* @return A list of {@link ExecutionEnvironment} parsed from the input
* model
*/
@SuppressWarnings("unchecked")
private List<ExecutionEnvironment> parseEnvironment(Object value, int depth) {
if (value instanceof Map) {
return parseEnvironment((Map<String, Object>) value, depth);
} else if (value instanceof List) {
return parseEnvironment((List<Object>) value, depth);
} else if (value instanceof String) {
return Collections.singletonList(new ExecutionEnvironment((String) value));
}
return Collections.singletonList(new ExecutionEnvironment());
}
private List<ExecutionEnvironment> parseEnvironment(List<Object> list, int depth) {
List<ExecutionEnvironment> environments = new ArrayList<ExecutionEnvironment>();
boolean isSimpleList = (depth == 0);
List<String> simpleList = new ArrayList<String>();
for (Object string : list) {
if (string instanceof String) {
simpleList.add((String) string);
} else {
isSimpleList = false;
}
}
if (isSimpleList) {
environments.add(new ExecutionEnvironment(simpleList));
} else {
environments.addAll(parseComplexList(list));
}
return environments;
}
@SuppressWarnings("unchecked")
private List<ExecutionEnvironment> parseComplexList(List<Object> list) {
List<ExecutionEnvironment> output = new ArrayList<ExecutionEnvironment>();
for (Object environment : list) {
if (environment instanceof List) {
output.add(new ExecutionEnvironment((List<String>) environment));
} else if (environment instanceof String) {
output.add(new ExecutionEnvironment((String) environment));
}
}
return output;
}
private List<ExecutionEnvironment> parseEnvironment(Map<String, Object> map, int depth) {
List<ExecutionEnvironment> environments = new ArrayList<ExecutionEnvironment>();
for (Map.Entry<String, Object> entry : map.entrySet()) {
for (ExecutionEnvironment list : parseEnvironment(entry.getValue(), depth + 1)) {
environments.add(new ExecutionEnvironment(list, entry.getKey()));
}
}
return environments;
}
/**
* Add tasks to the builder from the input model, excluding some id
* already picked up through buildIds
*
* @param builder The ProjectModel builder
* @param model The raw input model
*/
private void addTasks(Builder builder, Map<String, Object> model) {
for (Entry<String, Object> entry : model.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (!buildIdsContains(key)) {
builder.addTask(key, getCommands(value, ExecutionEnvironment.any()));
}
}
}
private boolean buildIdsContains(String value) {
return Arrays.binarySearch(buildIds, value) >= 0;
}
}
}