/*******************************************************************************
* Copyright (c) 2014, 2017 GoPivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* GoPivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.eclipse.boot.core.initializr;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.core.runtime.CoreException;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;
import org.osgi.framework.Version;
import org.osgi.framework.VersionRange;
import org.springframework.ide.eclipse.boot.util.Log;
import org.springsource.ide.eclipse.commons.frameworks.core.downloadmanager.URLConnectionFactory;
import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil;
/**
* This class is the 'parsed' form of the json metadata for spring intializr service.
*
* See: https://github.com/spring-io/initializr/wiki/Initializr-metadata-json-format
*
* @author Kris De Volder
*/
public class InitializrServiceSpec {
private JSONObject data;
public static final String JSON_CONTENT_TYPE_HEADER = "application/vnd.initializr.v2.1+json";
/**
* Boot version link template variable
*/
public static final String BOOT_VERSION_LINK_TEMPLATE_VARIABLE = "bootVersion";
/**
* Pattern matching link template variables
*/
private static final Pattern LINK_TEMPLATE_VARIABLE_PATTERN = Pattern.compile("\\{(.*?)\\}");
public InitializrServiceSpec(JSONObject jsonObject) {
this.data = jsonObject;
}
public static InitializrServiceSpec parseFrom(URLConnectionFactory urlConnectionFactory, URL url) throws IOException, Exception {
URLConnection conn = null;
InputStream input = null;
try {
conn = urlConnectionFactory.createConnection(url);
conn.addRequestProperty("Accept", JSON_CONTENT_TYPE_HEADER);
conn.connect();
input = conn.getInputStream();
return parseFrom(input);
} finally {
if (input!=null) {
try {
input.close();
} catch (IOException e) {
}
}
}
}
public static InitializrServiceSpec parseFrom(InputStream input) throws Exception {
return new InitializrServiceSpec(new JSONObject(new JSONTokener(new InputStreamReader(input, "utf8"))));
}
/////////////////////////////////////////////////
public static abstract class Nameable {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public static class Type extends Option {
private String action;
public void setAction(String action) {
this.action = action;
}
public String getAction() {
return action;
}
}
public static class Dependency extends Nameable implements IdAble{
private String id;
private String description;
private String versionRange;
private Links links;
public String getId() {
return id;
}
@Override
public String getName() {
String name = super.getName();
return name!=null?name:getId();
}
public void setId(String id) {
this.id = id;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Links getLinks() {
return links;
}
public void setLinks(Links links) {
this.links = links;
}
public static Dependency[] from(JSONArray values) throws JSONException {
Dependency[] deps = new Dependency[values.length()];
for (int i = 0; i < deps.length; i++) {
JSONObject obj = values.getJSONObject(i);
deps[i] = new Dependency();
deps[i].setId(obj.getString("id"));
deps[i].setName(obj.optString("name"));
deps[i].setDescription(obj.optString("description"));
deps[i].setVersionRange(obj.optString("versionRange"));
JSONObject linksObject = obj.optJSONObject("_links");
if (linksObject != null) {
deps[i].setLinks(Links.from(linksObject));
}
}
return deps;
}
public void setVersionRange(String range) {
this.versionRange = range;
}
public String getVersionRange() {
return versionRange;
}
@Override
public String toString() {
return "Dep("+id+","+description+","+versionRange+")";
}
public boolean isSupportedFor(String bootVersion) {
try {
if (StringUtils.isNotBlank(versionRange)) {
final VersionRange range = new VersionRange(versionRange);
String versionStr = bootVersion;
if (versionStr!=null) {
Version version = new Version(versionStr.replace("BUILD-SNAPSHOT", "ZZZZZZZZZZZZZ"));
//replacement of BS -> ZZ: see bug https://www.pivotaltracker.com/story/show/100963226
return range.includes(version);
}
}
} catch (Exception e) {
Log.log(e);
}
return true;
}
}
public static class Links {
private Link[] guides = new Link[0];
private Link[] references = new Link[0];
public Link[] getGuides() {
return guides;
}
public void setGuides(Link[] guides) {
this.guides = guides;
}
public Link[] getReferences() {
return references;
}
public void setReferences(Link[] references) {
this.references = references;
}
public static Links from(JSONObject json) throws JSONException {
Links links = new Links();
Object guidesObj = json.opt("guide");
if (guidesObj instanceof JSONArray) {
links.setGuides(Link.from((JSONArray)guidesObj));
} else if (guidesObj instanceof JSONObject) {
links.setGuides(new Link[] {Link.from((JSONObject)guidesObj)});
}
Object refsObj = json.opt("reference");
if (refsObj instanceof JSONArray) {
links.setReferences(Link.from((JSONArray)refsObj));
} else if (refsObj instanceof JSONObject) {
links.setReferences(new Link[] {Link.from((JSONObject)refsObj)});
}
return links;
}
}
public static class Link {
private String href;
private String title;
private boolean templated;
public String getHref() {
return href;
}
public void setHref(String href) {
this.href = href;
}
public String getTitle() {
return title;
}
public void setTitle(String description) {
this.title = description;
}
public boolean isTemplated() {
return templated;
}
public void setTemplated(boolean templated) {
this.templated = templated;
}
public static Link[] from(JSONArray values) throws JSONException{
Link[] links = new Link[values.length()];
for (int i = 0; i < values.length(); i++) {
JSONObject obj = values.getJSONObject(i);
links[i] = from(obj);
}
return links;
}
public static Link from(JSONObject obj) throws JSONException{
Link link = new Link();
link.setHref(obj.optString("href", null));
link.setTitle(obj.optString("title", null));
link.setTemplated(obj.optBoolean("templated", false));
return link;
}
}
public static class DependencyGroup extends Nameable {
private Dependency[] content;
public Dependency[] getContent() {
return content;
}
public void setContent(Dependency[] content) {
this.content = content;
}
}
public static class Option extends Nameable {
private String id;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
private boolean isDefault;
public boolean isDefault() {
return isDefault;
}
public void setDefault(boolean isDefault) {
this.isDefault = isDefault;
}
}
/////////////////////////////////////////////////////////////////
@SuppressWarnings("unchecked")
public Map<String, String> getTextInputs() throws JSONException {
Map<String,String> defaults = new HashMap<>();
Iterator<String> props = data.keys();
while(props.hasNext()) {
String key = props.next();
JSONObject obj = data.getJSONObject(key);
String type = obj.optString("type");
if ("text".equals(type)) {
defaults.put(key, obj.optString("default", ""));
}
}
return defaults;
}
public Type[] getTypeOptions(String groupName) {
try {
JSONObject obj = data.optJSONObject(groupName);
if (obj!=null && "action".equals(obj.optString("type"))) {
String defaultValue = obj.optString("default", "");
JSONArray arr = obj.getJSONArray("values");
Type[] options = new Type[arr.length()];
for (int i = 0; i < options.length; i++) {
JSONObject option = arr.getJSONObject(i);
options[i] = new Type();
String id = option.getString("id");
String name = option.getString("name");
String action = option.getString("action");
options[i].setId(id);
options[i].setName(name);
options[i].setAction(action);
options[i].setDefault(id.equals(defaultValue));
}
return options;
}
} catch (JSONException e) {
//ignore
}
return new Type[0];
}
public Option[] getSingleSelectOptions(String groupName) {
try {
JSONObject obj = data.optJSONObject(groupName);
if (obj!=null && "single-select".equals(obj.optString("type"))) {
String defaultValue = obj.optString("default", "");
JSONArray arr = obj.getJSONArray("values");
Option[] options = new Option[arr.length()];
for (int i = 0; i < options.length; i++) {
JSONObject option = arr.getJSONObject(i);
options[i] = new Option();
String id = option.getString("id");
String name = option.getString("name");
options[i].setId(id);
options[i].setName(name);
options[i].setDefault(id.equals(defaultValue));
}
return options;
}
} catch (JSONException e) {
//ignore
}
return new Option[0];
}
public DependencyGroup[] getDependencies() {
return getHierarchicalMultiSelect("dependencies");
}
private DependencyGroup[] getHierarchicalMultiSelect(String prop) {
try {
JSONObject obj = data.optJSONObject(prop);
if (obj!=null && "hierarchical-multi-select".equals(obj.optString("type"))) {
JSONArray arr = obj.getJSONArray("values");
DependencyGroup[] groups = new DependencyGroup[arr.length()];
for (int i = 0; i < groups.length; i++) {
JSONObject group = arr.getJSONObject(i);
groups[i] = new DependencyGroup();
String name = group.getString("name");
JSONArray values = group.getJSONArray("values");
groups[i].setName(name);
groups[i].setContent(Dependency.from(values));
}
return groups;
}
} catch (JSONException e) {
//ignore
}
return new DependencyGroup[0];
}
public static String substituteTemplateVariables(String template, Map<String, String> variableValues) throws CoreException {
Matcher matcher = LINK_TEMPLATE_VARIABLE_PATTERN.matcher(template);
String result = template;
while (matcher.find()) {
String variable = matcher.group(1);
String value = variableValues.get(variable);
if (value == null) {
throw ExceptionUtil.coreException("Initializr link has unknown " + variable + " in the template " + template);
} else {
result = result.replaceAll("\\{" + variable + "\\}", value);
}
}
return result;
}
}