EL autocomplete / code assist with Eclipse and Spring Beans
Asked Answered
D

2

3

In Eclipse, auto-complete for JSF / EL only works for legacy @ManagedBean or CDI beans (@Named), at least when using the JBoss tools plugin.
See also:   EL proposals / autocomplete / code assist in Facelets with Eclipse   or   Eclipse autocomplete (content assist) with facelets (jsf) and xhtml   or   Content Assist for JSF2 + CDI (weld) beans + Eclipse Helios
=> Summing-up:
- install JBoss tools JSF + CDI (http://download.jboss.org/jbosstools/oxygen/stable/updates/, JBoss Web and Java EE / JBoss Tools JSF + Visual Page Editor + Contexts and Dependency Injection Tools);
- in project properties: remove Project Facets/"JavaServer Faces" so the very slow Eclipse JSF-autocompleter will not be used,   activate CDI / CDI support.

But there is no support when using Spring, i.e. @Controller or @Component.
Typically, you should use CDI beans with full support for all JSF scopes now, but you may have your reasons or an existing project may use Spring.
See also:   Moving JSF Managed Beans to Spring beans   or   https://www.beyondjava.net/blog/integrate-jsf-2-spring-3-nicely/

So, how to support JSF/EL auto-complete for Spring web beans?

Darcie answered 22/2, 2018 at 17:29 Comment(0)
D
3

I digged into the JBoss tools implementation and a small change makes Spring users happy.
:-)
There is a solution based on the JSF tools (first) and an alternative based on the CDI tools (afterwards).

The following is based on jbosstools-4.5.2.Final using the plugin file org.jboss.tools.jsf_3.8.200.v20170908-0911.jar
But the changes should be the same or very similar for other versions (the relevant source files have their last changes back in Dec 2011 or Sept 2012).

The class org.jboss.tools.jsf.jsf2.bean.model.impl.AbstractMemberDefinitionhas to be extended in the methods getManagedBeanAnnotation() and isAnnotationPresent():

If @ManagedBean is not found, then also look for @Controller (which should be used in Spring, so @Service etc. is not offered in JSF). But this may easily be adjusted, see comments in the following source. Additionally, Spring uses the value annotation attribute instead of name - this is solved via a wrapper class.

public boolean isAnnotationPresent(String annotationTypeName) {
    //TW: added Spring annotations
    boolean b = (getAnnotation(annotationTypeName) != null);
    if (!b  &&  JSF2Constants.MANAGED_BEAN_ANNOTATION_TYPE_NAME.equals(annotationTypeName)) {
        b = (getAnnotation("org.springframework.stereotype.Controller") != null);
        /* with support for all Spring annotations:
        b = (getAnnotation("org.springframework.stereotype.Controller") != null
                ||  getAnnotation("org.springframework.stereotype.Service") != null
                ||  getAnnotation("org.springframework.stereotype.Repository") != null
                ||  getAnnotation("org.springframework.stereotype.Component") != null);
        */
    }
    return b;
}


public AnnotationDeclaration getManagedBeanAnnotation() {
    AnnotationDeclaration ad = annotationsByType.get(JSF2Constants.MANAGED_BEAN_ANNOTATION_TYPE_NAME);
    //TW: added Spring annotations
    if (ad != null)  return ad;
    ad = annotationsByType.get("org.springframework.stereotype.Controller");
    /* with support for all Spring annotations:
    if (ad == null)  ad = annotationsByType.get("org.springframework.stereotype.Service");
    if (ad == null)  ad = annotationsByType.get("org.springframework.stereotype.Repository");
    if (ad == null)  ad = annotationsByType.get("org.springframework.stereotype.Component");
    */
    if (ad != null) {
        // create wrapper to map "value" (used by Spring) to "name" (which is used by @ManageBean)
        ad = new AnnotationDeclaration() {
                private AnnotationDeclaration wrapped;

                AnnotationDeclaration init(AnnotationDeclaration wrappedAD) {
                    this.wrapped = wrappedAD;
                    return this;
                }

                @Override
                public Object getMemberValue(String name) {
                    Object val = wrapped.getMemberValue(name);
                    if (val == null  &&  "name".equals(name)) {
                        val = wrapped.getMemberValue(null);
                    }
                    return val;
                }

                @Override
                public Object getMemberValue(String name, boolean resolve) {
                    Object result = null;
                    if (resolve) {
                        result = this.getMemberConstantValue(name);
                    }
                    if (result == null) {
                        result = this.getMemberValue(name);
                    }
                    return result;
                }

                @Override
                public void setDeclaration(IJavaAnnotation annotation) {
                    wrapped.setDeclaration(annotation);
                }

                @Override
                public IJavaAnnotation getDeclaration() {
                    return wrapped.getDeclaration();
                }

                @Override
                public IResource getResource() {
                    return wrapped.getResource();
                }

                @Override
                public IMemberValuePair[] getMemberValuePairs() {
                    return wrapped.getMemberValuePairs();
                }

                @Override
                public Object getMemberConstantValue(String name) {
                    return wrapped.getMemberConstantValue(name);
                }

                @Override
                public Object getMemberDefaultValue(String name) {
                    return wrapped.getMemberDefaultValue(name);
                }

                @Override
                public IMember getParentMember() {
                    return wrapped.getParentMember();
                }

                @Override
                public String getTypeName() {
                    return wrapped.getTypeName();
                }

                @Override
                public IType getType() {
                    return wrapped.getType();
                }

                @Override
                public int getLength() {
                    return wrapped.getLength();
                }

                @Override
                public int getStartPosition() {
                    return wrapped.getStartPosition();
                }

                @Override
                public IAnnotationType getAnnotation() {
                    return wrapped.getAnnotation();
                }

                @Override
                public IAnnotation getJavaAnnotation() {
                    return wrapped.getJavaAnnotation();
                }

                @Override
                public IMember getSourceMember() {
                    return wrapped.getSourceMember();
                }

                @Override
                public IJavaElement getSourceElement() {
                    return wrapped.getSourceElement();
                }
            }.init(ad); // class
    }
    return ad;
}

