Przeglądaj źródła

Structure101 Build Plugin

Issue gh-6236
Josh Cummings 4 lat temu
rodzic
commit
6978f51f19

+ 6 - 0
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'

+ 35 - 0
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);
+	}
+
+
+}

+ 271 - 0
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("<local-project .* version=\"(.*?)\"");
+
+	private static final int BUFFER = 1024;
+	private static final long TOOBIG = 0x10000000; // ~268M
+	private static final int TOOMANY = 200;
+
+	private final MustacheFactory mustache = new DefaultMustacheFactory();
+	private final Mustache hspTemplate;
+	private final Mustache repositoryTemplate;
+
+	private final Project project;
+	private final Logger logger;
+
+	public S101Configurer(Project project) {
+		this.project = project;
+		this.logger = project.getLogger();
+		Resource template = new ClassPathResource("s101/project.java.hsp");
+		try (InputStream is = template.getInputStream()) {
+			this.hspTemplate = this.mustache.compile(new InputStreamReader(is), "project");
+		} catch (IOException ex) {
+			throw new UncheckedIOException(ex);
+		}
+		template = new ClassPathResource("s101/repository.xml");
+		try (InputStream is = template.getInputStream()) {
+			this.repositoryTemplate = this.mustache.compile(new InputStreamReader(is), "repository");
+		} catch (IOException ex) {
+			throw new UncheckedIOException(ex);
+		}
+	}
+
+	public void install(File installationDirectory, File configurationDirectory) {
+		deleteDirectory(installationDirectory);
+		installBuildTool(installationDirectory, configurationDirectory);
+	}
+
+	public void configure(File installationDirectory, File configurationDirectory) {
+		deleteDirectory(configurationDirectory);
+		String version = computeVersionFromInstallation(installationDirectory);
+		configureProject(version, configurationDirectory);
+	}
+
+	private String computeVersionFromInstallation(File installationDirectory) {
+		File buildJar = new File(installationDirectory, "structure101-java-build.jar");
+		try (JarInputStream input = new JarInputStream(new FileInputStream(buildJar))) {
+			JarEntry entry;
+			while ((entry = input.getNextJarEntry()) != null) {
+				if (entry.getName().contains("structure101-build.properties")) {
+					Properties properties = new Properties();
+					properties.load(input);
+					return properties.getProperty("s101-build");
+				}
+			}
+		} catch (Exception ex) {
+			throw new RuntimeException(ex);
+		}
+		throw new IllegalStateException("Unable to determine Structure101 version");
+	}
+
+	private boolean deleteDirectory(File directoryToBeDeleted) {
+		File[] allContents = directoryToBeDeleted.listFiles();
+		if (allContents != null) {
+			for (File file : allContents) {
+				deleteDirectory(file);
+			}
+		}
+		return directoryToBeDeleted.delete();
+	}
+
+	private String installBuildTool(File installationDirectory, File configurationDirectory) {
+		String source = "https://structure101.com/binaries/v6";
+		try (final WebClient webClient = new WebClient()) {
+			HtmlPage page = webClient.getPage(source);
+			for (HtmlAnchor anchor : page.getAnchors()) {
+				Matcher matcher = Pattern.compile("(structure101-build-java-all-)(.*).zip").matcher(anchor.getHrefAttribute());
+				if (matcher.find()) {
+					copyZipToFilesystem(source, installationDirectory, matcher.group(1) + matcher.group(2));
+					return matcher.group(2);
+				}
+			}
+			return null;
+		} catch (Exception ex) {
+			throw new RuntimeException(ex);
+		}
+	}
+
+	private void copyZipToFilesystem(String source, File destination, String name) {
+		try (ZipInputStream in = new ZipInputStream(new URL(source + "/" + name + ".zip").openStream())) {
+			ZipEntry entry;
+			String build = destination.getName();
+			int entries = 0;
+			long size = 0;
+			while ((entry = in.getNextEntry()) != null) {
+				if (entry.getName().equals(name + "/")) {
+					destination.mkdirs();
+				} else if (entry.getName().startsWith(name)) {
+					if (entries++ > 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<String, Object> 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<String, Object> 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<String, Object> hspTemplateValues(String version, File configurationDirectory) {
+		Map<String, Object> 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<Map<String, Object>> entries = new ArrayList<>();
+		Set<Project> projects = this.project.getAllprojects();
+		for (Project p : projects) {
+			SourceSetContainer sourceSets = (SourceSetContainer) p.getExtensions().findByName("sourceSets");
+			if (sourceSets == null) {
+				continue;
+			}
+			for (SourceSet source : sourceSets) {
+				Set<File> classDirs = source.getOutput().getClassesDirs().getFiles();
+				for (File directory : classDirs) {
+					Map<String, Object> 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;
+	}
+}

+ 35 - 0
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);
+	}
+
+
+}

+ 158 - 0
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<Project> {
+	@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<String> 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, "<property name=\"relative-to\" value=\"(.*)\" />", "<property name=\"relative-to\" value=\"" + value + "\" />");
+						} 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);
+		}
+	}
+}

