diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index b0555da6c7..82ed0c0038 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -56,6 +56,10 @@ gradlePlugin { id = "org.springframework.github.changelog" implementationClass = "org.springframework.gradle.github.changelog.GitHubChangelogPlugin" } + s101 { + id = "s101" + implementationClass = "s101.S101Plugin" + } } } @@ -76,9 +80,11 @@ dependencies { implementation 'gradle.plugin.org.gretty:gretty:3.0.1' implementation 'com.apollographql.apollo:apollo-runtime:2.4.5' implementation 'com.github.ben-manes:gradle-versions-plugin:0.38.0' + implementation 'com.github.spullara.mustache.java:compiler:0.9.4' implementation 'io.spring.gradle:propdeps-plugin:0.0.10.RELEASE' implementation 'io.spring.javaformat:spring-javaformat-gradle-plugin:0.0.15' implementation 'io.spring.nohttp:nohttp-gradle:0.0.10' + implementation 'net.sourceforge.htmlunit:htmlunit:2.37.0' implementation 'org.hidetake:gradle-ssh-plugin:2.10.1' implementation 'org.jfrog.buildinfo:build-info-extractor-gradle:4.9.10' implementation 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.7.1' diff --git a/buildSrc/src/main/java/s101/S101Configure.java b/buildSrc/src/main/java/s101/S101Configure.java new file mode 100644 index 0000000000..a8e2154dfd --- /dev/null +++ b/buildSrc/src/main/java/s101/S101Configure.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed 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 + * + * https://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 s101; + +import java.io.File; + +import org.gradle.api.DefaultTask; +import org.gradle.api.tasks.TaskAction; + +public class S101Configure extends DefaultTask { + @TaskAction + public void configure() throws Exception { + S101PluginExtension extension = getProject().getExtensions().getByType(S101PluginExtension.class); + File buildDirectory = extension.getInstallationDirectory().get(); + File projectDirectory = extension.getConfigurationDirectory().get(); + S101Configurer configurer = new S101Configurer(getProject()); + configurer.configure(buildDirectory, projectDirectory); + } + + +} diff --git a/buildSrc/src/main/java/s101/S101Configurer.java b/buildSrc/src/main/java/s101/S101Configurer.java new file mode 100644 index 0000000000..c508c6153b --- /dev/null +++ b/buildSrc/src/main/java/s101/S101Configurer.java @@ -0,0 +1,271 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed 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 + * + * https://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 s101; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.UncheckedIOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.html.HtmlAnchor; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import com.github.mustachejava.DefaultMustacheFactory; +import com.github.mustachejava.Mustache; +import com.github.mustachejava.MustacheFactory; +import org.apache.commons.io.IOUtils; +import org.gradle.api.Project; +import org.gradle.api.logging.Logger; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; + +public class S101Configurer { + private static final Pattern VERSION = Pattern.compile(" TOOMANY) { + throw new IllegalArgumentException("Zip file has more entries than expected"); + } + if (size + BUFFER > TOOBIG) { + throw new IllegalArgumentException("Zip file is larger than expected"); + } + String filename = entry.getName().replace(name, build); + if (filename.contains("maven")) { + continue; + } + if (filename.contains("jxbrowser")) { + continue; + } + if (filename.contains("jetty")) { + continue; + } + if (filename.contains("jfreechart")) { + continue; + } + if (filename.contains("piccolo2d")) { + continue; + } + if (filename.contains("plexus")) { + continue; + } + if (filename.contains("websocket")) { + continue; + } + validateFilename(filename, build); + this.logger.info("Downloading " + filename); + try (OutputStream out = new FileOutputStream(new File(destination.getParentFile(), filename))) { + byte[] data = new byte[BUFFER]; + int read; + while ((read = in.read(data, 0, BUFFER)) != -1 && TOOBIG - size >= read) { + out.write(data, 0, read); + size += read; + } + } + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private String validateFilename(String filename, String intendedDir) + throws java.io.IOException { + File f = new File(filename); + String canonicalPath = f.getCanonicalPath(); + + File iD = new File(intendedDir); + String canonicalID = iD.getCanonicalPath(); + + if (canonicalPath.startsWith(canonicalID)) { + return canonicalPath; + } else { + throw new IllegalArgumentException("File is outside extraction target directory."); + } + } + + private void configureProject(String version, File configurationDirectory) { + configurationDirectory.mkdirs(); + Map model = hspTemplateValues(version, configurationDirectory); + copyToProject(this.hspTemplate, model, new File(configurationDirectory, "project.java.hsp")); + copyToProject("s101/config.xml", new File(configurationDirectory, "config.xml")); + File repository = new File(configurationDirectory, "repository"); + File snapshots = new File(repository, "snapshots"); + if (!snapshots.exists() && !snapshots.mkdirs()) { + throw new IllegalStateException("Unable to create snapshots directory"); + } + copyToProject(this.repositoryTemplate, model, new File(repository, "repository.xml")); + } + + private void copyToProject(String location, File destination) { + Resource resource = new ClassPathResource(location); + try (InputStream is = resource.getInputStream(); + OutputStream os = new FileOutputStream(destination)) { + IOUtils.copy(is, os); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private void copyToProject(Mustache view, Map model, File destination) { + try (OutputStream os = new FileOutputStream(destination)) { + view.execute(new OutputStreamWriter(os), model).flush(); + } catch (IOException ex) { + throw new UncheckedIOException(ex); + } + } + + private Map hspTemplateValues(String version, File configurationDirectory) { + Map values = new LinkedHashMap<>(); + values.put("version", version); + values.put("patchVersion", version.split("\\.")[2]); + values.put("relativeTo", "const(THIS_FILE)/" + configurationDirectory.toPath().relativize(this.project.getProjectDir().toPath())); + + List> entries = new ArrayList<>(); + Set projects = this.project.getAllprojects(); + for (Project p : projects) { + SourceSetContainer sourceSets = (SourceSetContainer) p.getExtensions().findByName("sourceSets"); + if (sourceSets == null) { + continue; + } + for (SourceSet source : sourceSets) { + Set classDirs = source.getOutput().getClassesDirs().getFiles(); + for (File directory : classDirs) { + Map entry = new HashMap<>(); + entry.put("path", this.project.getProjectDir().toPath().relativize(directory.toPath())); + entry.put("module", p.getName()); + entries.add(entry); + } + } + } + values.put("entries", entries); + return values; + } +} diff --git a/buildSrc/src/main/java/s101/S101Install.java b/buildSrc/src/main/java/s101/S101Install.java new file mode 100644 index 0000000000..8d95db0d1f --- /dev/null +++ b/buildSrc/src/main/java/s101/S101Install.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed 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 + * + * https://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 s101; + +import java.io.File; + +import org.gradle.api.DefaultTask; +import org.gradle.api.tasks.TaskAction; + +public class S101Install extends DefaultTask { + @TaskAction + public void install() throws Exception { + S101PluginExtension extension = getProject().getExtensions().getByType(S101PluginExtension.class); + File installationDirectory = extension.getInstallationDirectory().get(); + File configurationDirectory = extension.getConfigurationDirectory().get(); + S101Configurer configurer = new S101Configurer(getProject()); + configurer.install(installationDirectory, configurationDirectory); + } + + +} diff --git a/buildSrc/src/main/java/s101/S101Plugin.java b/buildSrc/src/main/java/s101/S101Plugin.java new file mode 100644 index 0000000000..3a3f493ec0 --- /dev/null +++ b/buildSrc/src/main/java/s101/S101Plugin.java @@ -0,0 +1,158 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed 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 + * + * https://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 s101; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.JavaExec; + +public class S101Plugin implements Plugin { + @Override + public void apply(Project project) { + project.getExtensions().add("s101", new S101PluginExtension(project)); + project.getTasks().register("s101Install", S101Install.class, this::configure); + project.getTasks().register("s101Configure", S101Configure.class, this::configure); + project.getTasks().register("s101", JavaExec.class, this::configure); + } + + private void configure(S101Install install) { + install.setDescription("Installs Structure101 to your filesystem"); + } + + private void configure(S101Configure configure) { + configure.setDescription("Applies a default Structure101 configuration to the project"); + } + + private void configure(JavaExec exec) { + exec.setDescription("Runs Structure101 headless analysis, installing and configuring if necessary"); + exec.dependsOn("check"); + Project project = exec.getProject(); + S101PluginExtension extension = project.getExtensions().getByType(S101PluginExtension.class); + exec + .workingDir(extension.getInstallationDirectory()) + .classpath(new File(extension.getInstallationDirectory().get(), "structure101-java-build.jar")) + .args(new File(new File(project.getBuildDir(), "s101"), "config.xml")) + .args("-licensedirectory=" + extension.getLicenseDirectory().get()) + .systemProperty("s101.label", computeLabel(extension).get()) + .doFirst((task) -> { + installAndConfigureIfNeeded(project); + copyConfigurationToBuildDirectory(extension, project); + }) + .doLast((task) -> { + copyResultsBackToConfigurationDirectory(extension, project); + }); + } + + private Property computeLabel(S101PluginExtension extension) { + boolean hasBaseline = extension.getConfigurationDirectory().get().toPath() + .resolve("repository").resolve("snapshots").resolve("baseline").toFile().exists(); + if (!hasBaseline) { + return extension.getLabel().convention("baseline"); + } + return extension.getLabel().convention("recent"); + } + + private void installAndConfigureIfNeeded(Project project) { + S101Configurer configurer = new S101Configurer(project); + S101PluginExtension extension = project.getExtensions().getByType(S101PluginExtension.class); + File installationDirectory = extension.getInstallationDirectory().get(); + File configurationDirectory = extension.getConfigurationDirectory().get(); + if (!installationDirectory.exists()) { + configurer.install(installationDirectory, configurationDirectory); + } + if (!configurationDirectory.exists()) { + configurer.configure(installationDirectory, configurationDirectory); + } + } + + private void copyConfigurationToBuildDirectory(S101PluginExtension extension, Project project) { + Path configurationDirectory = extension.getConfigurationDirectory().get().toPath(); + Path buildDirectory = project.getBuildDir().toPath(); + copyDirectory(project, configurationDirectory, buildDirectory); + } + + private void copyResultsBackToConfigurationDirectory(S101PluginExtension extension, Project project) { + Path buildConfigurationDirectory = project.getBuildDir().toPath().resolve("s101"); + String label = extension.getLabel().get(); + if ("baseline".equals(label)) { // a new baseline was created + copyDirectory(project, buildConfigurationDirectory.resolve("repository").resolve("snapshots"), + extension.getConfigurationDirectory().get().toPath().resolve("repository")); + copyDirectory(project, buildConfigurationDirectory.resolve("repository"), + extension.getConfigurationDirectory().get().toPath()); + } + } + + private void copyDirectory(Project project, Path source, Path destination) { + try { + Files.walk(source) + .forEach(each -> { + Path relativeToSource = source.getParent().relativize(each); + Path resolvedDestination = destination.resolve(relativeToSource); + if (each.toFile().isDirectory()) { + resolvedDestination.toFile().mkdirs(); + return; + } + InputStream input; + if ("project.java.hsp".equals(each.toFile().getName())) { + Path relativeTo = project.getBuildDir().toPath().resolve("s101").relativize(project.getProjectDir().toPath()); + String value = "const(THIS_FILE)/" + relativeTo; + input = replace(each, "", ""); + } else if (each.toFile().toString().endsWith(".xml")) { + input = replace(each, "\\r\\n", "\n"); + } else { + input = input(each); + } + try { + Files.copy(input, resolvedDestination, StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private InputStream replace(Path file, String search, String replace) { + try { + byte[] b = Files.readAllBytes(file); + String contents = new String(b).replaceAll(search, replace); + return new ByteArrayInputStream(contents.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private InputStream input(Path file) { + try { + return new FileInputStream(file.toFile()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/buildSrc/src/main/java/s101/S101PluginExtension.java b/buildSrc/src/main/java/s101/S101PluginExtension.java new file mode 100644 index 0000000000..e5e548ee35 --- /dev/null +++ b/buildSrc/src/main/java/s101/S101PluginExtension.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2021 the original author or authors. + * + * Licensed 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 + * + * https://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 s101; + +import java.io.File; + +import org.gradle.api.Project; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputDirectory; + +public class S101PluginExtension { + private final Property licenseDirectory; + private final Property installationDirectory; + private final Property configurationDirectory; + private final Property label; + + @InputDirectory + public Property getLicenseDirectory() { + return this.licenseDirectory; + } + + public void setLicenseDirectory(String licenseDirectory) { + this.licenseDirectory.set(new File(licenseDirectory)); + } + + @InputDirectory + public Property getInstallationDirectory() { + return this.installationDirectory; + } + + public void setInstallationDirectory(String installationDirectory) { + this.installationDirectory.set(new File(installationDirectory)); + } + + @InputDirectory + public Property getConfigurationDirectory() { + return this.configurationDirectory; + } + + public void setConfigurationDirectory(String configurationDirectory) { + this.configurationDirectory.set(new File(configurationDirectory)); + } + + @Input + public Property getLabel() { + return this.label; + } + + public void setLabel(String label) { + this.label.set(label); + } + + public S101PluginExtension(Project project) { + this.licenseDirectory = project.getObjects().property(File.class) + .convention(new File(System.getProperty("user.home") + "/.Structure101/java")); + this.installationDirectory = project.getObjects().property(File.class) + .convention(new File(project.getBuildDir(), "s101")); + this.configurationDirectory = project.getObjects().property(File.class) + .convention(new File(project.getProjectDir(), "s101")); + this.label = project.getObjects().property(String.class); + if (project.hasProperty("s101.label")) { + setLabel((String) project.findProperty("s101.label")); + } + } +} diff --git a/buildSrc/src/main/resources/s101/config.xml b/buildSrc/src/main/resources/s101/config.xml new file mode 100644 index 0000000000..113236c456 --- /dev/null +++ b/buildSrc/src/main/resources/s101/config.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/buildSrc/src/main/resources/s101/project.java.hsp b/buildSrc/src/main/resources/s101/project.java.hsp new file mode 100644 index 0000000000..be20cf0e57 --- /dev/null +++ b/buildSrc/src/main/resources/s101/project.java.hsp @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + {{#entries}} + + {{/entries}} + + + + + + + + diff --git a/buildSrc/src/main/resources/s101/repository.xml b/buildSrc/src/main/resources/s101/repository.xml new file mode 100644 index 0000000000..466bdd2bb7 --- /dev/null +++ b/buildSrc/src/main/resources/s101/repository.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + +