Skip to content

Commit e3b3d3e

Browse files
authored
Merge pull request #279 from diffplug/eclipse_classloader_cache_extension
SpotlessCache extension for Eclipse formatters
2 parents 96e74ec + e6b32cc commit e3b3d3e

File tree

7 files changed

+295
-6
lines changed

7 files changed

+295
-6
lines changed

lib-extra/src/main/java/com/diffplug/spotless/extra/EclipseBasedStepBuilder.java

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
*/
4242
public class EclipseBasedStepBuilder {
4343
private final String formatterName;
44+
private final String formatterStepExt;
4445
private final ThrowingEx.Function<State, FormatterFunc> stateToFormatter;
4546
private final Provisioner jarProvisioner;
4647

@@ -63,14 +64,20 @@ public class EclipseBasedStepBuilder {
6364

6465
/** Initialize valid default configuration, taking latest version */
6566
public EclipseBasedStepBuilder(String formatterName, Provisioner jarProvisioner, ThrowingEx.Function<State, FormatterFunc> stateToFormatter) {
67+
this(formatterName, "", jarProvisioner, stateToFormatter);
68+
}
69+
70+
/** Initialize valid default configuration, taking latest version */
71+
public EclipseBasedStepBuilder(String formatterName, String formatterStepExt, Provisioner jarProvisioner, ThrowingEx.Function<State, FormatterFunc> stateToFormatter) {
6672
this.formatterName = Objects.requireNonNull(formatterName, "formatterName");
73+
this.formatterStepExt = Objects.requireNonNull(formatterStepExt, "formatterStepExt");
6774
this.jarProvisioner = Objects.requireNonNull(jarProvisioner, "jarProvisioner");
6875
this.stateToFormatter = Objects.requireNonNull(stateToFormatter, "stateToFormatter");
6976
}
7077

7178
/** Returns the FormatterStep (whose state will be calculated lazily). */
7279
public FormatterStep build() {
73-
return FormatterStep.createLazy(formatterName, this::get, stateToFormatter);
80+
return FormatterStep.createLazy(formatterName + formatterStepExt, this::get, stateToFormatter);
7481
}
7582

7683
/** Set dependencies for the corresponding Eclipse version */
@@ -123,6 +130,7 @@ EclipseBasedStepBuilder.State get() throws IOException {
123130
* Hence a lazy construction is not required.
124131
*/
125132
return new State(
133+
formatterStepExt,
126134
jarProvisioner,
127135
dependencies,
128136
settingsFiles);
@@ -137,12 +145,16 @@ public static class State implements Serializable {
137145
private static final long serialVersionUID = 1L;
138146

139147
private final JarState jarState;
148+
//The formatterStepExt assures that different class loaders are used for different step types
149+
@SuppressWarnings("unused")
150+
private final String formatterStepExt;
140151
private final FileSignature settingsFiles;
141152

142153
/** State constructor expects that all passed items are not modified afterwards */
143-
protected State(Provisioner jarProvisioner, List<String> dependencies, Iterable<File> settingsFiles) throws IOException {
154+
protected State(String formatterStepExt, Provisioner jarProvisioner, List<String> dependencies, Iterable<File> settingsFiles) throws IOException {
144155
this.jarState = JarState.from(dependencies, jarProvisioner);
145156
this.settingsFiles = FileSignature.signAsList(settingsFiles);
157+
this.formatterStepExt = formatterStepExt;
146158
}
147159

148160
/** Get formatter preferences */
@@ -158,10 +170,18 @@ public Optional<String> getMavenCoordinate(String prefix) {
158170
.filter(coordinate -> coordinate.startsWith(prefix)).findFirst();
159171
}
160172

161-
/** Load class based on the given configuration of JAR provider and Maven coordinates. */
173+
/**
174+
* Load class based on the given configuration of JAR provider and Maven coordinates.
175+
* Different class loader instances are provided in the following scenarios:
176+
* <ol>
177+
* <li>The JARs ({@link #jarState}) have changes (this should only occur during development)</li>
178+
* <li>Different configurations ({@link #settingsFiles}) are used for different sub-projects</li>
179+
* <li>The same Eclipse step implementation provides different formatter types ({@link #formatterStepExt})</li>
180+
* </ol>
181+
*/
162182
public Class<?> loadClass(String name) {
163183
try {
164-
return jarState.getClassLoader().loadClass(name);
184+
return jarState.getClassLoader(this).loadClass(name);
165185
} catch (ClassNotFoundException e) {
166186
throw Errors.asRuntime(e);
167187
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright 2016 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.spotless.extra.wtp;
17+
18+
import java.lang.reflect.InvocationTargetException;
19+
import java.lang.reflect.Method;
20+
import java.util.Properties;
21+
22+
import com.diffplug.spotless.FormatterFunc;
23+
import com.diffplug.spotless.Provisioner;
24+
import com.diffplug.spotless.extra.EclipseBasedStepBuilder;
25+
26+
/** Formatter step which calls out to the Groovy-Eclipse formatter. */
27+
public final class WtpEclipseFormatterStep {
28+
// prevent direct instantiation
29+
private WtpEclipseFormatterStep() {}
30+
31+
private static final String NAME = "eclipse wtp formatters";
32+
private static final String FORMATTER_PACKAGE = "com.diffplug.spotless.extra.eclipse.wtp.";
33+
private static final String DEFAULT_VERSION = "4.7.3a";
34+
private static final String FORMATTER_METHOD = "format";
35+
36+
public static String defaultVersion() {
37+
return DEFAULT_VERSION;
38+
}
39+
40+
/** Provides default configuration for CSSformatter */
41+
public static EclipseBasedStepBuilder createCssBuilder(Provisioner provisioner) {
42+
return new EclipseBasedStepBuilder(NAME, " - css", provisioner, state -> apply("EclipseCssFormatterStepImpl", state));
43+
}
44+
45+
/** Provides default configuration for HTML formatter */
46+
public static EclipseBasedStepBuilder createHtmlBuilder(Provisioner provisioner) {
47+
return new EclipseBasedStepBuilder(NAME, " - html", provisioner, state -> apply("EclipseHtmlFormatterStepImpl", state));
48+
}
49+
50+
/** Provides default configuration for Java Script formatter */
51+
public static EclipseBasedStepBuilder createJsBuilder(Provisioner provisioner) {
52+
return new EclipseBasedStepBuilder(NAME, " - js", provisioner, state -> apply("EclipseJsFormatterStepImpl", state));
53+
}
54+
55+
/** Provides default configuration for JSON formatter */
56+
public static EclipseBasedStepBuilder createJsonBuilder(Provisioner provisioner) {
57+
return new EclipseBasedStepBuilder(NAME, " - json", provisioner, state -> apply("EclipseJsonFormatterStepImpl", state));
58+
}
59+
60+
/** Provides default configuration for XML formatter */
61+
public static EclipseBasedStepBuilder createXmlBuilder(Provisioner provisioner) {
62+
return new EclipseBasedStepBuilder(NAME, " - xml", provisioner, state -> apply("EclipseXmlFormatterStepImpl", state));
63+
}
64+
65+
private static FormatterFunc apply(String className, EclipseBasedStepBuilder.State state) throws Exception {
66+
Class<?> formatterClazz = state.loadClass(FORMATTER_PACKAGE + className);
67+
Object formatter = formatterClazz.getConstructor(Properties.class).newInstance(state.getPreferences());
68+
Method method = formatterClazz.getMethod(FORMATTER_METHOD, String.class);
69+
return input -> {
70+
try {
71+
return (String) method.invoke(formatter, input);
72+
} catch (InvocationTargetException exceptionWrapper) {
73+
Throwable throwable = exceptionWrapper.getTargetException();
74+
Exception exception = (throwable instanceof Exception) ? (Exception) throwable : null;
75+
throw (null == exception) ? exceptionWrapper : exception;
76+
}
77+
};
78+
}
79+
80+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
@ParametersAreNonnullByDefault
2+
@ReturnValuesAreNonnullByDefault
3+
package com.diffplug.spotless.extra.wtp;
4+
5+
import javax.annotation.ParametersAreNonnullByDefault;
6+
7+
import com.diffplug.spotless.annotations.ReturnValuesAreNonnullByDefault;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Spotless formatter based on Eclipse-WTP version 3.9.5 (see https://www.eclipse.org/webtools/)
2+
com.diffplug.spotless:spotless-eclipse-wtp:3.9.5
3+
com.diffplug.spotless:spotless-eclipse-base:3.0.0
4+
com.google.code.findbugs:annotations:3.0.0
5+
com.google.code.findbugs:jsr305:3.0.0
6+
com.ibm.icu:icu4j:61.1
7+
org.eclipse.emf:org.eclipse.emf.common:2.12.0
8+
org.eclipse.emf:org.eclipse.emf.ecore:2.12.0
9+
org.eclipse.platform:org.eclipse.core.commands:3.9.100
10+
org.eclipse.platform:org.eclipse.core.contenttype:3.7.0
11+
org.eclipse.platform:org.eclipse.core.filebuffers:3.6.200
12+
org.eclipse.platform:org.eclipse.core.filesystem:1.7.100
13+
org.eclipse.platform:org.eclipse.core.jobs:3.10.0
14+
org.eclipse.platform:org.eclipse.core.resources:3.13.0
15+
org.eclipse.platform:org.eclipse.core.runtime:3.14.0
16+
org.eclipse.platform:org.eclipse.equinox.app:1.3.500
17+
org.eclipse.platform:org.eclipse.equinox.common:3.10.0
18+
org.eclipse.platform:org.eclipse.equinox.preferences:3.7.100
19+
org.eclipse.platform:org.eclipse.equinox.registry:3.8.0
20+
#Spotless currently loads all transitive dependencies.
21+
#jface requires platform specific JARs (not used by formatter), which are not hosted via M2.
22+
#org.eclipse.platform:org.eclipse.jface.text:3.13.0
23+
#org.eclipse.platform:org.eclipse.jface:3.14.0
24+
org.eclipse.platform:org.eclipse.osgi.services:3.7.0
25+
org.eclipse.platform:org.eclipse.osgi:3.13.0
26+
org.eclipse.platform:org.eclipse.text:3.6.300
27+
org.eclipse.xsd:org.eclipse.xsd:2.12.0
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Copyright 2016 DiffPlug
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.diffplug.spotless.extra.wtp;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import java.io.File;
21+
import java.io.FileOutputStream;
22+
import java.io.IOException;
23+
import java.io.OutputStream;
24+
import java.util.Arrays;
25+
import java.util.Properties;
26+
import java.util.function.Consumer;
27+
import java.util.function.Function;
28+
import java.util.stream.Collectors;
29+
30+
import org.junit.Test;
31+
import org.junit.runner.RunWith;
32+
import org.junit.runners.Parameterized;
33+
import org.junit.runners.Parameterized.Parameter;
34+
import org.junit.runners.Parameterized.Parameters;
35+
36+
import com.diffplug.spotless.FormatterStep;
37+
import com.diffplug.spotless.Provisioner;
38+
import com.diffplug.spotless.TestProvisioner;
39+
import com.diffplug.spotless.extra.EclipseBasedStepBuilder;
40+
import com.diffplug.spotless.extra.eclipse.EclipseCommonTests;
41+
42+
@RunWith(value = Parameterized.class)
43+
public class EclipseWtpFormatterStepTest extends EclipseCommonTests {
44+
45+
private enum WTP {
46+
// @formatter:off
47+
CSS( "body {\na: v; b: \nv;\n} \n",
48+
"body {\n\ta: v;\n\tb: v;\n}",
49+
WtpEclipseFormatterStep::createCssBuilder),
50+
HTML( "<!DOCTYPE html> <html>\t<head> <meta charset=\"UTF-8\"></head>\n</html> ",
51+
"<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"UTF-8\">\n</head>\n</html>\n",
52+
WtpEclipseFormatterStep::createHtmlBuilder),
53+
JS( "function f( ) {\na.b(1,\n2);}",
54+
"function f() {\n a.b(1, 2);\n}",
55+
WtpEclipseFormatterStep::createJsBuilder),
56+
JSON( "{\"a\": \"b\", \"c\": { \"d\": \"e\",\"f\": \"g\"}}",
57+
"{\n\t\"a\": \"b\",\n\t\"c\": {\n\t\t\"d\": \"e\",\n\t\t\"f\": \"g\"\n\t}\n}",
58+
WtpEclipseFormatterStep::createJsonBuilder),
59+
XML( "<a><b> c</b></a>", "<a>\n\t<b> c</b>\n</a>",
60+
WtpEclipseFormatterStep::createXmlBuilder);
61+
// @formatter:on
62+
63+
public final String input;
64+
public final String expectation;
65+
public final Function<Provisioner, EclipseBasedStepBuilder> builderMethod;
66+
67+
private WTP(String input, final String expectation, Function<Provisioner, EclipseBasedStepBuilder> builderMethod) {
68+
this.input = input;
69+
this.expectation = expectation;
70+
this.builderMethod = builderMethod;
71+
}
72+
}
73+
74+
@Parameters(name = "{0}")
75+
public static Iterable<WTP> data() {
76+
//TODO: XML is excluded. How to provide base location will be addressed by separate PR.
77+
return Arrays.asList(WTP.values()).stream().filter(e -> e != WTP.XML).collect(Collectors.toList());
78+
}
79+
80+
@Parameter(0)
81+
public WTP wtp;
82+
83+
@Override
84+
protected String[] getSupportedVersions() {
85+
return new String[]{"4.7.3a"};
86+
}
87+
88+
@Override
89+
protected String getTestInput(String version) {
90+
return wtp.input;
91+
}
92+
93+
@Override
94+
protected String getTestExpectation(String version) {
95+
return wtp.expectation;
96+
}
97+
98+
@Override
99+
protected FormatterStep createStep(String version) {
100+
EclipseBasedStepBuilder builder = wtp.builderMethod.apply(TestProvisioner.mavenCentral());
101+
builder.setVersion(version);
102+
return builder.build();
103+
}
104+
105+
/**
106+
* Check that configuration change is supported by all WTP formatters.
107+
* Some of the formatters only support static workspace configuration.
108+
* Hence separated class loaders are required for different configurations.
109+
*/
110+
@Test
111+
public void multipleConfigurations() throws Exception {
112+
FormatterStep tabFormatter = createStepForDefaultVersion(config -> {
113+
config.setProperty("indentationChar", "tab");
114+
config.setProperty("indentationSize", "1");
115+
});
116+
FormatterStep spaceFormatter = createStepForDefaultVersion(config -> {
117+
config.setProperty("indentationChar", "space");
118+
config.setProperty("indentationSize", "5");
119+
});
120+
121+
assertThat(formatWith(tabFormatter)).as("Tab formatting output unexpected").isEqualTo(wtp.expectation); //This is the default configuration
122+
assertThat(formatWith(spaceFormatter)).as("Space formatting output unexpected").isEqualTo(wtp.expectation.replace("\t", " "));
123+
}
124+
125+
private String formatWith(FormatterStep formatter) throws Exception {
126+
File baseLocation = File.createTempFile("EclipseWtpFormatterStepTest-", ".xml"); //Only required for relative path lookup
127+
return formatter.format(wtp.input, baseLocation);
128+
}
129+
130+
private FormatterStep createStepForDefaultVersion(Consumer<Properties> config) throws IOException {
131+
Properties configProps = new Properties();
132+
config.accept(configProps);
133+
File tempFile = File.createTempFile("EclipseWtpFormatterStepTest-", ".properties");
134+
OutputStream tempOut = new FileOutputStream(tempFile);
135+
configProps.store(tempOut, "test properties");
136+
EclipseBasedStepBuilder builder = wtp.builderMethod.apply(TestProvisioner.mavenCentral());
137+
builder.setVersion(WtpEclipseFormatterStep.defaultVersion());
138+
builder.setPreferences(Arrays.asList(tempFile));
139+
return builder.build();
140+
}
141+
}

lib/src/main/java/com/diffplug/spotless/JarState.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,15 @@ public ClassLoader getClassLoader() {
9292
return SpotlessCache.instance().classloader(this);
9393
}
9494

95+
/**
96+
* Returns a classloader containing only the jars in this JarState.
97+
*
98+
* The lifetime of the underlying cacheloader is controlled by {@link SpotlessCache}.
99+
*/
100+
public ClassLoader getClassLoader(Serializable key) {
101+
return SpotlessCache.instance().classloader(key, this);
102+
}
103+
95104
/** Returns unmodifiable view on sorted Maven coordinates */
96105
public Set<String> getMavenCoordinates() {
97106
return Collections.unmodifiableSet(mavenCoordinates);

lib/src/main/java/com/diffplug/spotless/SpotlessCache.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,14 @@ public final int hashCode() {
5959

6060
@SuppressFBWarnings("DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED")
6161
synchronized ClassLoader classloader(JarState state) {
62-
SerializedKey key = new SerializedKey(state);
62+
return classloader(state, state);
63+
}
64+
65+
@SuppressFBWarnings("DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED")
66+
synchronized ClassLoader classloader(Serializable key, JarState state) {
67+
SerializedKey serializedKey = new SerializedKey(key);
6368
return cache
64-
.computeIfAbsent(key, k -> new URLClassLoader(state.jarUrls(), null));
69+
.computeIfAbsent(serializedKey, k -> new URLClassLoader(state.jarUrls(), null));
6570
}
6671

6772
static SpotlessCache instance() {

0 commit comments

Comments
 (0)