I offer the two compiled classes (main + one inner class) here for direct download:
AbstractMemberDefinition.class + AbstractMemberDefinition$1.class
I promise a trustworthy compile with just above changes (i.e. without any malicious code or similar, you may check via a decompile with CFR, Procyon, aged JAD or Eclipse-ECD) - you may use them directly or perform the compile by yourself (BTW: Does stack overflow offer file attachments?)

Installation:

  • Exit Eclipse.
  • Make a backup copy of the original file
    eclipse_home\plugins\org.jboss.tools.jsf_3.8.200.v20170908-0911.jar
    (e.g. as *.jar_orig).
  • Copy the provided classes into org.jboss.tools.jsf_3.8.200.v20170908-0911.jar\org\jboss\tools\jsf\jsf2\bean\model\impl (e.g. via Total Commander or another tool supporting zip/jar handling; you may even use JDKs jar tool). Note: the A...$1.class is a new file.
  • Start Eclipse again and enjoy!

Go to a JSF page and Type Ctrl+Space after #{ to get a list of beans. Member auto-completion works, too (after #{beanName.), even recursive.
Even Ctrl+click or F3 on the bean name works!
Note: the first auto-completion call needs some seconds for the initial bean disovery.

BTW: For this, there is no need to activate CDI support for the project! (Build is quicker then because no CDI Builder is active.)


Alternatively, you may extend the JBoss tools CDI feature to discover Spring beans. It works the same and additionally they will be listed with Ctrl+Alt+Z (toolbar button Open CDI Named Bean).
Note: I did not check if there are any side effects if the non-CDI Spring beans are discovered as CDI beans!

For this, the file org.jboss.tools.cdi.internal.core.impl.definition.AbstractMemberDefinition has to be extended in the method getNamedAnnotation():

public AnnotationDeclaration getNamedAnnotation() {
    AnnotationDeclaration ad = getAnnotation(CDIConstants.NAMED_QUALIFIER_TYPE_NAME);
    //TW: added Spring annotations
    if (ad != null)  return ad;
    ad = getAnnotation("org.springframework.stereotype.Controller");
    /* add additional Spring annotations, if desired:
    if (ad != null)  return ad;
    ad = getAnnotation("org.springframework.stereotype.Service");
    if (ad != null)  return ad;
    ad = getAnnotation("org.springframework.stereotype.Repository");
    if (ad != null)  return ad;
    ad = getAnnotation("org.springframework.stereotype.Component");
    */
    return ad;
}

You have to copy the compiled class (download: CDI-AbstractMemberDefinition.class) into plugins\org.jboss.tools.cdi.core_1.8.201.v20171221-1913.jar\org\jboss\tools\cdi\internal\core\impl\definition

CDI support has to be active for the project.


Maybe someone working for the JBoss tools project may include this in the offical plugin.
Best would be to offer a preferences String, that allows to add arbitrary annotations - maybe even a project specific setting. This would then be a generic solution and no "offical Spring support" which might have political acceptance issues.
See https://issues.jboss.org/browse/JBIDE-25748