+ 80 - 0
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<File> licenseDirectory;
+	private final Property<File> installationDirectory;
+	private final Property<File> configurationDirectory;
+	private final Property<String> label;
+
+	@InputDirectory
+	public Property<File> getLicenseDirectory() {
+		return this.licenseDirectory;
+	}
+
+	public void setLicenseDirectory(String licenseDirectory) {
+		this.licenseDirectory.set(new File(licenseDirectory));
+	}
+
+	@InputDirectory
+	public Property<File> getInstallationDirectory() {
+		return this.installationDirectory;
+	}
+
+	public void setInstallationDirectory(String installationDirectory) {
+		this.installationDirectory.set(new File(installationDirectory));
+	}
+
+	@InputDirectory
+	public Property<File> getConfigurationDirectory() {
+		return this.configurationDirectory;
+	}
+
+	public void setConfigurationDirectory(String configurationDirectory) {
+		this.configurationDirectory.set(new File(configurationDirectory));
+	}
+
+	@Input
+	public Property<String> 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"));
+		}
+	}
+}

+ 32 - 0
buildSrc/src/main/resources/s101/config.xml

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<headless version="1.0">
+	<operations>
+		<operation type="publish">
+			<argument name="overwrite" value="true"/>
+			<argument name="diagrams" value="true"/>
+		</operation>
+		<operation type="check-key-measures">
+			<argument name="baseline" value="baseline"/>
+			<argument name="useProjectFileSpec" value="true"/>
+			<argument name="useProjectFileDiagrams" value="true"/>
+			<argument name="fail-on-architecture-violations" value="false"/>
+			<argument name="fail-on-fat-package" value="false"/>
+			<argument name="fail-on-fat-class" value="false"/>
+			<argument name="fail-on-fat-method" value="false"/>
+			<argument name="fail-on-feedback-dependencies" value="true"/>
+			<argument name="fail-on-spec-violation-dependencies" value="false"/>
+			<argument name="fail-on-total-problem-dependencies" value="false"/>
+			<argument name="fail-on-spec-item-violations" value="false"/>
+			<argument name="fail-on-biggest-class-tangle" value="true"/>
+			<argument name="fail-on-tangled-package" value="true"/>
+			<argument name="fail-on-architecture-violations" value="false"/>
+			<argument name="fail-on-total-problem-dependencies" value="true"/>
+			<argument name="identifier-on-violation" value="S101 key measure violation"/>
+		</operation>
+	</operations>
+	<arguments>
+		<argument name="local-project" value="const(THIS_FILE)/project.java.hsp"/>
+		<argument name="repository" value="const(THIS_FILE)/repository"/>
+		<argument name="project" value="snapshots"/>
+	</arguments>
+</headless>

+ 28 - 0
buildSrc/src/main/resources/s101/project.java.hsp

@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<local-project language="java" version="{{version}}" xml-version="3" flavor="j2se">
+  <property name="show-as-module" value="false" />
+  <property name="publish-architecture-artifacts" value="true" />
+  <property name="force-classpath" value="false" />
+  <property name="project-type" value="classpath" />
+  <property name="hide-externals" value="true" />
+  <property name="parse-archive-in-archive" value="false" />
+  <property name="include-injected-dependency" value="false" />
+  <property name="relative-to" value="{{relativeTo}}" />
+  <property name="action-set-mod" value="1" />
+  <property name="detail-mode" value="true" />
+  <property name="hide-deprecated" value="false" />
+  <property name="resolve-name-clashes" value="true" />
+  <property name="project-excluded" />
+  <property name="show-needs-to-compile" value="false" />
+  <classpath>
+    {{#entries}}
+    <classpathentry kind="lib" path="{{path}}" module="{{module}}" />
+    {{/entries}}
+  </classpath>
+  <pom-root-files />
+  <modules-in-scope />
+  <restructuring>
+    <set version="3" name="Action list 1" hiview="Codemap" active="true" todo="false" list="0" />
+  </restructuring>
+  <grid-set sep="." version="{{version}}" />
+</local-project>

+ 14 - 0
buildSrc/src/main/resources/s101/repository.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<structure101-repository language="java" version="{{patchVersion}}">
+  <xs-configuration>
+    <entry metric="Tangled" scope="design" threshold="0" color="153,53,0" />
+    <entry metric="Fat" scope="design" threshold="120" color="255,153,0" />
+    <entry metric="Fat" scope="leaf package" threshold="120" color="0,153,153" />
+    <entry metric="Fat" scope="class" threshold="120" color="255,153,153" />
+    <entry metric="Fat" scope="method" threshold="15" color="51,255,51" />
+  </xs-configuration>
+  <!--Note: All date strings are stored in short US format e.g. 2/1/06 for 1st Feb 2006-->
+  <project name="snapshots" dir="snapshots" baselineSnapshot="default" version="{{patchVersion}}">
+    <snapshot label="baseline" location="baseline" timestamp="3/16/21, 4:42 PM" version="{{patchVersion}}" detail="true" good="true" size="20" />
+  </project>
+</structure101-repository>