blob: ba751c4528a69130104006c3bbd5ef77d73593ff [file] [log] [blame]
/*
* Copyright (c) 2010-2020 BSI Business Systems Integration AG.
* 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:
* BSI Business Systems Integration AG - initial API and implementation
*/
package org.eclipse.scout.sdk.core.s.project;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.EncodedKeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Base64.Encoder;
import java.util.Collection;
import java.util.regex.Pattern;
import javax.xml.transform.Result;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.eclipse.scout.sdk.core.log.SdkLog;
import org.eclipse.scout.sdk.core.s.environment.IEnvironment;
import org.eclipse.scout.sdk.core.s.environment.IProgress;
import org.eclipse.scout.sdk.core.s.util.maven.IMavenConstants;
import org.eclipse.scout.sdk.core.s.util.maven.MavenBuild;
import org.eclipse.scout.sdk.core.s.util.maven.MavenRunner;
import org.eclipse.scout.sdk.core.util.CoreUtils;
import org.eclipse.scout.sdk.core.util.Ensure;
import org.eclipse.scout.sdk.core.util.JavaTypes;
import org.eclipse.scout.sdk.core.util.Strings;
import org.eclipse.scout.sdk.core.util.Xml;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
/**
* <h3>{@link ScoutProjectNewHelper}</h3>
*
* @since 5.2.0
*/
public final class ScoutProjectNewHelper {
public static final String SCOUT_ARCHETYPES_VERSION = "11.0.0-SNAPSHOT";
public static final String SCOUT_ARCHETYPES_HELLOWORLD_ARTIFACT_ID = "scout-helloworld-app";
public static final String SCOUT_ARCHETYPES_HELLOJS_ARTIFACT_ID = "scout-hellojs-app";
public static final String SCOUT_ARCHETYPES_GROUP_ID = "org.eclipse.scout.archetypes";
public static final Pattern DISPLAY_NAME_PATTERN = Pattern.compile("[^\"/<>=:]+");
public static final Pattern SYMBOLIC_NAME_PATTERN = Pattern.compile("^[a-z][a-z0-9_]{0,32}(?:\\.[a-z][a-z0-9_]{0,32}){0,16}$");
public static final String DEFAULT_JAVA_ENV = "1.8";
private ScoutProjectNewHelper() {
}
public static void createProject(Path workingDir, String groupId, String artifactId, String displayName, IEnvironment env, IProgress progress) throws IOException {
createProject(workingDir, groupId, artifactId, displayName, null, env, progress);
}
public static void createProject(Path workingDir, String groupId, String artifactId, String displayName, String javaVersion, IEnvironment env, IProgress progress) throws IOException {
createProject(workingDir, groupId, artifactId, displayName, javaVersion, null, null, null, env, progress);
}
@SuppressWarnings("squid:S00107")
public static void createProject(Path workingDir, String groupId, String artifactId, String displayName, String javaVersion,
String archetypeGroupId, String archetypeArtifactId, String archetypeVersion, IEnvironment env, IProgress progress) throws IOException {
// validate input
Ensure.notNull(workingDir);
String groupIdMsg = getMavenGroupIdErrorMessage(groupId);
if (groupIdMsg != null) {
throw new IllegalArgumentException(groupIdMsg);
}
String artifactIdMsg = getMavenArtifactIdErrorMessage(artifactId);
if (artifactIdMsg != null) {
throw new IllegalArgumentException(artifactIdMsg);
}
String displayNameMsg = getDisplayNameErrorMessage(displayName);
if (displayNameMsg != null) {
throw new IllegalArgumentException(displayNameMsg);
}
if (Strings.isEmpty(javaVersion)) {
javaVersion = DEFAULT_JAVA_ENV;
}
if (Strings.isBlank(archetypeGroupId) || Strings.isBlank(archetypeArtifactId) || Strings.isBlank(archetypeVersion)) {
// use default
archetypeGroupId = SCOUT_ARCHETYPES_GROUP_ID;
archetypeArtifactId = SCOUT_ARCHETYPES_HELLOJS_ARTIFACT_ID;
archetypeVersion = SCOUT_ARCHETYPES_VERSION;
}
String pck = getPackage(groupId, artifactId);
String artifactName = getArtifactName(artifactId);
// create command
String[] authKeysForWar = generateKeyPairSafe();
String[] authKeysForDev = generateKeyPairSafe();
MavenBuild archetypeBuild = new MavenBuild()
.withWorkingDirectory(workingDir)
.withGoal("archetype:generate")
.withOption(MavenBuild.OPTION_BATCH_MODE)
.withProperty("archetypeGroupId", archetypeGroupId)
.withProperty("archetypeArtifactId", archetypeArtifactId)
.withProperty("archetypeVersion", archetypeVersion)
.withProperty("groupId", groupId)
.withProperty("artifactId", artifactId)
.withProperty("version", "1.0.0-SNAPSHOT")
.withProperty("package", pck)
.withProperty("displayName", displayName)
.withProperty("scoutAuthPublicKey", authKeysForWar[1])
.withProperty("scoutAuthPrivateKey", authKeysForWar[0])
.withProperty("scoutAuthPublicKeyDev", authKeysForDev[1])
.withProperty("scoutAuthPrivateKeyDev", authKeysForDev[0])
.withProperty("javaVersion", javaVersion)
.withProperty("simpleArtifactName", artifactName)
.withProperty("userName", CoreUtils.getUsername());
// execute archetype generation
MavenRunner.execute(archetypeBuild, env, progress);
postProcessRootPom(workingDir.resolve(artifactId));
}
static String getArtifactName(String artifactId) {
int pos = artifactId.lastIndexOf('.');
if (pos < 0 || pos >= artifactId.length() - 1) {
return artifactId;
}
return artifactId.substring(pos + 1);
}
static String getPackage(String groupId, String artifactId) {
if (artifactId.startsWith(groupId)) {
return artifactId;
}
return new StringBuilder(groupId).append(JavaTypes.C_DOT).append(artifactId).toString();
}
static String[] generateKeyPairSafe() {
try {
return generateKeyPair();
}
catch (GeneralSecurityException e) {
SdkLog.warning("Could not generate a new key pair.", e);
String keyPlaceholder = "TODO_use_org.eclipse.scout.rt.platform.security.SecurityUtility.main(String[]))";
return new String[]{keyPlaceholder, keyPlaceholder};
}
}
/**
* Creates a new key pair (private and public key) compatible with the Scout Runtime.<br>
* <b>This method must behave exactly like the one implemented in
* org.eclipse.scout.rt.platform.security.SecurityUtility.generateKeyPair().</b>
*
* @return A {@link String} array of length=2 containing the base64 encoded private key at index zero and the base64
* encoded public key at index 1.
*/
static String[] generateKeyPair() throws NoSuchAlgorithmException, NoSuchProviderException, InvalidAlgorithmParameterException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("EC", "SunEC");
AlgorithmParameterSpec spec = new ECGenParameterSpec("secp256k1");
keyGen.initialize(spec, new SecureRandom());
KeyPair keyPair = keyGen.generateKeyPair();
EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(keyPair.getPublic().getEncoded());
EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(keyPair.getPrivate().getEncoded());
Encoder base64Encoder = Base64.getEncoder();
return new String[]{base64Encoder.encodeToString(pkcs8EncodedKeySpec.getEncoded()) /*private key*/, base64Encoder.encodeToString(x509EncodedKeySpec.getEncoded()) /* public key*/};
}
/**
* Workaround so that only the parent module is referenced in the root (remove non-parent modules)
*/
@SuppressWarnings("findbugs:NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE")
static void postProcessRootPom(Path targetDirectory) throws IOException {
try {
Path pom = targetDirectory.resolve(IMavenConstants.POM);
if (!Files.isReadable(pom) || !Files.isRegularFile(pom)) {
return;
}
Document doc = Xml.get(pom);
Element modules = Xml.firstChildElement(doc.getDocumentElement(), "modules").get();
NodeList childNodes = modules.getChildNodes();
Collection<Node> nodesToRemove = new ArrayList<>();
String targetDirectoryName = targetDirectory.getFileName().toString();
for (int i = 0; i < childNodes.getLength(); i++) {
Node n = childNodes.item(i);
if (n.getNodeType() == Node.TEXT_NODE
|| (n.getNodeType() == Node.ELEMENT_NODE && "module".equals(((Element) n).getTagName()) && !targetDirectoryName.equals(n.getTextContent().trim()))) {
nodesToRemove.add(n);
}
}
for (Node n : nodesToRemove) {
modules.removeChild(n);
}
Ensure.isTrue(modules.getChildNodes().getLength() == 1, "Parent module is missing in root pom.");
//noinspection NestedTryStatement
try (OutputStream out = Files.newOutputStream(pom, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) {
writeDocument(doc, new StreamResult(out));
}
}
catch (TransformerException e) {
throw new IOException(e);
}
}
static void writeDocument(Document document, Result result) throws TransformerException {
Transformer transformer = Xml.createTransformer(false);
transformer.transform(new DOMSource(document), result);
}
public static String getDisplayNameErrorMessage(CharSequence displayNameCandidate) {
if (Strings.isEmpty(displayNameCandidate)) {
return "Display Name is not set.";
}
if (!DISPLAY_NAME_PATTERN.matcher(displayNameCandidate).matches()) {
//noinspection HardcodedFileSeparator
return "The Display Name must not contain these characters: \\\"/<>:=";
}
return null;
}
public static String getMavenArtifactIdErrorMessage(String artifactIdCandidate) {
return getMavenNameErrorMessage(artifactIdCandidate, "Artifact Id");
}
public static String getMavenGroupIdErrorMessage(String groupIdCandidate) {
return getMavenNameErrorMessage(groupIdCandidate, "Group Id");
}
private static String getMavenNameErrorMessage(String nameCandidate, String attributeName) {
if (Strings.isEmpty(nameCandidate)) {
return attributeName + " is not set.";
}
if (!SYMBOLIC_NAME_PATTERN.matcher(nameCandidate).matches()) {
return "The " + attributeName + " value is not valid.";
}
// reserved java keywords
String jkw = getContainingJavaKeyWord(nameCandidate);
if (jkw != null) {
return "The " + attributeName + " must not contain the Java keyword '" + jkw + "'.";
}
return null;
}
private static String getContainingJavaKeyWord(String s) {
for (String keyWord : JavaTypes.getJavaKeyWords()) {
if (s.startsWith(keyWord + JavaTypes.C_DOT) || s.endsWith(JavaTypes.C_DOT + keyWord) || s.contains(JavaTypes.C_DOT + keyWord + JavaTypes.C_DOT)) {
return keyWord;
}
}
return null;
}
}