diff --git a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/SpotlessEclipseFramework.java b/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/SpotlessEclipseFramework.java
index 2eaf9e568e..4292c7914c 100644
--- a/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/SpotlessEclipseFramework.java
+++ b/_ext/eclipse-base/src/main/java/com/diffplug/spotless/extra/eclipse/base/SpotlessEclipseFramework.java
@@ -53,7 +53,7 @@ public enum DefaultBundles {
*
*
* Per default, the platform is not activated. Some plugins use this information
- * to determine whether they are running in a headless modes (without IDE).
+ * to determine whether they are running in a headless mode (without IDE).
*
*/
PLATFORM(org.eclipse.core.internal.runtime.PlatformActivator.class, Bundle.RESOLVED),
diff --git a/lib-extra/build.gradle b/lib-extra/build.gradle
index 491531f0c7..4b8717f4f4 100644
--- a/lib-extra/build.gradle
+++ b/lib-extra/build.gradle
@@ -85,6 +85,26 @@ p2deps {
p2repo 'https://download.eclipse.org/tools/cdt/releases/10.7/'
install 'org.eclipse.cdt.core'
}
+
+ /*
+ * JDT core manipulation required for clean-up base interfaces and import sorting
+ * It depends on JDT core, which is required for formatting.
+ */
+ /**compile("org.eclipse.jdt:org.eclipse.jdt.core.manipulation:${VER_ECLIPSE_JDT_CORE_MANIPULATION}") {
+ exclude group: 'org.eclipse.jdt', module: 'org.eclipse.jdt.launching'
+ exclude group: 'org.eclipse.platform', module: 'org.eclipse.ant.core'
+ exclude group: 'org.eclipse.platform', module: 'org.eclipse.core.expressions'
+ }*/
+
+ /*
+ * JDT UI required for clean-up.
+ * Only the org.eclipse.jdt.internal.corext.fix package is required.
+ * All dependencies (like SWT) are excluded.
+ */
+ /**compile("org.eclipse.jdt:org.eclipse.jdt.ui:${VER_ECLIPSE_JDT_UI}") {
+ exclude group: 'org.eclipse.platform'
+ exclude group: 'org.eclipse.jdt'
+ }*/
}
// we'll hold the core lib to a high standard
diff --git a/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/CleanUpFactory.java b/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/CleanUpFactory.java
new file mode 100644
index 0000000000..425f569363
--- /dev/null
+++ b/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/CleanUpFactory.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright 2016 DiffPlug
+ *
+ * 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
+ *
+ * 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 com.diffplug.spotless.extra.glue.jdt;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Properties;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+import javax.xml.xpath.XPathExpressionException;
+
+import org.assertj.core.util.Sets;
+import org.eclipse.jdt.internal.corext.fix.CleanUpConstants;
+import org.eclipse.jdt.internal.corext.fix.CleanUpConstantsOptions;
+import org.eclipse.jdt.ui.cleanup.CleanUpOptions;
+import org.eclipse.jdt.ui.cleanup.ICleanUp;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+/** Provides configured clean-up implementations. */
+final class CleanUpFactory {
+
+ private final static Set UNSUPPORTED_CLASSES = Collections.unmodifiableSet(Sets.newLinkedHashSet(
+ "org.eclipse.jdt.internal.ui.fix.UnimplementedCodeCleanUp" //Would require Eclipse templates
+ ));
+
+ @SuppressWarnings("serial")
+ private final static Map UNSUPPORTED_CONFIG = Collections.unmodifiableMap(new HashMap() {
+ {
+ put(CleanUpConstants.REMOVE_UNUSED_CODE_IMPORTS, new FixedValue("false", "Unused import clean-up only works in case all imports can be resolved. As an alternative use: " + CleanUpConstants.ORGANIZE_IMPORTS));
+ }
+ });
+
+ private final static String CLEAN_UP_CONFIG_FILE_NAME = "plugin.xml";
+ private final static String CLEAN_UP_CONFIG_DEPENDENCY_NAME = "org.eclipse.jdt.ui";
+ private static List> CLEAN_UP_SEQUENCE = null;
+ private final CleanUpOptions options;
+
+ CleanUpFactory(Properties settings) {
+ options = new CleanUpOptions();
+ Logger logger = LoggerFactory.getLogger(CleanUpFactory.class);
+ CleanUpConstantsOptions.setDefaultOptions(CleanUpConstants.DEFAULT_CLEAN_UP_OPTIONS, options);
+ UNSUPPORTED_CONFIG.entrySet().stream().forEach(entry -> options.setOption(entry.getKey(), entry.getValue().value));
+ settings.forEach((key, value) -> {
+ FixedValue fixed = UNSUPPORTED_CONFIG.get(key);
+ if (null != fixed && fixed.value != value) {
+ logger.warn(String.format("Using %s for %s instead of %s: %s", fixed.value, key, value, fixed.reason));
+ } else {
+ options.setOption(key.toString(), value.toString());
+ }
+ });
+ try {
+ initializeCleanupActions();
+ } catch (IOException | ParserConfigurationException | XPathExpressionException e) {
+ throw new RuntimeException("Faild to read Eclipse Clean-Up configuration.", e);
+ }
+ }
+
+ private static synchronized void initializeCleanupActions() throws IOException, ParserConfigurationException, XPathExpressionException {
+ if (null != CLEAN_UP_SEQUENCE) {
+ return;
+ }
+ ClassLoader loader = CleanUpFactory.class.getClassLoader();
+ Optional configUrl = Collections.list(loader.getResources(CLEAN_UP_CONFIG_FILE_NAME)).stream().filter(url -> url.getPath().contains(CLEAN_UP_CONFIG_DEPENDENCY_NAME)).findAny();
+ if (!configUrl.isPresent()) {
+ throw new RuntimeException("Could not find JAR containing " + CLEAN_UP_CONFIG_DEPENDENCY_NAME + ":" + CLEAN_UP_CONFIG_FILE_NAME);
+ }
+ InputStream configXmlStream = configUrl.get().openStream();
+ try {
+ SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser();
+ CleanUpExtensionHandler handler = new CleanUpExtensionHandler();
+ saxParser.parse(configXmlStream, handler);
+ CLEAN_UP_SEQUENCE = handler.getCleanUpSequence();
+ } catch (SAXException e) {
+ //Add information about the XML location
+ throw new RuntimeException("Failed to parse " + configUrl.get().toExternalForm(), e);
+ }
+ }
+
+ public List create() {
+ return CLEAN_UP_SEQUENCE.stream().map(constructor -> {
+ try {
+ ICleanUp cleanUp = constructor.newInstance();
+ cleanUp.setOptions(options);
+ return cleanUp;
+ } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
+ throw new RuntimeException("Failed to created clean-up action for " + constructor.getName(), e);
+ }
+ }).collect(Collectors.toList());
+ }
+
+ private static class FixedValue {
+ public final String value;
+ public final String reason;
+
+ FixedValue(String value, String reason) {
+ this.value = value;
+ this.reason = reason;
+ }
+ };
+
+ private final static class CleanUpExtensionHandler extends DefaultHandler {
+ private final static String CLEAN_UP_ELEMENT_NAME = "cleanUp";
+ private final static String ID_ATTRIBUTE_NAME = "id";
+ private final static String CLASS_ATTRIBUTE_NAME = "class";
+ private final static String RUN_AFTER_ATTRIBUTE_NAME = "runAfter";
+ private final Map> constructor;
+ private final Map runAfter;
+ private final LinkedList sorted;
+
+ CleanUpExtensionHandler() {
+ constructor = new HashMap<>();
+ runAfter = new LinkedHashMap<>(); //E.g. the elements are already sorted
+ sorted = new LinkedList<>();
+ }
+
+ @Override
+ public void startDocument() throws SAXException {
+ constructor.clear();
+ runAfter.clear();
+ sorted.clear();
+ super.startDocument();
+ }
+
+ @Override
+ public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
+ if (CLEAN_UP_ELEMENT_NAME == qName) {
+ String id = getMandatoryAttribute(attributes, ID_ATTRIBUTE_NAME);
+ String className = getMandatoryAttribute(attributes, CLASS_ATTRIBUTE_NAME);
+ if (!UNSUPPORTED_CLASSES.contains(className)) {
+ try {
+ Class> clazz = Class.forName(className);
+ Class extends ICleanUp> clazzImplementsICleanUp = clazz.asSubclass(ICleanUp.class);
+ constructor.put(id, clazzImplementsICleanUp.getConstructor());
+ } catch (ClassNotFoundException | ClassCastException | NoSuchMethodException | SecurityException e) {
+ throw new SAXException("Failed to obtain constructor for " + CLEAN_UP_ELEMENT_NAME + " element class " + className, e);
+ }
+ }
+ String runAfterId = attributes.getValue(RUN_AFTER_ATTRIBUTE_NAME);
+ if (null == runAfterId) {
+ sorted.push(id);
+ } else {
+ runAfter.put(id, runAfterId);
+ }
+ }
+ super.startElement(uri, localName, qName, attributes);
+ }
+
+ private static String getMandatoryAttribute(Attributes attributes, String qName) throws SAXException {
+ String value = attributes.getValue(qName);
+ if (null == value) {
+ throw new SAXException(CLEAN_UP_ELEMENT_NAME + " element without " + qName + " attribute.");
+ }
+ return value;
+ }
+
+ @Override
+ public void endDocument() throws SAXException {
+ if (runAfter.isEmpty()) {
+ throw new SAXException(CLEAN_UP_ELEMENT_NAME + " element has not been found in XML.");
+ }
+ while (!runAfter.isEmpty()) {
+ //E.g. the elements are already sorted. Hence only one iteration is expected.
+ List foundEntries = new ArrayList<>(runAfter.size());
+ for (Map.Entry entry : runAfter.entrySet()) {
+ int runAfterIndex = sorted.lastIndexOf(entry.getValue());
+ if (0 <= runAfterIndex) {
+ foundEntries.add(entry.getKey());
+ sorted.add(runAfterIndex + 1, entry.getKey());
+ }
+ }
+ foundEntries.forEach(e -> runAfter.remove(e));
+ if (foundEntries.isEmpty()) {
+ throw new SAXException(CLEAN_UP_ELEMENT_NAME + " element the following precessor IDs cannot be resolved: " + runAfter.values().stream().collect(Collectors.joining("; ")));
+ }
+ }
+ super.endDocument();
+ }
+
+ public List> getCleanUpSequence() {
+ return sorted.stream().map(id -> constructor.get(id)).filter(clazz -> null != clazz).collect(Collectors.toList());
+ }
+ }
+
+}
diff --git a/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/EclipseJdtCleanUpStepImpl.java b/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/EclipseJdtCleanUpStepImpl.java
new file mode 100644
index 0000000000..5644f1e41e
--- /dev/null
+++ b/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/EclipseJdtCleanUpStepImpl.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright 2016 DiffPlug
+ *
+ * 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
+ *
+ * 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 com.diffplug.spotless.extra.glue.jdt;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+import java.util.Properties;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.jdt.core.ICompilationUnit;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.dom.CompilationUnit;
+import org.eclipse.jdt.core.manipulation.SharedASTProviderCore;
+import org.eclipse.jdt.core.refactoring.CompilationUnitChange;
+import org.eclipse.jdt.ui.cleanup.CleanUpContext;
+import org.eclipse.jdt.ui.cleanup.CleanUpRequirements;
+import org.eclipse.jdt.ui.cleanup.ICleanUp;
+import org.eclipse.jdt.ui.cleanup.ICleanUpFix;
+import org.eclipse.ltk.core.refactoring.RefactoringStatus;
+import org.eclipse.ltk.core.refactoring.RefactoringStatusEntry;
+import org.eclipse.text.edits.TextEdit;
+import org.eclipse.text.edits.UndoEdit;
+
+/** Clean-up step which calls out to the Eclipse JDT clean-up / import sorter. */
+public class EclipseJdtCleanUpStepImpl extends EclipseJdtCoreManipulation {
+
+ /**
+ * In case of Eclipse JDT clean-up problems (warnings + errors)
+ * the clean-up step is skipped if not problems shall not be ignored.
+ *
+ * Value is either 'true' or 'false' ('false' per default)
+ *
+ */
+ public static final String IGNORE_CLEAN_UP_PROBLEMS = "ignoreCleanUpProblems";
+
+ private final boolean ignoreCleanUpProblems;
+ private final IJavaProject jdtConfiguration;
+ private final CleanUpFactory cleanUpFactory;
+
+ public EclipseJdtCleanUpStepImpl(Properties settings) throws Exception {
+ jdtConfiguration = createProject(settings);
+ cleanUpFactory = new CleanUpFactory(settings);
+ ignoreCleanUpProblems = Boolean.parseBoolean(settings.getProperty(IGNORE_CLEAN_UP_PROBLEMS, "false"));
+ }
+
+ /** Formats Java raw text. The file-location is used in log messages. */
+ public String format(String raw, String fileLocation) throws Exception {
+ ICompilationUnit compilationUnit = createCompilationUnit(raw, jdtConfiguration);
+ SpotlessRefactoring refactoring = new SpotlessRefactoring(compilationUnit, ignoreCleanUpProblems);
+ RefactoringStatus report = refactoring.apply(cleanUpFactory.create());
+ Arrays.stream(report.getEntries()).map(entry -> new SpotlessStatus(entry, fileLocation)).forEach(status -> logger.log(status));
+ return compilationUnit.getBuffer().getContents();
+ }
+
+ /**
+ * Spotless version of {@code org.eclipse.jdt.internal.corext.fix.CleanUpRefactoring}.
+ *
+ * Spotless does not request (graphical) user feedback neither does it provide undo-information.
+ * Since Spotless re-factoring / formatting is applied without any further explanation of the changes (preview, warnings, ...),
+ * it skips per default steps reporting problems (non-fatal errors or warnings) to ensure that the result is as expected by the user.
+ * Spotless applies the JDT re-factoring without providing a project scope (dependencies, ...).
+ * Hence steps can cause (fatal) errors which would pass within an Eclipse project.
+ * Unlike the Eclipse re-factoring process, Spotless does not abort in case a step
+ * fails, but just reports and skips the step.
+ */
+ private static class SpotlessRefactoring {
+
+ private final ICompilationUnit source;
+ private final ICompilationUnit[] sources;
+ private final boolean ignoreProblems;
+ private final IProgressMonitor doNotMonitor;
+ private CompilationUnit lazyAst;
+ private boolean astIsFresh;
+
+ SpotlessRefactoring(ICompilationUnit sourceToRefactor, boolean ignoreCleanUpProblems) {
+ source = sourceToRefactor;
+ sources = new ICompilationUnit[]{sourceToRefactor};
+ ignoreProblems = ignoreCleanUpProblems;
+ doNotMonitor = new NullProgressMonitor();
+ lazyAst = null;
+ astIsFresh = false;
+ }
+
+ RefactoringStatus apply(List steps) throws CoreException {
+ RefactoringStatus overallStatus = new RefactoringStatus();
+ for (ICleanUp step : steps) {
+ apply(step, overallStatus);
+ }
+ return overallStatus;
+ }
+
+ private void apply(ICleanUp step, RefactoringStatus overallStatus) throws CoreException {
+ RefactoringStatus preCheckStatus = step.checkPreConditions(source.getJavaProject(), sources, doNotMonitor);
+ overallStatus.merge(preCheckStatus);
+ if (isStepOk(preCheckStatus)) {
+ CleanUpContext context = createContext(step.getRequirements());
+ ICleanUpFix fix = step.createFix(context);
+ RefactoringStatus postCheckStatus = apply(step, Optional.ofNullable(fix));
+ overallStatus.merge(postCheckStatus);
+ }
+ }
+
+ private RefactoringStatus apply(ICleanUp step, Optional fix) throws CoreException {
+ RefactoringStatus postCheckStatus = new RefactoringStatus();
+ if (fix.isPresent()) {
+ CompilationUnitChange change = fix.get().createChange(doNotMonitor);
+ TextEdit edit = change.getEdit();
+ if (null != edit) {
+ UndoEdit undo = source.applyTextEdit(edit, doNotMonitor);
+ postCheckStatus = step.checkPostConditions(doNotMonitor);
+ if (isStepOk(postCheckStatus)) {
+ astIsFresh = false;
+ } else {
+ postCheckStatus.addInfo("Undo step " + step.getClass().getSimpleName());
+ if (null != undo) {
+ source.applyTextEdit(undo, doNotMonitor);
+ }
+ }
+ }
+ }
+ return postCheckStatus;
+ }
+
+ private boolean isStepOk(RefactoringStatus stepStatus) {
+ if (ignoreProblems) {
+ return stepStatus.getSeverity() < RefactoringStatus.FATAL;
+ }
+ return stepStatus.getSeverity() < RefactoringStatus.WARNING;
+ }
+
+ private CleanUpContext createContext(CleanUpRequirements requirements) {
+ if ((requirements.requiresAST() && null == lazyAst) ||
+ (requirements.requiresFreshAST() && false == astIsFresh)) {
+ lazyAst = SharedASTProviderCore.getAST(source, SharedASTProviderCore.WAIT_YES, null);
+ astIsFresh = true;
+ }
+ return new CleanUpContext(source, lazyAst);
+ }
+
+ };
+
+ private static class SpotlessStatus implements IStatus {
+ private final IStatus cleanUpStatus;
+ private final String fileLocationAsPluginId;
+
+ SpotlessStatus(RefactoringStatusEntry entry, String fileLocation) {
+ cleanUpStatus = entry.toStatus();
+ fileLocationAsPluginId = fileLocation;
+ }
+
+ @Override
+ public IStatus[] getChildren() {
+ return cleanUpStatus.getChildren();
+ }
+
+ @Override
+ public int getCode() {
+ return cleanUpStatus.getCode();
+ }
+
+ @Override
+ public Throwable getException() {
+ return cleanUpStatus.getException();
+ }
+
+ @Override
+ public String getMessage() {
+ return cleanUpStatus.getMessage();
+ }
+
+ @Override
+ public String getPlugin() {
+ /*
+ * The plugin ID of the JDT Clean-Up is always a common string.
+ * Hence it does not add any valuable information for the Spotless user.
+ * It is replaced by the file location which is hidden from the JDT re-factoring
+ * process.
+ */
+ return fileLocationAsPluginId;
+ }
+
+ @Override
+ public int getSeverity() {
+ return cleanUpStatus.getSeverity();
+ }
+
+ @Override
+ public boolean isMultiStatus() {
+ return cleanUpStatus.isMultiStatus();
+ }
+
+ @Override
+ public boolean isOK() {
+ return cleanUpStatus.isOK();
+ }
+
+ @Override
+ public boolean matches(int severityMask) {
+ return cleanUpStatus.matches(severityMask);
+ }
+
+ };
+}
diff --git a/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/EclipseJdtCoreManipulation.java b/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/EclipseJdtCoreManipulation.java
new file mode 100644
index 0000000000..ec009b5f75
--- /dev/null
+++ b/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/EclipseJdtCoreManipulation.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright 2016 DiffPlug
+ *
+ * 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
+ *
+ * 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 com.diffplug.spotless.extra.glue.jdt;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.eclipse.core.internal.runtime.InternalPlatform;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IProjectDescription;
+import org.eclipse.core.resources.ProjectScope;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.ILog;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.content.IContentTypeManager;
+import org.eclipse.core.runtime.preferences.DefaultScope;
+import org.eclipse.core.runtime.preferences.IEclipsePreferences;
+import org.eclipse.jdt.core.IBuffer;
+import org.eclipse.jdt.core.ICompilationUnit;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.IPackageFragment;
+import org.eclipse.jdt.core.IPackageFragmentRoot;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jdt.core.manipulation.CodeStyleConfiguration;
+import org.eclipse.jdt.core.manipulation.JavaManipulation;
+import org.eclipse.jdt.internal.core.BufferManager;
+import org.eclipse.jdt.internal.core.CompilationUnit;
+import org.eclipse.jdt.internal.core.DefaultWorkingCopyOwner;
+import org.eclipse.jdt.internal.core.JavaCorePreferenceInitializer;
+import org.eclipse.jdt.internal.core.PackageFragment;
+import org.eclipse.jdt.internal.core.nd.indexer.Indexer;
+import org.eclipse.jdt.internal.corext.codemanipulation.CodeGenerationSettingsConstants;
+
+import com.diffplug.spotless.extra.eclipse.base.SpotlessEclipseFramework;
+
+/**
+ * Basic set-up for formatting features based on {@code org.eclipse.jdt:org.eclipse.jdt.core.manipulation}.
+ */
+class EclipseJdtCoreManipulation {
+ // The JDT UI shall be used for creating the settings (JavaUI not imported due to dependencies).
+ private final static String JDT_UI_PLUGIN_ID = "org.eclipse.jdt.ui";
+
+ private final static String ROOT_AS_SRC = "";
+ private final static String PROJECT_NAME = "spotless";
+ private final static String SOURCE_NAME = "source.java";
+
+ protected final ILog logger;
+
+ private final AtomicInteger uniqueProjectId = new AtomicInteger(0);
+ private final Map defaultOptions;
+
+ protected EclipseJdtCoreManipulation() throws Exception {
+ if (SpotlessEclipseFramework.setup(
+ core -> {
+ /*
+ * Indexer needs to exist (but is not used) for JDT clean-up.
+ * The indexer is not created in headless mode by JDT.
+ * 'Active' platform state signals non-headless mode ('Resolved' is default state)..
+ */
+ core.add(new org.eclipse.core.internal.registry.osgi.Activator());
+
+ core.add(new org.eclipse.core.internal.runtime.PlatformActivator());
+ core.add(new org.eclipse.core.internal.preferences.Activator());
+ core.add(new org.eclipse.core.internal.runtime.Activator());
+ },
+ config -> {
+ config.hideEnvironment();
+ config.disableDebugging();
+ config.ignoreUnsupportedPreferences();
+ config.useTemporaryLocations();
+ config.changeSystemLineSeparator();
+
+ /*
+ * The default 'no content type specific handling' is insufficient.
+ * The Java source type needs to be recognized by file extension.
+ */
+ config.add(IContentTypeManager.class, new JavaContentTypeManager());
+
+ config.useSlf4J(EclipseJdtOrganizeImportStepImpl.class.getPackage().getName());
+ config.set(InternalPlatform.PROP_OS, ""); //Required for org.eclipse.core.internal.resources.OS initialization
+ },
+ plugins -> {
+ plugins.applyDefault();
+
+ //JDT configuration requires an existing project source folder.
+ plugins.add(new org.eclipse.core.internal.filesystem.Activator());
+ plugins.add(new JavaCore());
+ })) {
+
+ initializeJdtUiPreferenceDefaults();
+ /*
+ * Assure that the 'allowed keys' are initialized, otherwise
+ * JProject will not accept any options.
+ */
+ new JavaCorePreferenceInitializer().initializeDefaultPreferences();
+
+ /*
+ * Don't run indexer in background (does not disable thread but the job scheduling)
+ */
+ Indexer.getInstance().enableAutomaticIndexing(false);
+ }
+
+ defaultOptions = new HashMap<>();
+ defaultOptions.put(JavaCore.COMPILER_SOURCE, getJavaCoreVersion());
+ logger = JavaCore.getPlugin().getLog();
+ }
+
+ private static void initializeJdtUiPreferenceDefaults() {
+ //Following values correspond org.eclipse.jdt.ui.PreferenceConstants (not used due to SWT dependency)
+ JavaManipulation.setPreferenceNodeId(JDT_UI_PLUGIN_ID);
+ IEclipsePreferences prefs = DefaultScope.INSTANCE.getNode(JDT_UI_PLUGIN_ID);
+
+ prefs.put(CodeStyleConfiguration.ORGIMPORTS_IMPORTORDER, "java;javax;org;com");
+ prefs.put(CodeStyleConfiguration.ORGIMPORTS_ONDEMANDTHRESHOLD, "99");
+ prefs.put(CodeStyleConfiguration.ORGIMPORTS_STATIC_ONDEMANDTHRESHOLD, "99");
+
+ prefs.put(CodeGenerationSettingsConstants.CODEGEN_KEYWORD_THIS, "false");
+ prefs.put(CodeGenerationSettingsConstants.CODEGEN_USE_OVERRIDE_ANNOTATION, "false");
+ prefs.put(CodeGenerationSettingsConstants.CODEGEN_ADD_COMMENTS, "true");
+ prefs.put(CodeGenerationSettingsConstants.ORGIMPORTS_IGNORELOWERCASE, "true");
+ }
+
+ private static String getJavaCoreVersion() {
+ final String javaVersion = System.getProperty("java.version");
+ final List orderedSupportedCoreVersions = JavaCore.getAllVersions();
+ for (String coreVersion : orderedSupportedCoreVersions) {
+ if (javaVersion.startsWith(coreVersion)) {
+ return coreVersion;
+ }
+ }
+ return orderedSupportedCoreVersions.get(orderedSupportedCoreVersions.size() - 1);
+ }
+
+ /**
+ * Creates a JAVA project and applies the configuration.
+ * @param settings Configuration settings
+ * @return Configured JAVA project
+ * @throws Exception In case the project creation fails
+ */
+ protected final IJavaProject createProject(Properties settings) throws Exception {
+ String uniqueProjectName = String.format("%s-%d", PROJECT_NAME, uniqueProjectId.incrementAndGet());
+ IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(uniqueProjectName);
+ // The project must be open before items (natures, folders, sources, ...) can be created
+ if (!project.exists()) {
+ project.create(null); //Might still exist in case of restarts and dedicated class loader
+ }
+ project.open(0, null);
+
+ //If the project nature is not set, the AST is not created for the compilation units
+ IProjectDescription description = project.getDescription();
+ description.setNatureIds(new String[]{JavaCore.NATURE_ID});
+ project.setDescription(description, null);
+ IJavaProject jProject = JavaCore.create(project);
+
+ Map allSettings = new HashMap<>(defaultOptions);
+ settings.forEach((key, value) -> {
+ allSettings.put(key.toString(), value.toString());
+ });
+ //Configure JDT manipulation processor
+ IEclipsePreferences projectPrefs = new ProjectScope(project.getProject()).getNode(JavaManipulation.getPreferenceNodeId());
+ allSettings.forEach((key, value) -> {
+ projectPrefs.put(key.toString(), value.toString());
+ });
+ /*
+ * Configure options taken directly from the Java project (without qualifier).
+ * Whether a setting is a Java project option or not, is filtered by the
+ * JavaCorePreferenceInitializer, initialized by the constructor of this class.
+ */
+ jProject.setOptions(allSettings);
+
+ // Eclipse source files require an existing source folder for creation
+ IPackageFragmentRoot src = jProject.getPackageFragmentRoot(jProject.getProject());
+ IPackageFragment pkg = src.createPackageFragment(ROOT_AS_SRC, true, null);
+ IFolder folder = project.getFolder(uniqueProjectName);
+ if (!folder.exists()) {
+ folder.create(0, false, null);
+ }
+
+ // Eclipse clean-up requires an existing source file
+ pkg.createCompilationUnit(SOURCE_NAME, "", true, null);
+
+ return jProject;
+ }
+
+ protected final ICompilationUnit createCompilationUnit(String contents, IJavaProject jProject) throws Exception {
+ IPackageFragmentRoot src = jProject.getPackageFragmentRoot(jProject.getProject());
+ IPackageFragment pkg = src.getPackageFragment(ROOT_AS_SRC);
+ return new RamCompilationUnit((PackageFragment) pkg, contents);
+ }
+
+ /** Keep compilation units in RAM */
+ private static class RamCompilationUnit extends CompilationUnit {
+
+ /*
+ * Each RAM compilation unit has its own buffer manager to
+ * prevent dropping of CUs when a maximum size is reached.
+ */
+ private final RamBufferManager manager;
+
+ RamCompilationUnit(PackageFragment parent, String contents) {
+ super(parent, SOURCE_NAME, DefaultWorkingCopyOwner.PRIMARY);
+ manager = new RamBufferManager();
+ IBuffer buffer = BufferManager.createBuffer(this);
+ buffer.setContents(contents.toCharArray());
+ manager.add(buffer);
+ }
+
+ @Override
+ public boolean exists() {
+ return true;
+ }
+
+ @Override
+ protected BufferManager getBufferManager() {
+ return manager;
+ }
+
+ @Override
+ public void save(IProgressMonitor pm, boolean force) throws JavaModelException {
+ //RAM CU is never stored on disk
+ }
+
+ @Override
+ public ICompilationUnit getWorkingCopy(IProgressMonitor monitor) throws JavaModelException {
+ return new RamCompilationUnit((PackageFragment) this.getParent(), getBuffer().getContents());
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return this == obj; //Working copies are not supported
+ }
+ }
+
+ /** Work around package privileges */
+ private static class RamBufferManager extends BufferManager {
+ void add(IBuffer buffer) {
+ addBuffer(buffer);
+ }
+ }
+}
diff --git a/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/EclipseJdtOrganizeImportStepImpl.java b/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/EclipseJdtOrganizeImportStepImpl.java
new file mode 100644
index 0000000000..910cfe6b89
--- /dev/null
+++ b/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/EclipseJdtOrganizeImportStepImpl.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2016 DiffPlug
+ *
+ * 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
+ *
+ * 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 com.diffplug.spotless.extra.glue.jdt;
+
+import java.util.Properties;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.OperationCanceledException;
+import org.eclipse.jdt.core.ICompilationUnit;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.dom.CompilationUnit;
+import org.eclipse.jdt.core.manipulation.OrganizeImportsOperation;
+import org.eclipse.jdt.core.manipulation.SharedASTProviderCore;
+
+/** Clean-up step which calls out to the Eclipse JDT clean-up / import sorter. */
+public class EclipseJdtOrganizeImportStepImpl extends EclipseJdtCoreManipulation {
+ private final IJavaProject jdtConfiguration; //The project stores the JDT clean-up configuration
+
+ public EclipseJdtOrganizeImportStepImpl(Properties settings) throws Exception {
+ jdtConfiguration = createProject(settings);
+ }
+
+ public String format(String raw) throws Exception {
+ ICompilationUnit compilationUnit = createCompilationUnit(raw, jdtConfiguration);
+ CompilationUnit ast = SharedASTProviderCore.getAST(compilationUnit, SharedASTProviderCore.WAIT_YES, null);
+ OrganizeImportsOperation formatOperation = new OrganizeImportsOperation(compilationUnit, ast, false, false, true, null);
+ try {
+ formatOperation.run(null);
+ return compilationUnit.getSource();
+ } catch (OperationCanceledException | CoreException e) {
+ throw new IllegalArgumentException("Invalid java syntax for formatting.", e);
+ }
+ }
+}
diff --git a/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/JavaContentTypeManager.java b/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/JavaContentTypeManager.java
new file mode 100644
index 0000000000..2ddf5d4d92
--- /dev/null
+++ b/lib-extra/src/jdt/java/com/diffplug/spotless/extra/glue/jdt/JavaContentTypeManager.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2016 DiffPlug
+ *
+ * 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
+ *
+ * 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 com.diffplug.spotless.extra.glue.jdt;
+
+import org.eclipse.core.internal.content.ContentType;
+import org.eclipse.core.internal.content.ContentTypeCatalog;
+import org.eclipse.core.runtime.content.IContentType;
+import org.eclipse.core.runtime.content.IContentTypeMatcher;
+import org.eclipse.core.runtime.preferences.IScopeContext;
+import org.eclipse.jdt.internal.compiler.util.SuffixConstants;
+
+import com.diffplug.spotless.extra.eclipse.base.service.NoContentTypeSpecificHandling;
+
+/**
+ * Java compilation unit validation requires Java content type to be recognized.
+ * All source is assumed to be Java. A content description is not required/provided.
+ *
+ * See {@code org.eclipse.jdt.internal.core.util.Util} for details.
+ *
+ */
+public class JavaContentTypeManager extends NoContentTypeSpecificHandling {
+
+ private final IContentType contentType;
+
+ public JavaContentTypeManager() {
+ contentType = ContentType.createContentType(
+ new ContentTypeCatalog(null, 0),
+ "",
+ "",
+ (byte) 0,
+ new String[]{SuffixConstants.EXTENSION_java, SuffixConstants.EXTENSION_JAVA},
+ new String[0],
+ new String[0],
+ "",
+ "",
+ null,
+ null);
+ }
+
+ @Override
+ public IContentType getContentType(String contentTypeIdentifier) {
+ return contentType;
+ }
+
+ @Override
+ public IContentType[] getAllContentTypes() {
+ return new IContentType[]{contentType};
+ }
+
+ @Override
+ public IContentTypeMatcher getMatcher(ISelectionPolicy customPolicy, IScopeContext context) {
+ return this;
+ }
+}
diff --git a/lib-extra/src/main/java/com/diffplug/spotless/extra/java/EclipseJdtCleanUpStep.java b/lib-extra/src/main/java/com/diffplug/spotless/extra/java/EclipseJdtCleanUpStep.java
new file mode 100644
index 0000000000..95fb774676
--- /dev/null
+++ b/lib-extra/src/main/java/com/diffplug/spotless/extra/java/EclipseJdtCleanUpStep.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2016-2023 DiffPlug
+ *
+ * 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
+ *
+ * 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 com.diffplug.spotless.extra.java;
+
+import com.diffplug.spotless.FormatterFunc;
+import com.diffplug.spotless.Jvm;
+import com.diffplug.spotless.Provisioner;
+import com.diffplug.spotless.extra.EquoBasedStepBuilder;
+
+import dev.equo.solstice.p2.P2Model;
+
+import java.io.File;
+import java.util.Properties;
+
+/** Formatter step which calls out to the Eclipse JDT formatter. */
+public final class EclipseJdtCleanUpStep {
+ // prevent direct instantiation
+ private EclipseJdtCleanUpStep() {}
+
+ private static final String NAME = "eclipse jdt cleanup";
+ private static final Jvm.Support JVM_SUPPORT = Jvm. support(NAME).add(11, "4.26");
+
+ public static String defaultVersion() {
+ return JVM_SUPPORT.getRecommendedFormatterVersion();
+ }
+
+ public static EquoBasedStepBuilder createBuilder(Provisioner provisioner) {
+ return new EquoBasedStepBuilder(NAME, provisioner, EclipseJdtCleanUpStep::apply) {
+ @Override
+ protected P2Model model(String version) {
+ var model = new P2Model();
+ addPlatformRepo(model, version);
+ model.getInstall().add("org.eclipse.jdt.core");
+ return model;
+ }
+
+ @Override
+ public void setVersion(String version) {
+ if (version.endsWith(".0")) {
+ String newVersion = version.substring(0, version.length() - 2);
+ System.err.println("Recommend replacing '" + version + "' with '" + newVersion + "' for Eclipse JDT");
+ version = newVersion;
+ }
+ super.setVersion(version);
+ }
+ };
+ }
+
+ private static FormatterFunc apply(EquoBasedStepBuilder.State state) throws Exception {
+ JVM_SUPPORT.assertFormatterSupported(state.getSemanticVersion());
+ Class> formatterClazz = state.getJarState().getClassLoader().loadClass("com.diffplug.spotless.extra.glue.jdt.EclipseJdtCleanUpStepImpl");
+ var formatter = formatterClazz.getConstructor(Properties.class).newInstance(state.getPreferences());
+ var method = formatterClazz.getMethod("format", String.class, File.class);
+ FormatterFunc formatterFunc = (FormatterFunc.NeedsFile) (input, file) -> (String) method.invoke(formatter, input, file);
+ return JVM_SUPPORT.suggestLaterVersionOnError(state.getSemanticVersion(), formatterFunc);
+ }
+}
diff --git a/lib-extra/src/main/java/com/diffplug/spotless/extra/java/EclipseJdtOrganizeImportsStep.java b/lib-extra/src/main/java/com/diffplug/spotless/extra/java/EclipseJdtOrganizeImportsStep.java
new file mode 100644
index 0000000000..ed446f5378
--- /dev/null
+++ b/lib-extra/src/main/java/com/diffplug/spotless/extra/java/EclipseJdtOrganizeImportsStep.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2016-2023 DiffPlug
+ *
+ * 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
+ *
+ * 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 com.diffplug.spotless.extra.java;
+
+import com.diffplug.spotless.FormatterFunc;
+import com.diffplug.spotless.Jvm;
+import com.diffplug.spotless.Provisioner;
+import com.diffplug.spotless.extra.EquoBasedStepBuilder;
+
+import dev.equo.solstice.p2.P2Model;
+
+import java.io.File;
+import java.util.Properties;
+
+/** Formatter step which calls out to the Eclipse JDT formatter. */
+public final class EclipseJdtOrganizeImportsStep {
+ // prevent direct instantiation
+ private EclipseJdtOrganizeImportsStep() {}
+
+ private static final String NAME = "eclipse jdt organizeImports";
+ private static final Jvm.Support JVM_SUPPORT = Jvm. support(NAME).add(11, "4.26");
+
+ public static String defaultVersion() {
+ return JVM_SUPPORT.getRecommendedFormatterVersion();
+ }
+
+ public static EquoBasedStepBuilder createBuilder(Provisioner provisioner) {
+ return new EquoBasedStepBuilder(NAME, provisioner, EclipseJdtOrganizeImportsStep::apply) {
+ @Override
+ protected P2Model model(String version) {
+ var model = new P2Model();
+ addPlatformRepo(model, version);
+ model.getInstall().add("org.eclipse.jdt.core");
+ return model;
+ }
+
+ @Override
+ public void setVersion(String version) {
+ if (version.endsWith(".0")) {
+ String newVersion = version.substring(0, version.length() - 2);
+ System.err.println("Recommend replacing '" + version + "' with '" + newVersion + "' for Eclipse JDT");
+ version = newVersion;
+ }
+ super.setVersion(version);
+ }
+ };
+ }
+
+ private static FormatterFunc apply(EquoBasedStepBuilder.State state) throws Exception {
+ JVM_SUPPORT.assertFormatterSupported(state.getSemanticVersion());
+ Class> formatterClazz = state.getJarState().getClassLoader().loadClass("com.diffplug.spotless.extra.glue.jdt.EclipseJdtOrganizeImportsStepImpl");
+ var formatter = formatterClazz.getConstructor(Properties.class).newInstance(state.getPreferences());
+ var method = formatterClazz.getMethod("format", String.class, File.class);
+ FormatterFunc formatterFunc = (FormatterFunc.NeedsFile) (input, file) -> (String) method.invoke(formatter, input, file);
+ return JVM_SUPPORT.suggestLaterVersionOnError(state.getSemanticVersion(), formatterFunc);
+ }
+}
diff --git a/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtCleanUpStepImplTest.java b/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtCleanUpStepImplTest.java
new file mode 100644
index 0000000000..7805537a6c
--- /dev/null
+++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtCleanUpStepImplTest.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2016 DiffPlug
+ *
+ * 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
+ *
+ * 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 com.diffplug.spotless.extra.java;
+
+import java.util.Properties;
+import java.util.function.Consumer;
+
+import com.diffplug.spotless.TestProvisioner;
+import com.diffplug.spotless.extra.EquoBasedStepBuilder;
+import com.diffplug.spotless.extra.eclipse.EquoResourceHarness;
+
+import org.assertj.core.util.Arrays;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.manipulation.CodeStyleConfiguration;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.opentest4j.AssertionFailedError;
+
+/** Eclipse JDT wrapper integration tests */
+public class EclipseJdtCleanUpStepImplTest extends EquoResourceHarness {
+ private final static String INPUT = "package p; class C{}";
+ private final static String EXPECTED = "package p;\nclass C {\n}";
+ private static String SOURCE_FILE_PATH = "some .. / \\ ill&formatted/$path";
+ private static TestData TEST_DATA = null;
+
+ @BeforeAll
+ public static void initializeStatic() throws Exception {
+ TEST_DATA = TestData.getTestDataOnFileSystem();
+ }
+
+ private static EquoBasedStepBuilder createBuilder() {
+ return EclipseJdtCleanUpStep.createBuilder(TestProvisioner.mavenCentral());
+ }
+
+ public EclipseJdtCleanUpStepImplTest() {
+ super(createBuilder(), INPUT, EXPECTED);
+ }
+
+ @Test
+ public void emptyInput() throws Throwable {
+ cleanUpTest("", "", config -> {});
+ }
+
+ @Test
+ public void defaultConfiguration() throws Throwable {
+ for (String testFile : Arrays.array("Simple", "Statics", "Wildcards")) {
+ try {
+ cleanUpTest(testFile, config -> {});
+ } catch (AssertionFailedError e) {
+ throw new AssertionFailedError(testFile + " - " + e.getMessage(), e.getExpected(), e.getActual());
+ }
+ }
+ }
+
+ @Test
+ public void invalidConfiguration() throws Throwable {
+ //Smoke test, no exceptions expected
+ cleanUpTest("", "", config -> {
+ config.put("invalid.key", "some.value");
+ });
+ cleanUpTest("", "", config -> {
+ config.put(JavaCore.COMPILER_SOURCE, "-42");
+ });
+ cleanUpTest("", "", config -> {
+ config.put(JavaCore.COMPILER_SOURCE, "Not an integer");
+ });
+ }
+
+ @Test
+ public void importConfiguration() throws Throwable {
+ String defaultOrganizedInput = TEST_DATA.input("ImportConfiguration");
+ cleanUpTest(defaultOrganizedInput, defaultOrganizedInput, config -> {});
+
+ String customOrganizedOutput = TEST_DATA.afterOrganizedImports("ImportConfiguration");
+ cleanUpTest(defaultOrganizedInput, customOrganizedOutput, config -> {
+ config.put(CodeStyleConfiguration.ORGIMPORTS_IMPORTORDER, "foo;#foo;");
+ });
+ }
+
+ private static void cleanUpTest(final String fileName, final Consumer config) throws Exception {
+ cleanUpTest(TEST_DATA.input(fileName), TEST_DATA.afterCleanUp(fileName), config);
+ }
+
+ private static void cleanUpTest(final String input, final String expected, final Consumer config) throws Exception {
+ Properties properties = new Properties();
+ config.accept(properties);
+ EclipseJdtCleanUpStepImpl formatter = new EclipseJdtCleanUpStepImpl(properties);
+ String output = formatter.format(input, SOURCE_FILE_PATH);
+ Assertions.assertEquals("Unexpected clean-up result " + TestData.toString(properties),
+ expected, output);
+ }
+}
diff --git a/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtOrganizeImportStepImplTest.java b/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtOrganizeImportStepImplTest.java
new file mode 100644
index 0000000000..8b23adf7ce
--- /dev/null
+++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/java/EclipseJdtOrganizeImportStepImplTest.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2016 DiffPlug
+ *
+ * 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
+ *
+ * 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 com.diffplug.spotless.extra.java;
+
+import java.util.Properties;
+import java.util.function.Consumer;
+
+import com.diffplug.spotless.TestProvisioner;
+import com.diffplug.spotless.extra.EquoBasedStepBuilder;
+import com.diffplug.spotless.extra.eclipse.EquoResourceHarness;
+
+import org.assertj.core.util.Arrays;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.manipulation.CodeStyleConfiguration;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.opentest4j.AssertionFailedError;
+
+/** Eclipse JDT wrapper integration tests */
+public class EclipseJdtOrganizeImportStepImplTest extends EquoResourceHarness {
+ private final static String INPUT = "package p; class C{}";
+ private final static String EXPECTED = "package p;\nclass C {\n}";
+
+ private static TestData TEST_DATA = null;
+
+ @BeforeAll
+ public static void initializeStatic() throws Exception {
+ TEST_DATA = TestData.getTestDataOnFileSystem();
+ }
+
+ private static EquoBasedStepBuilder createBuilder() {
+ return EclipseJdtOrganizeImportStep.createBuilder(TestProvisioner.mavenCentral());
+ }
+
+ public EclipseJdtOrganizeImportStepImplTest() {
+ super(createBuilder(), INPUT, EXPECTED);
+ }
+
+ @Test
+ public void emptyInput() throws Throwable {
+ organizeImportTest("", "", config -> {});
+ }
+
+ @Test
+ public void defaultConfiguration() throws Throwable {
+ for (String testFile : Arrays.array("Simple", "Statics", "Wildcards")) {
+ try {
+ organizeImportTest(testFile, config -> {});
+ } catch (AssertionFailedError e) {
+ throw new AssertionFailedError(testFile + " - " + e.getMessage(), e.getExpected(), e.getActual());
+ }
+ }
+ }
+
+ @Test
+ public void defaultPackage() throws Throwable {
+ String input = TEST_DATA.input("Simple").replaceFirst("package .+", "");
+ String expected = TEST_DATA.afterOrganizedImports("Simple").replaceFirst("package .+", "");
+ organizeImportTest(input, expected, config -> {});
+ }
+
+ @Test
+ public void invalidConfiguration() throws Throwable {
+ //Smoke test, no exceptions expected
+ organizeImportTest("", "", config -> {
+ config.put("invalid.key", "some.value");
+ });
+ organizeImportTest("", "", config -> {
+ config.put(JavaCore.COMPILER_SOURCE, "-42");
+ });
+ organizeImportTest("", "", config -> {
+ config.put(JavaCore.COMPILER_SOURCE, "Not an integer");
+ });
+ }
+
+ @Test
+ public void customConfiguration() throws Throwable {
+ String defaultOrganizedInput = TEST_DATA.input("ImportConfiguration");
+ organizeImportTest(defaultOrganizedInput, defaultOrganizedInput, config -> {});
+
+ String customOrganizedOutput = TEST_DATA.afterOrganizedImports("ImportConfiguration");
+ organizeImportTest(defaultOrganizedInput, customOrganizedOutput, config -> {
+ config.put(CodeStyleConfiguration.ORGIMPORTS_IMPORTORDER, "foo;#foo;");
+ });
+ }
+
+ private static void organizeImportTest(final String fileName, final Consumer config) throws Exception {
+ organizeImportTest(TEST_DATA.input(fileName), TEST_DATA.afterOrganizedImports(fileName), config);
+ }
+
+ private static void organizeImportTest(final String input, final String expected, final Consumer config) throws Exception {
+ Properties properties = new Properties();
+ config.accept(properties);
+ EclipseJdtOrganizeImportStepImpl formatter = new EclipseJdtOrganizeImportStepImpl(properties);
+ String output = formatter.format(input);
+ Assertions.assertEquals("Unexpected import organization " + TestData.toString(properties),
+ expected, output);
+ }
+}
diff --git a/lib-extra/src/test/java/com/diffplug/spotless/extra/java/TestData.java b/lib-extra/src/test/java/com/diffplug/spotless/extra/java/TestData.java
new file mode 100644
index 0000000000..db6aaa5b72
--- /dev/null
+++ b/lib-extra/src/test/java/com/diffplug/spotless/extra/java/TestData.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2016 DiffPlug
+ *
+ * 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
+ *
+ * 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 com.diffplug.spotless.extra.java;
+/*
+ * Copyright 2016 DiffPlug
+ *
+ * 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
+ *
+ * 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.
+ */
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Properties;
+
+public class TestData {
+ private static final String EXTENSION_INPUT = ".input";
+ private static final String EXTENSION_ORGANIZED_IMPORTS = ".organized";
+ private static final String EXTENSION_CLEAN_UP = ".cleanup";
+
+ public static TestData getTestDataOnFileSystem() {
+ final String userDir = System.getProperty("user.dir", ".");
+ Path dataPath = Paths.get(userDir, "src", "test", "resources");
+ if (Files.isDirectory(dataPath)) {
+ return new TestData(dataPath);
+ }
+ throw new IllegalArgumentException("Test data not found:" + dataPath.toString());
+ }
+
+ private final Path resourcesPath;
+
+ private TestData(Path resourcesPath) {
+ this.resourcesPath = resourcesPath.toAbsolutePath();
+ if (!Files.isDirectory(resourcesPath)) {
+ throw new IllegalArgumentException(String.format("'%1$s' is not a directory.", resourcesPath));
+ }
+ }
+
+ public String input(final String fileName) throws Exception {
+ Path filePath = resourcesPath.resolve(fileName + EXTENSION_INPUT);
+ return read(filePath);
+ }
+
+ public String afterOrganizedImports(final String fileName) {
+ Path filePath = resourcesPath.resolve(fileName + EXTENSION_ORGANIZED_IMPORTS);
+ return read(filePath);
+ }
+
+ public String afterCleanUp(final String fileName) {
+ Path filePath = resourcesPath.resolve(fileName + EXTENSION_CLEAN_UP);
+ return read(filePath);
+ }
+
+ private String read(final Path filePath) {
+ if (!Files.isRegularFile(filePath)) {
+ throw new IllegalArgumentException(String.format("'%1$s' is not a regular file.", filePath));
+ }
+ try {
+ String checkedOutFileContent = new String(java.nio.file.Files.readAllBytes(filePath), "ASCII");
+ return checkedOutFileContent.replace("\r", ""); //Align GIT end-of-line normalization
+ } catch (IOException e) {
+ throw new IllegalArgumentException(String.format("Failed to read '%1$s'.", filePath), e);
+ }
+ }
+
+ public static String toString(Properties properties) {
+ StringBuilder result = new StringBuilder();
+ result.append('[');
+ properties.forEach((k, v) -> {
+ result.append(k.toString());
+ result.append('=');
+ result.append(v.toString());
+ result.append(';');
+ });
+ result.append(']');
+ return result.toString();
+ }
+}
diff --git a/testlib/src/main/resources/java/eclipse/cleanup/ImportConfiguration.input b/testlib/src/main/resources/java/eclipse/cleanup/ImportConfiguration.input
new file mode 100644
index 0000000000..ba163e17f2
--- /dev/null
+++ b/testlib/src/main/resources/java/eclipse/cleanup/ImportConfiguration.input
@@ -0,0 +1,12 @@
+package bar.foo;
+
+import static foo.bar.B.someMethod; //Default configuration places static imports before none-static
+
+import foo.bar.B;
+
+class A {
+ static {
+ someMethod();
+ new B();
+ }
+}
diff --git a/testlib/src/main/resources/java/eclipse/cleanup/ImportConfiguration.organized b/testlib/src/main/resources/java/eclipse/cleanup/ImportConfiguration.organized
new file mode 100644
index 0000000000..e51cf93fd7
--- /dev/null
+++ b/testlib/src/main/resources/java/eclipse/cleanup/ImportConfiguration.organized
@@ -0,0 +1,12 @@
+package bar.foo;
+
+import foo.bar.B;
+
+import static foo.bar.B.someMethod; //Default configuration places static imports before none-static
+
+class A {
+ static {
+ someMethod();
+ new B();
+ }
+}
diff --git a/testlib/src/main/resources/java/eclipse/cleanup/Simple.input b/testlib/src/main/resources/java/eclipse/cleanup/Simple.input
new file mode 100644
index 0000000000..a837ae3d8d
--- /dev/null
+++ b/testlib/src/main/resources/java/eclipse/cleanup/Simple.input
@@ -0,0 +1,25 @@
+package bar.foo;
+
+import javax.net.SocketFactory; //Wrong order in Other group
+
+import java.net.Socket; //Wrong within group
+import java.lang.System; //Can be removed since implicitly imported
+import java.io.IOException;
+import java.io.PrintStream; //Class can be found but is not used
+
+import foo.bar.C; //Class cannot be found and is not used
+import foo.bar.B; //Class cannot be found but is used
+
+
+class A {
+ private static final SocketFactory FACTORY;
+ static {
+ FACTORY = SocketFactory.getDefault();
+ }
+ public static Socket open() throws IOException {
+ return FACTORY.createSocket();
+ }
+ public static B create() {
+ return new B();
+ }
+}
diff --git a/testlib/src/main/resources/java/eclipse/cleanup/Simple.organized b/testlib/src/main/resources/java/eclipse/cleanup/Simple.organized
new file mode 100644
index 0000000000..cead7fd671
--- /dev/null
+++ b/testlib/src/main/resources/java/eclipse/cleanup/Simple.organized
@@ -0,0 +1,22 @@
+package bar.foo;
+
+import java.io.IOException;
+import java.net.Socket; //Wrong within group
+
+import javax.net.SocketFactory; //Wrong order in Other group
+
+import foo.bar.B; //Class cannot be found but is used
+
+
+class A {
+ private static final SocketFactory FACTORY;
+ static {
+ FACTORY = SocketFactory.getDefault();
+ }
+ public static Socket open() throws IOException {
+ return FACTORY.createSocket();
+ }
+ public static B create() {
+ return new B();
+ }
+}
diff --git a/testlib/src/main/resources/java/eclipse/cleanup/Statics.input b/testlib/src/main/resources/java/eclipse/cleanup/Statics.input
new file mode 100644
index 0000000000..fb7e77809f
--- /dev/null
+++ b/testlib/src/main/resources/java/eclipse/cleanup/Statics.input
@@ -0,0 +1,15 @@
+package bar.foo;
+
+import static foo.bar.B.someMethod; // Is used
+import static foo.bar.B.someOtherMethod; // Covered already by non-static import
+import static foo.bar.B.notUsed; // Is not used
+
+import foo.bar.B;
+
+class A {
+ static {
+ someMethod();
+ B.someOtherMethod();
+ new B();
+ }
+}
diff --git a/testlib/src/main/resources/java/eclipse/cleanup/Statics.organized b/testlib/src/main/resources/java/eclipse/cleanup/Statics.organized
new file mode 100644
index 0000000000..d88b552ff6
--- /dev/null
+++ b/testlib/src/main/resources/java/eclipse/cleanup/Statics.organized
@@ -0,0 +1,13 @@
+package bar.foo;
+
+import static foo.bar.B.someMethod; // Is used
+
+import foo.bar.B;
+
+class A {
+ static {
+ someMethod();
+ B.someOtherMethod();
+ new B();
+ }
+}
diff --git a/testlib/src/main/resources/java/eclipse/cleanup/Wildcards.input b/testlib/src/main/resources/java/eclipse/cleanup/Wildcards.input
new file mode 100644
index 0000000000..3576db1b61
--- /dev/null
+++ b/testlib/src/main/resources/java/eclipse/cleanup/Wildcards.input
@@ -0,0 +1,18 @@
+package bar.foo;
+
+import static foo.bar.C.*; //Might be used
+import static foo.bar.B.someMethod; //Is used
+import static foo.bar.B.someOtherMethod; //Is not used
+import foo.bar.B; //Is used
+import foo.bar.E; //Not used
+import hello.world.*; //Might be used
+
+class A {
+ public static B create() {
+ someMethodOfC();
+ new ClassInHelloWorld();
+
+ someMethod();
+ return new B();
+ }
+}
diff --git a/testlib/src/main/resources/java/eclipse/cleanup/Wildcards.organized b/testlib/src/main/resources/java/eclipse/cleanup/Wildcards.organized
new file mode 100644
index 0000000000..8f7b8ef389
--- /dev/null
+++ b/testlib/src/main/resources/java/eclipse/cleanup/Wildcards.organized
@@ -0,0 +1,17 @@
+package bar.foo;
+
+import static foo.bar.B.someMethod; //Is used
+import static foo.bar.C.*; //Might be used
+
+import foo.bar.B; //Is used
+import hello.world.*; //Might be used
+
+class A {
+ public static B create() {
+ someMethodOfC();
+ new ClassInHelloWorld();
+
+ someMethod();
+ return new B();
+ }
+}