Darcie answered 22/2, 2018 at 17:29 Comment(3)
The exact same files still work for JBoss Tools 4.23 (for Eclipse 2022-03) but you have to change them inside org.jboss.tools.jsf_3.9.0.v20210224-1443.jarDarcie
I created a pull request (github.com/jbosstools/jbosstools-javaee/pull/489) so this hopefully finds a way into the official release soon. Another pull request (github.com/jbosstools/jbosstools-base/pull/734) adds support for EL 3 syntax, especially +=Darcie
I created patched jars for v4.23 for JSF: el.core.jar and also EL 3.0 syntax support (see also issues.redhat.com/browse/JBIDE-27256): tools.jsf.jar Simply replace the existing jars inside eclipse/plugins.Darcie
H
0

I tried first solution explained by @Thies and it still works for org.jboss.tools.jsf_3.8.500.v20200930-0907.jar. Actually this is the only way I found to reach my Spring Beans (Having @Component etc...) from my xhtml files. I wish that he provided the .class files with support for all Spring annotations instead of just @Controller, but then I realized that I can do it for everybody. So I downloaded source code and recompiled those two classes to have @Component support and other Spring annotations. I also wanted to add detailed steps that are required to recompile the files, for the sake of inexperienced coders like me. :)

I tried everything on a clean installation of "Eclipse JEE 2020-09 R Win32 x86_64" and an empty Maven repository. UPPER CASE INFORMATIONS below are things that I have done wrong at the first time and then corrected, so I hope you do not repeat same time consuming mistakes. :)

Before starting the first solution explained by @Thies:

  1. ADD ONLY 'JBoss Tools JSF' to the Eclipse. THIS STEP MUST BE THE FIRST STEP, because the compilation procedure (next steps) requires some common libraries installed on this step. You don't need to install any version of STS (Spring Tools Suite). DO NOT USE "Eclipse Marketplace", it will just confuse you.
    • Use 'Help' > 'Install New Software...' from the Eclipse main menu.
    • Add 'JBoss Tools' repository clicking on 'Add...' button. I named it 'JBoss Tools' and the location URL was http://download.jboss.org/jbosstools/photon/stable/updates/ for me. I found that URL after I clicked on 'Download' button at https://tools.jboss.org/downloads/jbosstools/, under 'Update Site' tab.
    • After the repository is selected on the drop-box, FIND AND CHECK ONLY THE 'JBoss Tools JSF' BOX (it is under 'JBoss WEB and Java EE Development' tree) and follow the instructions. Its dependencies will automatically be installed by Eclipse if necessary, so you don't need to check any other check-boxes, only 1 checked.
    • Eclipse will ask for permission to install 'Jboss Tools JSF' because Eclipse doesn't trust 'JBoss Tools' repository. Install anyway and restart the eclipse as requested at the end of the installation.
  2. Download the latest , 'javaee' project source code from GitHub here. Download it as a zip archive (we will not use all of them), extract 'jbosstools-javaee-master' folder to your eclipse workspace BUT DO NOT IMPORT ALL OF THEM. It is not necessary and it takes a little long. Continue reading:
    • On Eclipse select 'File' > 'Import...'
    • On 'Import' window, BROWSE AND SELECT ONLY <your-eclipse-workspace>\jbosstools-javaee-master\jsf\plugins\org.jboss.tools.jsf\ folder. Then hit the 'Finish'" button. Wait for Eclipse to do its job, DO NOT TRY TO RUSH THINGS, just wait. :) It will download maven dependencies total of 20-25 MB.
    • Install anything if it asks for them (E.g. "tycho" plugins). Follow as Eclipse instructs. And install anyway even if Eclipse does not trust them. Restart Eclipse if it asks for it.
    • Wait if there are any ongoing action that are started automatically by Eclipse. Check the 'Progress' view of Eclipse to see if there are any ongoing actions.
  3. Right click on the project ('org.jboss.tools.jsf' project) on 'Project Explorer' view of Eclipse. Then select 'Maven' -> 'Update Project' just to be sure everything is all right.
  4. On Eclipse main menu find 'Project' -> 'Clean' to Open 'Clean' window. Ensure ONLY "org.jboss.tools.jsf" project is selected and 'Build only the selected projects' selection is active to save some time (assuming you have other projects in your workspace which aren't relevant to us). Then hit the 'Clean' button and wait for eclipse to clean an rebuild the project.
  5. Find <your-eclipse-workspace>\jbosstools-javaee-master\jsf\plugins\org.jboss.tools.jsf\src\org\jboss\tools\jsf\jsf2\bean\model\impl\AbstractMemberDefinition.java file (find it on Eclipse project) and perform the changes proposed by @Thies as first solution on previous post. But this time make the changes with support for all Spring annotations. :) See his commented out code and update as necessary. It might be something like this:
  • Add the imports to the end of the other imports at top:
import org.eclipse.jdt.core.IMember;
import org.eclipse.jdt.core.IMemberValuePair;
import org.jboss.tools.common.java.IAnnotationType;
  • Replace the below functions:
