blob: c47db99eb3e146f0170bcd973c66d2f1b76141b2 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
*
* 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 org.netbeans.modules.gradle.spi.newproject;
import org.netbeans.modules.gradle.NbGradleProjectImpl;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.Writer;
import java.nio.charset.Charset;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import org.gradle.tooling.GradleConnectionException;
import org.gradle.tooling.GradleConnector;
import org.gradle.tooling.ProjectConnection;
import org.netbeans.api.progress.ProgressHandle;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.util.Lookup;
import org.openide.util.NbBundle.Messages;
import static org.netbeans.modules.gradle.spi.newproject.Bundle.*;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.api.project.Project;
import org.netbeans.api.project.ProjectManager;
import org.netbeans.modules.gradle.GradleProjectLoader;
import org.netbeans.modules.gradle.ProjectTrust;
import org.netbeans.modules.gradle.api.GradleProjects;
import org.netbeans.modules.gradle.api.NbGradleProject.Quality;
import org.openide.loaders.DataFolder;
import org.openide.loaders.DataObject;
import org.openide.util.NbBundle;
/**
*
* @author Laszlo Kishalmi
*/
public final class TemplateOperation implements Runnable {
private static final Logger LOG = Logger.getLogger(TemplateOperation.class.getName());
public interface ProjectConfigurator {
void configure(Project project);
}
interface OperationStep {
/**
* Return the message which shall be displayed on the progress handle.
* @return the progress message for this step.
*/
String getMessage();
/**
* Execute the operation step and return the files
* which shall be opened after the whole TemplateOperation process.
*
* @return the files which shall be opened in the IDE or {@code null} for none.
*/
Set<FileObject> execute();
}
final List<OperationStep> steps = new LinkedList<>();
final ProgressHandle handle;
final Set<FileObject> importantFiles = new LinkedHashSet<>();
public TemplateOperation(ProgressHandle handle) {
this.handle = handle;
}
public TemplateOperation() {
this(null);
}
@Override
public void run() {
if (handle != null) {
handle.start(steps.size());
}
try {
int work = 0;
for (OperationStep step : steps) {
if (handle != null) {
handle.progress(step.getMessage(), work++);
}
LOG.log(Level.FINE, "Executing Gradle Project Template Operation {0}", step);
Set<FileObject> filesToOpen = step.execute();
if (filesToOpen != null) {
importantFiles.addAll(filesToOpen);
}
}
} finally {
if (handle != null) {
handle.finish();
}
}
}
public Set<FileObject> getImportantFiles() {
return Collections.unmodifiableSet(importantFiles);
}
@Messages({
"# {0} - Folder name",
"MSG_CREATE_FOLDER=Creating foder: {0}"
})
public void createFolder(File target) {
steps.add(new CreateDirStep(target, MSG_CREATE_FOLDER(target.getName())));
}
@Messages({
"# {0} - Package name",
"MSG_CREATE_PACKAGE=Creating package: {0}"
})
public void createPackage(File base, String pkg) {
String relativePath = pkg.replace('.', '/');
steps.add(new CreateDirStep(new File(base, relativePath),MSG_CREATE_PACKAGE(pkg)));
}
public void addConfigureProject(File projectDir, ProjectConfigurator configurator) {
steps.add(new ConfigureProjectStep(projectDir, configurator));
}
public void addWrapperInit(File target) {
steps.add(new InitGradleWrapper(target));
}
/** * Begin creation of new project using Gradle's
* <a target="_blank" href="https://docs.gradle.org/current/userguide/build_init_plugin.html">gradle init</a>
* functionality. Use the returned {@link InitOperation} object to specify
* additional properties and then call
* {@link InitOperation#add()} to finish the request.
*
*
* @param target the directory to place the project at
* @param type either {@code java-application}, {@code java-library}, etc.
* @return the {@link InitOperation} builder to finish the request
* @since 2.20
*/
public InitOperation createGradleInit(File target, String type) {
return new InitStep(target, type);
}
/** Builder to specify additional parameters for the {@link #createGradleInit(java.io.File, java.lang.String)}
* operation. At the end call {@link #add()} to finish the operation and
* add it to the list of {@link OperationStep}s to perform.
*
* @since 2.20
*/
public abstract class InitOperation {
InitOperation() {
}
/** Add the operation to the list of {@link OperationStep}s to perform.
* @since 2.20
*/
public final void add() {
steps.add((OperationStep) this);
}
/** Specify the type of DSL to use.
* @param dsl either {@code groovy} or {@code kotlin}
* @return this builder to chain the calls.
* @since 2.20
*/
public abstract InitOperation dsl(String dsl);
/** Specify the type of test framework.
* @param testFramework {@code junit-jupiter}, {@code spock}, {@code testng}
* @return this builder to chain the calls.
* @since 2.20
*/
public abstract InitOperation testFramework(String testFramework);
/** Specify base package of the project
* @param pkg base package for the sources
* @return this builder to chain the calls.
* @since 2.20
*/
public abstract InitOperation basePackage(String pkg);
/** Specify project name.
* @param name the (logical) name of the project
* @return this builder to chain the calls.
* @since 2.20
*/
public abstract InitOperation projectName(String name);
}
private final class InitStep extends InitOperation implements OperationStep {
private final File target;
private final String type;
private String dsl;
private String testFramework;
private String basePackage;
private String projectName;
InitStep(File target, String type) {
this.target = target;
this.type = type;
}
@Override
public InitStep dsl(String dsl) {
this.dsl = dsl;
return this;
}
@Override
public InitStep testFramework(String testFramework) {
this.testFramework = testFramework;
return this;
}
@Override
public InitStep basePackage(String pkg) {
this.basePackage = pkg;
return this;
}
@Override
public InitStep projectName(String name) {
this.projectName = name;
return this;
}
@NbBundle.Messages({
"MSG_INIT_GRADLE=Initializing {0} in {1}"
})
@Override
public String getMessage() {
return Bundle.MSG_INIT_GRADLE(type, target);
}
@Override
public Set<FileObject> execute() {
GradleConnector gconn = GradleConnector.newConnector();
target.mkdirs();
try (ProjectConnection pconn = gconn.forProjectDirectory(target).connect()) {
List<String> args = new ArrayList<>();
args.add("init");
// gradle init --type java-application --test-framework junit-jupiter --dsl groovy --package com.example --project-name example
args.add("--type");
args.add(type);
// --test-framework junit-jupiter
if (testFramework != null) {
args.add("--test-framework");
args.add(testFramework);
}
// --dsl groovy
if (dsl != null) {
args.add("--dsl");
args.add(dsl);
}
// --package com.example
if (basePackage != null) {
args.add("--package");
args.add(basePackage);
}
// --project-name example
if (projectName != null) {
args.add("--project-name");
args.add(projectName);
}
pconn.newBuild().withArguments("--offline").forTasks(args.toArray(new String[0])).run(); //NOI18N
} catch (GradleConnectionException | IllegalStateException ex) {
// Well for some reason we were not able to load Gradle.
// Ignoring that for now
}
return Collections.singleton(FileUtil.toFileObject(target));
}
}
public void copyFromFile(String templateName, File target, Map<String, ? extends Object> tokens) {
steps.add(new CopyFromFileTemplate(templateName, target, tokens, false));
}
public void openFromFile(String templateName, File target, Map<String, ? extends Object> tokens) {
steps.add(new CopyFromFileTemplate(templateName, target, tokens, true));
}
public void copyFromTemplate(String template, File target, Map<String, ? extends Object> tokens) {
steps.add(new CopyFromTemplate(template, target, tokens, false));
}
public void openFromTemplate(String template, File target, Map<String, ? extends Object> tokens) {
steps.add(new CopyFromTemplate(template, target, tokens, true));
}
public void addProjectPreload(File projectDir) {
steps.add(new PreloadProject(projectDir));
}
private static abstract class BaseOperationStep implements OperationStep {
@Override
public final String toString() {
return "Step: " + getMessage();
}
}
private static final class CreateDirStep extends BaseOperationStep {
final String message;
final File dir;
public CreateDirStep(File dir, String message) {
this.dir = dir;
this.message = message;
}
@Override
public String getMessage() {
return message;
}
@Override
public Set<FileObject> execute() {
try {
FileUtil.createFolder(dir);
Thread.sleep(200);
} catch (InterruptedException | IOException ex) {
}
return null;
}
}
private static final class ConfigureProjectStep extends BaseOperationStep {
final File dir;
final ProjectConfigurator configurator;
public ConfigureProjectStep(File dir, ProjectConfigurator configurator) {
this.dir = dir;
this.configurator = configurator;
}
@Override
@Messages("MSG_CONFIGURING_PROJECT=Configuring Project...")
public String getMessage() {
return MSG_CONFIGURING_PROJECT();
}
@Override
public final Set<FileObject> execute() {
if (GradleProjects.testForProject(dir)) {
try {
FileObject projectDir = FileUtil.toFileObject(dir);
Project project = ProjectManager.getDefault().findProject(projectDir);
ProjectTrust.getDefault().trustProject(project);
NbGradleProjectImpl impl = project != null ? project.getLookup().lookup(NbGradleProjectImpl.class): null;
if (impl != null) {
impl.projectWithQuality(null, Quality.FULL, false, false);
configurator.configure(project);
}
} catch (IOException ex) {
}
}
return Collections.<FileObject>emptySet();
}
}
private static final class PreloadProject extends BaseOperationStep {
final File dir;
public PreloadProject(File dir) {
this.dir = dir;
}
@Override
@Messages({
"# {0} - Folder Name",
"MSM_CHECKING_FOLDER=Checking folder: {0}",
"# {0} - Project Name",
"MSG_PRELOAD_PROJECT=Load: {0}"
})
public String getMessage() {
return GradleProjects.testForProject(dir) ? MSG_PRELOAD_PROJECT(dir.getName()) : MSM_CHECKING_FOLDER(dir.getName());
}
@Override
public Set<FileObject> execute() {
if (GradleProjects.testForProject(dir)) {
try {
FileObject projectDir = FileUtil.toFileObject(dir);
Project project = ProjectManager.getDefault().findProject(projectDir);
if (project == null) {
ProjectManager.getDefault().clearNonProjectCache();
}
project = ProjectManager.getDefault().findProject(projectDir);
if (project != null) {
//Let's trust the generated project
ProjectTrust.getDefault().trustProject(project);
NbGradleProjectImpl nbProject = project.getLookup().lookup(NbGradleProjectImpl.class);
if (nbProject != null) {
//Just load the project into the cache.
GradleProjectLoader loader = nbProject.getLookup().lookup(GradleProjectLoader.class);
if (loader != null) {
loader.loadProject(Quality.FULL_ONLINE, null, true, false);
}
}
return Collections.singleton(projectDir);
}
} catch (IOException | IllegalArgumentException ex) {
}
}
return null;
}
}
private static final class InitGradleWrapper extends BaseOperationStep {
final File projectDir;
public InitGradleWrapper(File projectDir) {
this.projectDir = projectDir;
}
@Override
@Messages("MSG_INIT_WRAPPER=Initializing Gradle Wrapper")
public String getMessage() {
return MSG_INIT_WRAPPER();
}
@Override
public Set<FileObject> execute() {
GradleConnector gconn = GradleConnector.newConnector();
try (ProjectConnection pconn = gconn.forProjectDirectory(projectDir).connect()) {
pconn.newBuild().withArguments("--offline").forTasks("wrapper").run(); //NOI18N
} catch (GradleConnectionException | IllegalStateException ex) {
// Well for some reason we were not able to load Gradle.
// Ignoring that for now
}
return null;
}
}
private static final class CopyFromFileTemplate extends BaseOperationStep {
final File target;
final Map<String, ? extends Object> tokens;
final boolean important;
final String templateName;
public CopyFromFileTemplate(String templateName, File target, Map<String, ? extends Object> tokens, boolean important) {
this.templateName = templateName;
this.target = target;
this.tokens = tokens;
this.important = important;
}
@Override
@Messages({
"# {0} - File name",
"MSG_COPY_TEMPLATE=Generating {0}..."
})
public String getMessage() {
return MSG_COPY_TEMPLATE(target.getName());
}
@Override
public Set<FileObject> execute() {
ClassLoader l = Lookup.getDefault().lookup(ClassLoader.class);
if (l == null) {
l = Thread.currentThread().getContextClassLoader();
}
ScriptEngineManager scriptEngineManager = new ScriptEngineManager(l);
ScriptEngine engine = scriptEngineManager.getEngineByName("freemarker"); //NOI18N
assert engine != null;
Map<String, Object> bindings = engine.getContext().getBindings(ScriptContext.ENGINE_SCOPE);
String basename = target.getName();
bindings.put("name", basename.replaceFirst("\\.[^./]+$", "")); // NOI18N
bindings.put("user", System.getProperty("user.name")); // NOI18N
Date d = new Date();
bindings.put("date", DateFormat.getDateInstance().format(d)); // NOI18N
bindings.put("time", DateFormat.getTimeInstance().format(d)); // NOI18N
bindings.put("nameAndExt", target.getName()); // NOI18N
bindings.putAll(tokens);
try {
FileObject fo = FileUtil.createData(target);
// Requesting Charset information at this stahe would try to
// load the project from the dir.
Charset targetEnc = Charset.defaultCharset();
bindings.put("encoding", targetEnc.name()); //NOI18N
try (Writer w = new OutputStreamWriter(fo.getOutputStream(), targetEnc)) {
engine.getContext().setWriter(w);
//engine.getContext().setAttribute(ScriptEngine.FILENAME, "/" + templateName, ScriptContext.ENGINE_SCOPE);
try (Reader is = new InputStreamReader(TemplateOperation.class.getResourceAsStream("/" + templateName))) {
engine.eval(is);
} catch (IOException | ScriptException ex) {
throw new IOException(ex.getMessage(), ex);
}
return important ? Collections.singleton(fo) : null;
} catch (IOException ex) {}
} catch (IOException ex) {}
return null;
}
}
private static final class CopyFromTemplate extends BaseOperationStep {
final File target;
final Map<String, ? extends Object> tokens;
final boolean important;
final String templateName;
public CopyFromTemplate(String templateName, File target, Map<String, ? extends Object> tokens, boolean important) {
this.templateName = templateName;
this.target = target;
this.tokens = tokens;
this.important = important;
}
@Override
public String getMessage() {
return MSG_COPY_TEMPLATE(target.getName());
}
@Override
public Set<FileObject> execute() {
FileObject template = FileUtil.getConfigFile(templateName);
if (template != null) {
String targetName = target.getName();
try {
FileObject targetParent = FileUtil.createFolder(target.getParentFile());
DataFolder targetFolder = DataFolder.findFolder(targetParent);
DataObject o = DataObject.find(template);
DataObject newData = o.createFromTemplate(targetFolder, targetName, tokens);
return important ? Collections.singleton(newData.getPrimaryFile()) : null;
} catch (IOException ex) {
}
}
return null;
}
}
}