diff --git a/sb4e.test/.classpath b/sb4e.test/.classpath index ffa8aa244b3d25d7f04778db0c3b0215f7791d83..291fc90f3204c81b61541bf4fb2b7450984fb792 100644 --- a/sb4e.test/.classpath +++ b/sb4e.test/.classpath @@ -1,12 +1,12 @@ <?xml version="1.0" encoding="UTF-8"?> <classpath> - <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-9"> <attributes> <attribute name="maven.pomderived" value="true"/> </attributes> </classpathentry> <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/> - <classpathentry kind="src" path="src"/> - <classpathentry kind="src" path="resources"/> + <classpathentry kind="src" path="src/"/> + <classpathentry kind="src" path="resources/"/> <classpathentry kind="output" path="target/classes"/> </classpath> diff --git a/sb4e.test/.settings/org.eclipse.jdt.core.prefs b/sb4e.test/.settings/org.eclipse.jdt.core.prefs index 6e80039d3b822e65e46fbf18906ef652814e9505..63864f1a37b2edebce3e44a76bc790c00b5ba409 100644 --- a/sb4e.test/.settings/org.eclipse.jdt.core.prefs +++ b/sb4e.test/.settings/org.eclipse.jdt.core.prefs @@ -1,8 +1,8 @@ eclipse.preferences.version=1 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 -org.eclipse.jdt.core.compiler.compliance=1.8 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=9 +org.eclipse.jdt.core.compiler.compliance=9 org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning -org.eclipse.jdt.core.compiler.source=1.8 +org.eclipse.jdt.core.compiler.source=9 diff --git a/sb4e.test/META-INF/MANIFEST.MF b/sb4e.test/META-INF/MANIFEST.MF index ae208b70c2f3148cf17f073dd879ae07bd84dd60..0f3fb9a4f7322cd80d4dfd144942b4ac43f888fd 100644 --- a/sb4e.test/META-INF/MANIFEST.MF +++ b/sb4e.test/META-INF/MANIFEST.MF @@ -12,5 +12,7 @@ Require-Bundle: no.tobask.sb4e;bundle-version="0.9.0", org.eclipse.ui;bundle-version="3.109.0" Import-Package: org.eclipse.core.resources, org.eclipse.core.runtime;version="3.5.0", + org.eclipse.e4.core.contexts;version="1.6.0", org.eclipse.jdt.core, + org.eclipse.jdt.core.compiler, org.eclipse.swt.widgets diff --git a/sb4e.test/resources/no/tobask/sb4e/test/Test.fxml b/sb4e.test/resources/no/tobask/sb4e/test/Test.fxml index 5599e8b1dae787b952dc6fad07028b0956a47b03..2653627b42cc17744018d327480596ba54585797 100644 --- a/sb4e.test/resources/no/tobask/sb4e/test/Test.fxml +++ b/sb4e.test/resources/no/tobask/sb4e/test/Test.fxml @@ -2,7 +2,7 @@ <?import javafx.scene.layout.AnchorPane?> -<AnchorPane xmlns:fx="http://javafx.com/fxml/1"> +<AnchorPane xmlns:fx="http://javafx.com/fxml/1" fx:id="mainWindow"> <!-- TODO Add Nodes --> </AnchorPane> diff --git a/sb4e.test/src/no/tobask/sb4e/test/FxControllerValidatorTest.java b/sb4e.test/src/no/tobask/sb4e/test/FxControllerValidatorTest.java new file mode 100644 index 0000000000000000000000000000000000000000..344c6fa8daca93740139c9eca761e165ef1783b8 --- /dev/null +++ b/sb4e.test/src/no/tobask/sb4e/test/FxControllerValidatorTest.java @@ -0,0 +1,63 @@ +package no.tobask.sb4e.test; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.e4.core.contexts.IEclipseContext; +import org.eclipse.jdt.core.compiler.BuildContext; +import org.eclipse.jdt.core.compiler.CategorizedProblem; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.PlatformUI; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExternalResource; +import org.mockito.ArgumentCaptor; + +import javafx.embed.swt.FXCanvas; +import no.tobask.sb4e.FxControllerProblem; +import no.tobask.sb4e.FxControllerValidator; +import no.tobask.sb4e.FxmlDocumentListener; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.*; + +import java.net.MalformedURLException; + +public class FxControllerValidatorTest { + + IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject("testProject"); + + @Rule + public final ExternalResource projectResource = new JavaProjectResource(project); + + @Test + public void buildStarting_discoversAndReportsProblems_whenThereAreProblems() throws MalformedURLException { + FxControllerValidator validator = new FxControllerValidator(); + FxmlDocumentListener documentListener = mock(FxmlDocumentListener.class); + validator.setDocumentListener(documentListener); + + BuildContext buildContext = mock(BuildContext.class); + IFile controller = project.getFolder("src").getFile("TestController.java"); + IFile document = project.getFolder("src").getFile("Test.fxml"); + + when(buildContext.getFile()).thenReturn(controller); + when(documentListener.isAssignedController("src.TestController")).thenReturn(true); + when(documentListener.getDocument("src.TestController")) + .thenReturn(document.getLocationURI().toURL()); + + validator.buildStarting(new BuildContext[] {buildContext}, true); + ArgumentCaptor<CategorizedProblem[]> problems = ArgumentCaptor + .forClass(CategorizedProblem[].class); + verify(buildContext).recordNewProblems(problems.capture()); + CategorizedProblem[] recordedProblems = problems.getValue(); + assertEquals(1, recordedProblems.length); + FxControllerProblem problem = (FxControllerProblem) recordedProblems[0]; + String[] arguments = problem.getArguments(); + assertEquals(1, arguments.length); + assertEquals("mainWindow;javafx.scene.layout.AnchorPane", arguments[0]); + } + +} diff --git a/sb4e.test/src/no/tobask/sb4e/test/JavaProjectResource.java b/sb4e.test/src/no/tobask/sb4e/test/JavaProjectResource.java index 039f8f7ae620b85f91318f5c91379bb8513646e9..52c2fc85838c20e3342b69ad403d40c1b00bbece 100644 --- a/sb4e.test/src/no/tobask/sb4e/test/JavaProjectResource.java +++ b/sb4e.test/src/no/tobask/sb4e/test/JavaProjectResource.java @@ -48,11 +48,12 @@ public class JavaProjectResource extends ExternalResource { private String getContents(String fileName) throws IOException { StringBuilder builder = new StringBuilder(); - InputStream stream = getClass().getResourceAsStream(fileName); - BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); - String line; - while ((line = reader.readLine()) != null) { - builder.append(line); + try (InputStream stream = getClass().getResourceAsStream(fileName)) { + BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); + String line; + while ((line = reader.readLine()) != null) { + builder.append(line); + } } return builder.toString(); } diff --git a/sb4e/META-INF/MANIFEST.MF b/sb4e/META-INF/MANIFEST.MF index 139fa17ee2f38b4ecf0b5071b8c2a6963bab3033..a177ad00f67480fb7684e12fa93f013722f2d3d7 100644 --- a/sb4e/META-INF/MANIFEST.MF +++ b/sb4e/META-INF/MANIFEST.MF @@ -27,8 +27,13 @@ Import-Package: com.oracle.javafx.scenebuilder.kit.editor, org.eclipse.e4.core.di.annotations;version="1.6.0", org.eclipse.e4.ui.di, org.eclipse.jdt.core, + org.eclipse.jdt.core.compiler, org.eclipse.jdt.core.dom, - org.eclipse.jdt.launching + org.eclipse.jdt.core.dom.rewrite, + org.eclipse.jdt.launching, + org.eclipse.jdt.ui.text.java, + org.eclipse.jdt.ui.text.java.correction, + org.eclipse.text.edits Bundle-ActivationPolicy: lazy Bundle-ClassPath: . Export-Package: no.tobask.sb4e, diff --git a/sb4e/plugin.xml b/sb4e/plugin.xml index 0ab2b4c17323c2e7559b6a718368f92bb98af3aa..7298087ddeb36d8d67c96e1f49773a2f4e405633 100644 --- a/sb4e/plugin.xml +++ b/sb4e/plugin.xml @@ -80,5 +80,44 @@ class="no.tobask.sb4e.handlers.IncludeFxmlHandler"> </handler> </extension> + <extension + point="org.eclipse.jdt.core.compilationParticipant"> + <compilationParticipant + class="no.tobask.sb4e.FxControllerValidator" + id="FxControllerValidator" + createsProblems="true"> + <managedMarker + markerType="no.tobask.sb4e.fxcontrollerproblemmarker"> + </managedMarker> + </compilationParticipant> + </extension> + <extension + id="no.tobask.sb4e.fxcontrollerproblemmarker" + name="Fx controller problem" + point="org.eclipse.core.resources.markers"> + <super + type="org.eclipse.jdt.core.problem"> + </super> + </extension> + <extension + point="org.eclipse.jdt.ui.quickFixProcessors"> + <quickFixProcessor + class="no.tobask.sb4e.MissingFxIdsFixer" + id="no.tobask.sb4e.missingfxidsfixer" + name="Missing fxids fixer"> + <handledMarkerTypes> + <markerType + id="no.tobask.sb4e.fxcontrollerproblemmarker"> + </markerType> + </handledMarkerTypes> + <enablement> + <with variable="projectNatures"> + <iterate operator="or"> + <equals value="org.eclipse.jdt.core.javanature"/> + </iterate> + </with> + </enablement> + </quickFixProcessor> + </extension> </plugin> diff --git a/sb4e/src/no/tobask/sb4e/Activator.java b/sb4e/src/no/tobask/sb4e/Activator.java index 272fa110a5908b5b9241b5c7f70bffa06d34ae75..050eb555e892bae965c6016a84da1a2342e67a56 100644 --- a/sb4e/src/no/tobask/sb4e/Activator.java +++ b/sb4e/src/no/tobask/sb4e/Activator.java @@ -1,15 +1,28 @@ package no.tobask.sb4e; +import org.eclipse.core.resources.IResourceChangeEvent; +import org.eclipse.core.resources.ResourcesPlugin; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; public class Activator implements BundleActivator { private static BundleContext context; + private static FxmlDocumentListener fxmlDocumentListener = + new FxmlDocumentListener(); + private static EclipseProjectsClassLoader classLoader = new EclipseProjectsClassLoader(); public static BundleContext getContext() { return context; } + + public static FxmlDocumentListener getFxmlDocumentListener() { + return fxmlDocumentListener; + } + + public static EclipseProjectsClassLoader getClassLoader() { + return classLoader; + } /* * (non-Javadoc) @@ -17,6 +30,8 @@ public class Activator implements BundleActivator { */ public void start(BundleContext bundleContext) throws Exception { Activator.context = bundleContext; + ResourcesPlugin.getWorkspace().addResourceChangeListener(fxmlDocumentListener, + IResourceChangeEvent.POST_CHANGE); } /* @@ -25,6 +40,7 @@ public class Activator implements BundleActivator { */ public void stop(BundleContext bundleContext) throws Exception { Activator.context = null; + ResourcesPlugin.getWorkspace().removeResourceChangeListener(fxmlDocumentListener); } } diff --git a/sb4e/src/no/tobask/sb4e/FxControllerProblem.java b/sb4e/src/no/tobask/sb4e/FxControllerProblem.java new file mode 100644 index 0000000000000000000000000000000000000000..3023148f1a547de02fafca9e330958fa4fff29fb --- /dev/null +++ b/sb4e/src/no/tobask/sb4e/FxControllerProblem.java @@ -0,0 +1,91 @@ +package no.tobask.sb4e; + +import java.util.List; + +import org.eclipse.core.resources.IResource; +import org.eclipse.jdt.core.compiler.CategorizedProblem; + +public class FxControllerProblem extends CategorizedProblem { + + List<String> missingIds; + IResource resource; + + public FxControllerProblem(List<String> missingIds, IResource resource) { + this.missingIds = missingIds; + this.resource = resource; + } + + @Override + public String[] getArguments() { + return missingIds.toArray(new String[0]); + } + + @Override + public int getID() { + return 1234; + } + + @Override + public String getMessage() { + return "One or more fx:ids from associated FXML document do not have corresponding fields"; + } + + @Override + public char[] getOriginatingFileName() { + return resource.getName().toCharArray(); + } + + @Override + public int getSourceEnd() { + return -1; + } + + @Override + public int getSourceLineNumber() { + return 1; + } + + @Override + public int getSourceStart() { + return 0; + } + + @Override + public boolean isError() { + return false; + } + + @Override + public boolean isWarning() { + return true; + } + + @Override + public void setSourceEnd(int sourceEnd) { + // TODO Auto-generated method stub + + } + + @Override + public void setSourceLineNumber(int lineNumber) { + // TODO Auto-generated method stub + + } + + @Override + public void setSourceStart(int sourceStart) { + // TODO Auto-generated method stub + + } + + @Override + public int getCategoryID() { + return CAT_POTENTIAL_PROGRAMMING_PROBLEM; + } + + @Override + public String getMarkerType() { + return "no.tobask.sb4e.fxcontrollerproblemmarker"; + } + +} diff --git a/sb4e/src/no/tobask/sb4e/FxControllerValidator.java b/sb4e/src/no/tobask/sb4e/FxControllerValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..ad81556e3d3aa98f392c8c52ebf2864c15e5668f --- /dev/null +++ b/sb4e/src/no/tobask/sb4e/FxControllerValidator.java @@ -0,0 +1,101 @@ +package no.tobask.sb4e; + +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.eclipse.core.resources.IResource; +import org.eclipse.e4.core.contexts.IEclipseContext; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.compiler.BuildContext; +import org.eclipse.jdt.core.compiler.CategorizedProblem; +import org.eclipse.jdt.core.compiler.CompilationParticipant; +import org.eclipse.jdt.core.dom.AST; +import org.eclipse.jdt.core.dom.ASTParser; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.ui.PlatformUI; + +import com.oracle.javafx.scenebuilder.kit.i18n.I18N; +import com.oracle.javafx.scenebuilder.kit.fxom.FXOMDocument; +import com.oracle.javafx.scenebuilder.kit.fxom.FXOMInstance; +import com.oracle.javafx.scenebuilder.kit.fxom.FXOMObject; + +public class FxControllerValidator extends CompilationParticipant { + + FxmlDocumentListener documentListener; + + public FxControllerValidator() { + documentListener = Activator.getFxmlDocumentListener(); + } + + public void setDocumentListener(FxmlDocumentListener documentListener) { + this.documentListener = documentListener; + } + + @Override + public void buildStarting(BuildContext[] files, boolean isBatch) { + for (BuildContext file : files) { + ICompilationUnit clazz = (ICompilationUnit) JavaCore.create(file.getFile()); + String className = clazz.findPrimaryType().getFullyQualifiedName(); + if (documentListener.isAssignedController(className)) { + URL documentLocation = documentListener.getDocument(className); + try { + String fxmlContent = FXOMDocument.readContentFromURL(documentLocation); + FXOMDocument document = new FXOMDocument(fxmlContent, documentLocation, + Activator.getClassLoader(), I18N.getBundle()); + CompilationUnit ast = getAst(clazz); + file.recordNewProblems(getProblems(ast, document)); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + private CompilationUnit getAst(ICompilationUnit source) { + ASTParser parser = ASTParser.newParser(AST.JLS10); + parser.setSource(source); + parser.setResolveBindings(true); + return (CompilationUnit) parser.createAST(null); + } + + @Override + public boolean isActive(IJavaProject project) { + return true; + } + + private CategorizedProblem[] getProblems(CompilationUnit ast, FXOMDocument document) { + FxControllerVisitor visitor = new FxControllerVisitor(); + ast.accept(visitor); + Map<String, String> controllerIds = visitor.getFxIds(); + Map<String, FXOMObject> documentIds = document.collectFxIds(); + List<String> missingIds = new ArrayList<>(); + for (Entry<String, FXOMObject> id : documentIds.entrySet()) { + if (!controllerIds.containsKey(id.getKey())) { + FXOMObject fxomObject = id.getValue(); + if (fxomObject instanceof FXOMInstance) { + FXOMInstance instance = (FXOMInstance) fxomObject; + missingIds.add(id.getKey() + ";" + instance.getDeclaredClass().getName()); + } + } + } + + if (!missingIds.isEmpty()) { + try { + IResource resource = ast.getTypeRoot().getCorrespondingResource(); + return new CategorizedProblem[] {new FxControllerProblem(missingIds, resource)}; + } catch (JavaModelException e) { + e.printStackTrace(); + } + } + return new CategorizedProblem[0]; + } + +} diff --git a/sb4e/src/no/tobask/sb4e/FxControllerVisitor.java b/sb4e/src/no/tobask/sb4e/FxControllerVisitor.java new file mode 100644 index 0000000000000000000000000000000000000000..468295d23cdda04f96aab38b3367d06d1daad7ae --- /dev/null +++ b/sb4e/src/no/tobask/sb4e/FxControllerVisitor.java @@ -0,0 +1,66 @@ +package no.tobask.sb4e; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.core.dom.ASTVisitor; +import org.eclipse.jdt.core.dom.Annotation; +import org.eclipse.jdt.core.dom.FieldDeclaration; +import org.eclipse.jdt.core.dom.IExtendedModifier; +import org.eclipse.jdt.core.dom.ITypeBinding; +import org.eclipse.jdt.core.dom.Modifier; +import org.eclipse.jdt.core.dom.SimpleType; +import org.eclipse.jdt.core.dom.Type; +import org.eclipse.jdt.core.dom.VariableDeclarationFragment; + +import javafx.fxml.FXML; + +public class FxControllerVisitor extends ASTVisitor { + + private Map<String, String> fxIds = new HashMap<>(); // var name -> type + private static final String FXML_ANNOTATION = FXML.class.getSimpleName(); + + @Override + public boolean visit(FieldDeclaration node) { + if (isFxIdCandidate(node)) { + Type type = node.getType(); + ITypeBinding typeBinding = type.resolveBinding(); + String typeName; + if (typeBinding != null) { + typeName = typeBinding.getQualifiedName(); + } else { + if (type.isSimpleType()) { + typeName = ((SimpleType) type).getName().getFullyQualifiedName(); + } else { + typeName = "UNKNOWN"; + } + } + List<VariableDeclarationFragment> fragments = node.fragments(); + String variableName = fragments.get(0).getName().toString(); + fxIds.put(variableName, typeName); + } + return super.visit(node); + } + + + public Map<String, String> getFxIds() { + return fxIds; + } + + private boolean isFxIdCandidate(FieldDeclaration field) { + List<IExtendedModifier> modifiers = field.modifiers(); + if (Modifier.isPublic(field.getModifiers())) { + return true; + } + boolean hasFxmlAnnotation = false; + for (IExtendedModifier modifier : modifiers) { + if (modifier.isAnnotation()) { + String annotationName = ((Annotation) modifier).getTypeName().toString(); + hasFxmlAnnotation = hasFxmlAnnotation || annotationName.equals(FXML_ANNOTATION); + } + } + return hasFxmlAnnotation; + } + +} diff --git a/sb4e/src/no/tobask/sb4e/FxmlDocumentListener.java b/sb4e/src/no/tobask/sb4e/FxmlDocumentListener.java new file mode 100644 index 0000000000000000000000000000000000000000..f86996d60f2331aaf2165bf12db81dcbba02d410 --- /dev/null +++ b/sb4e/src/no/tobask/sb4e/FxmlDocumentListener.java @@ -0,0 +1,158 @@ +package no.tobask.sb4e; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceChangeEvent; +import org.eclipse.core.resources.IResourceChangeListener; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.resources.IWorkspaceRoot; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jdt.core.IJavaElement; +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; + +public class FxmlDocumentListener implements IResourceChangeListener { + + private Map<String, URL> controllers = new HashMap<>(); + private boolean discoveryPerformed = false; + + @Override + public void resourceChanged(IResourceChangeEvent event) { + try { + event.getDelta().accept(this::visit); + } catch (CoreException e) { + e.printStackTrace(); + } + } + + private boolean visit(IResourceDelta delta) { + IResource resource = delta.getResource(); + if (resource != null && resource.getType() == IResource.FILE) { + IFile file = (IFile) resource; + String extension = file.getFileExtension(); + if (extension != null && extension.equals("fxml")) { + try { + updateControllers(file); + } catch (IOException e) { + e.printStackTrace(); + } + return false; + } + } + return true; + } + + private void updateControllers(IFile fxmlFile) throws IOException { + String controller = getController(fxmlFile); + if (controller != null) { + if (fxmlFile.exists()) { + controllers.put(controller, fxmlFile.getLocationURI().toURL()); + } else { + controllers.remove(controller); + } + } + } + + private String getController(IFile fxmlFile) throws IOException { + try (InputStream inputStream = fxmlFile.getLocationURI().toURL().openStream()) { + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + String line; + while ((line = reader.readLine()) != null) { + String[] tokens = line.split(" "); + for (String token : tokens) { + if (token.startsWith("fx:controller=")) { + return token.split("=")[1].replaceAll("[^a-zA-Z0-9\\.]", ""); + } + } + } + } + return null; + } + + public boolean isAssignedController(String controllerName) { + if (!discoveryPerformed) { + discoverControllers(); + discoveryPerformed = true; + } + return controllers.containsKey(controllerName); + } + + public URL getDocument(String controllerName) { + if (!discoveryPerformed) { + discoverControllers(); + discoveryPerformed = true; + } + return controllers.get(controllerName); + } + + private void discoverControllers() { + try { + for (IFile fxmlFile : getAllFxmlFilesInWorkspace()) { + updateControllers(fxmlFile); + } + } catch (JavaModelException | IOException e) { + e.printStackTrace(); + } + } + + private Collection<IFile> getAllFxmlFilesInWorkspace() throws JavaModelException { + Collection<IFile> files = new ArrayList<>(); + IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot(); + for (IProject project : root.getProjects()) { + IJavaProject javaProject = JavaCore.create(project); + files.addAll(getFxmlFiles(javaProject)); + } + return files; + } + + private Collection<IFile> getFxmlFiles(IJavaProject javaProject) throws JavaModelException { + Collection<IFile> files = new ArrayList<>(); + for (IPackageFragmentRoot pkgRoot : javaProject.getPackageFragmentRoots()) { + if (pkgRoot.getKind() == IPackageFragmentRoot.K_SOURCE) { + files.addAll(getFxmlFiles(pkgRoot)); + } + } + return files; + } + + private List<IFile> getFxmlFiles(IPackageFragmentRoot pkgRoot) throws JavaModelException { + List<IFile> files = new ArrayList<>(); + for (IJavaElement child : pkgRoot.getChildren()) { + if (child.getElementType() == IJavaElement.PACKAGE_FRAGMENT) { + files.addAll(getFxmlFiles((IPackageFragment) child)); + } + } + return files; + } + + private List<IFile> getFxmlFiles(IPackageFragment pkg) throws JavaModelException { + List<IFile> files = new ArrayList<>(); + for (Object resource : pkg.getNonJavaResources()) { + if (resource instanceof IFile) { + IFile file = (IFile) resource; + String extension = file.getFileExtension(); + if (extension != null && extension.equals("fxml")) { + files.add(file); + } + } + } + return files; + } + +} diff --git a/sb4e/src/no/tobask/sb4e/MissingFxIdsFixer.java b/sb4e/src/no/tobask/sb4e/MissingFxIdsFixer.java new file mode 100644 index 0000000000000000000000000000000000000000..5a1dacdbc5568426e827bafb3c1e2ea3f48fe1d4 --- /dev/null +++ b/sb4e/src/no/tobask/sb4e/MissingFxIdsFixer.java @@ -0,0 +1,97 @@ +package no.tobask.sb4e; + +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.dom.AST; +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.BodyDeclaration; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.FieldDeclaration; +import org.eclipse.jdt.core.dom.ImportDeclaration; +import org.eclipse.jdt.core.dom.MarkerAnnotation; +import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.eclipse.jdt.core.dom.VariableDeclarationFragment; +import org.eclipse.jdt.core.dom.rewrite.ASTRewrite; +import org.eclipse.jdt.core.dom.rewrite.ListRewrite; +import org.eclipse.jdt.ui.text.java.IInvocationContext; +import org.eclipse.jdt.ui.text.java.IJavaCompletionProposal; +import org.eclipse.jdt.ui.text.java.IProblemLocation; +import org.eclipse.jdt.ui.text.java.IQuickFixProcessor; +import org.eclipse.jdt.ui.text.java.correction.ASTRewriteCorrectionProposal; + +public class MissingFxIdsFixer implements IQuickFixProcessor { + + @Override + public boolean hasCorrections(ICompilationUnit unit, int problemId) { + return problemId == 1234; + } + + @Override + public IJavaCompletionProposal[] getCorrections(IInvocationContext context, IProblemLocation[] locations) + throws CoreException { + CompilationUnit unit = context.getASTRoot(); + AST unitAst = unit.getAST(); + TypeDeclaration type = (TypeDeclaration) unit.types().get(0); + + ASTRewrite rewrite = ASTRewrite.create(unitAst); + ListRewrite bodyDeclarationsRewrite = rewrite.getListRewrite(type, + TypeDeclaration.BODY_DECLARATIONS_PROPERTY); + ListRewrite importsRewrite = rewrite.getListRewrite(unit, CompilationUnit.IMPORTS_PROPERTY); + + List<ImportDeclaration> imports = unit.imports(); + List<String> importNames = imports.stream().map(i -> i.getName().getFullyQualifiedName()) + .collect(Collectors.toList()); + + String[] missingIds = locations[0].getProblemArguments(); + for (String missingId : missingIds) { + String[] parts = missingId.split(";"); + String variableName = parts[0]; + String variableType = parts[1]; + String[] variableTypeParts = variableType.split("\\."); + + VariableDeclarationFragment variableFragment = unitAst.newVariableDeclarationFragment(); + variableFragment.setName(unitAst.newSimpleName(variableName)); + + FieldDeclaration field = unitAst.newFieldDeclaration(variableFragment); + String simpleTypeName = variableTypeParts[variableTypeParts.length-1]; + field.setType(unitAst.newSimpleType(unitAst.newName(simpleTypeName))); + + AST fieldAst = field.getAST(); + MarkerAnnotation annotation = fieldAst.newMarkerAnnotation(); + annotation.setTypeName(unitAst.newSimpleName("FXML")); + field.modifiers().add(annotation); + + ASTNode lastField = getLastField(type); + if (lastField == null) { + bodyDeclarationsRewrite.insertFirst(field, null); + } else { + bodyDeclarationsRewrite.insertAfter(field, lastField, null); + } + + if (!importNames.contains(variableType)) { + ImportDeclaration importDeclaration = unitAst.newImportDeclaration(); + importDeclaration.setName(unitAst.newName(variableType)); + importsRewrite.insertLast(importDeclaration, null); + } + } + + ASTRewriteCorrectionProposal fix = new ASTRewriteCorrectionProposal("Add missing fields", + context.getCompilationUnit(), rewrite, 1); + return new IJavaCompletionProposal[] {fix}; + } + + private ASTNode getLastField(TypeDeclaration type) { + List<BodyDeclaration> bodyDeclarations = type.bodyDeclarations(); + ASTNode lastField = null; + for (BodyDeclaration bodyDeclaration : bodyDeclarations) { + if (bodyDeclaration.getNodeType() == ASTNode.FIELD_DECLARATION) { + lastField = bodyDeclaration; + } + } + return lastField; + } + +}