public boolean isAnnotationPresent(String annotationTypeName) {
    //TW: added Spring annotations
    boolean b = (getAnnotation(annotationTypeName) != null);
    if (!b  &&  JSF2Constants.MANAGED_BEAN_ANNOTATION_TYPE_NAME.equals(annotationTypeName)) {
        b = (getAnnotation("org.springframework.stereotype.Controller") != null
                ||  getAnnotation("org.springframework.stereotype.Service") != null
                ||  getAnnotation("org.springframework.stereotype.Repository") != null
                ||  getAnnotation("org.springframework.stereotype.Component") != null);
    }
    return b;
}


public AnnotationDeclaration getManagedBeanAnnotation() {
    AnnotationDeclaration ad = annotationsByType.get(JSF2Constants.MANAGED_BEAN_ANNOTATION_TYPE_NAME);
    //TW: added Spring annotations
    if (ad != null)  return ad;
    ad = annotationsByType.get("org.springframework.stereotype.Controller");
    if (ad == null)  ad = annotationsByType.get("org.springframework.stereotype.Component");
    if (ad == null)  ad = annotationsByType.get("org.springframework.stereotype.Service");
    if (ad == null)  ad = annotationsByType.get("org.springframework.stereotype.Repository");
    if (ad != null) {
        // create wrapper to map "value" (used by Spring) to "name" (which is used by @ManageBean)
        ad = new AnnotationDeclaration() {
                private AnnotationDeclaration wrapped;

                AnnotationDeclaration init(AnnotationDeclaration wrappedAD) {
                    this.wrapped = wrappedAD;
                    return this;
                }

                @Override
                public Object getMemberValue(String name) {
                    Object val = wrapped.getMemberValue(name);
                    if (val == null  &&  "name".equals(name)) {
                        val = wrapped.getMemberValue(null);
                    }
                    return val;
                }

                @Override
                public Object getMemberValue(String name, boolean resolve) {
                    Object result = null;
                    if (resolve) {
                        result = this.getMemberConstantValue(name);
                    }
                    if (result == null) {
                        result = this.getMemberValue(name);
                    }
                    return result;
                }

                @Override
                public void setDeclaration(IJavaAnnotation annotation) {
                    wrapped.setDeclaration(annotation);
                }

                @Override
                public IJavaAnnotation getDeclaration() {
                    return wrapped.getDeclaration();
                }

                @Override
                public IResource getResource() {
                    return wrapped.getResource();
                }

                @Override
                public IMemberValuePair[] getMemberValuePairs() {
                    return wrapped.getMemberValuePairs();
                }

                @Override
                public Object getMemberConstantValue(String name) {
                    return wrapped.getMemberConstantValue(name);
                }

                @Override
                public Object getMemberDefaultValue(String name) {
                    return wrapped.getMemberDefaultValue(name);
                }

                @Override
                public IMember getParentMember() {
                    return wrapped.getParentMember();
                }

                @Override
                public String getTypeName() {
                    return wrapped.getTypeName();
                }

                @Override
                public IType getType() {
                    return wrapped.getType();
                }

                @Override
                public int getLength() {
                    return wrapped.getLength();
                }

                @Override
                public int getStartPosition() {
                    return wrapped.getStartPosition();
                }

                @Override
                public IAnnotationType getAnnotation() {
                    return wrapped.getAnnotation();
                }

                @Override
                public IAnnotation getJavaAnnotation() {
                    return wrapped.getJavaAnnotation();
                }

                @Override
                public IMember getSourceMember() {
                    return wrapped.getSourceMember();
                }

                @Override
                public IJavaElement getSourceElement() {
                    return wrapped.getSourceElement();
                }
            }.init(ad); // class
    }
    return ad;
}
  1. Repeat Step-4. (On Eclipse main menu perform 'Project' > 'Clean' and wait for Eclipse to rebuild)
  2. Find AbstractMemberDefinition.class and AbstractMemberDefinition$1.class files on <your-eclipse-workspace>\jbosstools-javaee-master\jsf\plugins\org.jboss.tools.jsf\target\classes\org\jboss\tools\jsf\jsf2\bean\model\impl folder.
  3. Exit Eclipse and perform "Installation" steps from first solution of @Thies on previous post. I used "7-Zip" to open and update all ZIP and JAR archives, because it is free and easy to use.

Here are the AbstractMemberDefinition.class and AbstractMemberDefinition$1.class files I compiled to support 4 spring annotations; @Component, @Service, @Repository and @Controller. I hope they save some time for somebody one day.

I hope somebody will publish the result of https://issues.jboss.org/browse/JBIDE-25748 in the future and we will just laugh remembering these days and all the coding we have done because of our laziness. :)

Hepsibah answered 7/10, 2020 at 20:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.