Posts tagged: JDT

Application Coder

Post to Twitter

This article is about our new Research project: Application Coder.

The application is a very simple Java Code Editor. It shows a tree with Java files and has a Code Editor for modifying files. The code editor is the popular Ace Editor and we use Eclipse JDT for java compilation. The application was written as standard JVx application with vaadin UI.

We made tests as single-page vaadin application, embedded with iframes and embedded with div areas. All versions work without problems but the last one is preferred. If you embedd a vaadin application with divs, it's a little bit tricky because you have to configure the client-side on your own, but you get full access to the whole html page. This wouldn't be possible if you use an iframe because you can't access the main html page.

Some cool features of our editor are: error annotations and error markers. And the best feature is our LIVE Preview of code changes!

Annotations and marker

Annotations and marker


Watch this video:

Application coder


The application coder is not only a Java code editor, it was designed especially for JVx applications, because it groups client code and business logic. The preview also starts a JVx application and LIVE preview reloads the application.

Our coder application has a push mechanism and reloads every preview window automatically and immediate after compilation.

Above video shows multiple instances of our coder application, embedded in divs and it's possible to drag around and resize the applications (thanks to jquery-ui).

The application is a perfect showcase of JVx because it's not a database application as many other JVx applications. We've used vaadin, jquery-ui and vaadin-addons to create a great UX. JVx is technology independent and open for other technologies.

Source Code modification with Eclipse AST

Post to Twitter

We heavily use Eclipse AST and JDT. The problem with both APIs is that you don't find a lot of documentation or examples. The best documentation was and is the source code. It shows all secrets.

Last friday at Eclipse DemoCamp Vienna, I discussed with CodeCop about AST and JDT and the missing documentation :)

I wrote a simple class for class/superclass/package/javadoc manipulation some months ago. It is included in VisionX and is not free, but no matter. Here is the source code:

/*
(c) SIB Visions 2012

http://www.sibvisions.com
*/
public class SimpleSourceFile {
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // Class members
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    /** the source parser. */
    private ASTParser parser;

    /** the parsed tokens of the source file. */
    private CompilationUnit cuSource;

    /** the original document. */
    private Document docOrig;

    /** the class type info. */
    private TypeDeclaration tdClass;

    /** the javadoc of the class. */
    private JavadocInfo jdoc;

    /** the full qualified name of the class. */
    private String sClassName;

    /** the full qualified name of the super class after loading. */
    private String sLoadedSuperClass;

    /** the full qualified name of the parent class. */
    private String sSuperClass;

    /** whether the source is changed. */
    private boolean bChanged = false;

    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // Initialization
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    /**
     * Creates a new instance of <code>SimpleSourceFile</code> without source
     * information.
     */

    public SimpleSourceFile() {
        load("");
    }

    /**
     * Creates a new instance of <code>SimpleSourceFile</code> with a given
     * source file.
     *  
     * @param pFile the java source file
     * @throws IOException if an error occurs during reading the file
     */

    public SimpleSourceFile(File pFile) throws IOException {
        if (pFile.exists()) {
            JavaSource source = JavaSource.get(pFile.getAbsolutePath());

            if (source != null) {
                synchronized(source) {
                    load(new String(FileUtil.getContent(pFile)));
                }
            } else {
                load(new String(FileUtil.getContent(pFile)));
            }
        } else {
            load("");
        }
    }

    /**
     * Creates a new instance of <code>SimpleSourceFile</code> with a given
     * source stream. The stream is closed after reading
     *  
     * @param pStream the java source stream
     * @throws IOException if an error occurs during reading the stream
     */

    public SimpleSourceFile(InputStream pStream) throws IOException {
        this(pStream, true);
    }

    /**
     * Creates a new instance of <code>SimpleSourceFile</code> with a given
     * source stream.
     *  
     * @param pStream the java source stream
     * @param pAutoClose whether the stream should be closed after reading
     * @throws IOException if an error occurs during reading the stream
     */

    public SimpleSourceFile(InputStream pStream, boolean pAutoClose) throws IOException {
        load(new String(FileUtil.getContent(pStream, pAutoClose)));
    }

    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    // User-defined methods
    //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    /**
     * Loads the properties from the source file.
     *
     * @param pSource the java source
     */
@SuppressWarnings({
        "unchecked", "rawtypes"
    })
    private void load(String pSource) {
        jdoc = new JavadocInfo();

        if (parser == null) {
            parser = ASTUtil.createParser();
        }

        parser.setSource(pSource.toCharArray());

        cuSource = (CompilationUnit) parser.createAST(null);

        if (docOrig != null) {
            docOrig.set(pSource);
        } else {
            docOrig = new Document(pSource);
        }

        //we support only one class per file
        List types = cuSource.types();

        if (types.size() > 0 && types.get(0) instanceof TypeDeclaration) {
            tdClass = (TypeDeclaration) types.get(0);

            String sPackage;

            //we have to use the current package, if set
            if (cuSource.getPackage() != null) {
                sPackage = cuSource.getPackage().getName().getFullyQualifiedName() + ".";
            } else {
                sPackage = "";
            }

            sClassName = sPackage + tdClass.getName().getFullyQualifiedName();

            if (tdClass.getSuperclassType() != null) {
                String sSuperName = ((SimpleType)(tdClass.getSuperclassType())).getName().getFullyQualifiedName();

                if (sSuperName.indexOf('.') > 0) {
                    sSuperClass = sSuperName;
                } else {
                    //try to detect the full qualified name of the super class through the imports

                    List < ImportDeclaration > liImports = cuSource.imports();

                    boolean bFound = false;

                    if (liImports != null) {
                        String sTempName = "." + sSuperName;
                        String sImport;

                        for (int i = 0, anz = liImports.size(); !bFound && i < anz; i++) {
                            sImport = liImports.get(i).getName().getFullyQualifiedName();
                            if (sImport.endsWith(sTempName)) {
                                bFound = true;

                                sSuperClass = liImports.get(i).getName().getFullyQualifiedName();
                            }
                        }
                    }

                    if (!bFound) {
                        //we use the current package
                       sSuperClass = sPackage + sSuperName;
                    }
                }

                //cache for optimizing imports in apply
                sLoadedSuperClass = sSuperClass;
            }

            jdoc = new JavadocInfo(tdClass.getJavadoc(), pSource);
        } else {
            tdClass = null;
        }

        //currently loaded file can not be changed!
        bChanged = false;
    }

    /**
     * Sets the comment for the class.
     *
     * @param pText the comment
     */

    public void setClassComment(String pText) {
        bChanged |= !CommonUtil.equals(pText, jdoc.getDescription());

        jdoc.setDescription(pText);
    }

    /**
     * Gets the comment for the class.
     *
     * @return the comment
     */

    public String getClassComment() {
        return jdoc.getDescription();
    }

    /**
     * Sets the full qualified name of the class.
     *
     * @param pName the class name
     */

    public void setClassName(String pName) {
        if (pName == null) {
            throw new IllegalArgumentException("It is not allowed to remove the class definition (class name = null)!");
        }

        bChanged |= !CommonUtil.equals(pName, sClassName);

        sClassName = pName;
    }

    /**
     * Gets the full qualified name of the class.
     *
     * @return the class name
     */

    public String getClassName() {
        return sClassName;
    }

    /**
     * Sets the full qualified class name of the parent class.
     *
     * @param pSuperClass the full qualified class name of the parent class
     */

    public void setSuperClass(String pSuperClass) {
        bChanged |= !CommonUtil.equals(pSuperClass, sSuperClass);

        sSuperClass = pSuperClass;
    }

    /**
     * Gets the full qualified class name of the parent class.
     *
     * @return the class name
     */

    public String getSuperClass() {
        return sSuperClass;
    }

    /**
     * Replaces the relevant information with the specific values
     * and saves the source file.
     *
     * @throws Exception if the source modification fails
     */
@SuppressWarnings("unchecked")
    protected void apply() throws Exception {
        if (!bChanged) {
            return;
        }

        AST ast = cuSource.getAST();

        ASTRewrite arw = ASTRewrite.create(ast);

        //----------------------------------------------------
        // Change Package, Classname and Constructor
        //----------------------------------------------------

        String sCurrentPackage;

        //set package
        String[] sClassInfo = ClassUtil.splitName(sClassName);

        if (sClassInfo[0] != null) {
            if (cuSource.getPackage() != null) {
                arw.set(cuSource.getPackage(), PackageDeclaration.NAME_PROPERTY, ast.newName(sClassInfo[0]), null);

                sCurrentPackage = sClassInfo[0];
            } else {
                PackageDeclaration pdcl = ast.newPackageDeclaration();
                pdcl.setName(ast.newName(sClassInfo[0]));

                arw.set(cuSource, CompilationUnit.PACKAGE_PROPERTY, pdcl, null);

                sCurrentPackage = sClassInfo[0];
            }
        } else {
            arw.set(cuSource, CompilationUnit.PACKAGE_PROPERTY, null, null);

            sCurrentPackage = null;
        }

        if (tdClass == null) {
            //new class
            tdClass = ast.newTypeDeclaration();
            tdClass.setName(ast.newSimpleName(sClassInfo[1]));

            ListRewrite lrwImp = arw.getListRewrite(cuSource, CompilationUnit.TYPES_PROPERTY);
            lrwImp.insertFirst(tdClass, null);
        } else {
            //change class name
            SimpleName snClass = ast.newSimpleName(sClassInfo[1]);

            arw.set(tdClass, TypeDeclaration.NAME_PROPERTY, snClass, null);

            //chage constructors

            MethodDeclaration[] methods = tdClass.getMethods();

            if (methods != null) {
                for (MethodDeclaration method: methods) {
                    if (method.isConstructor()) {
                        arw.set(method, MethodDeclaration.NAME_PROPERTY, snClass, null);
                    }
                }
            }
        }

        //change Superclass

        List < ImportDeclaration > liImports = cuSource.imports();

        //remove "old" import
       if (sLoadedSuperClass != null) {
            if (liImports != null) {
                boolean bFound = false;

                for (int i = liImports.size() - 1; !bFound && i >= 0; i--) {
                    if (sLoadedSuperClass.equals(liImports.get(i).getName().getFullyQualifiedName())) {
                        bFound = true;

                        arw.remove(liImports.get(i), null);

                        liImports.remove(i);
                    }
                }
            }
        }

        if (sSuperClass == null) {
            arw.set(tdClass, TypeDeclaration.SUPERCLASS_TYPE_PROPERTY, null, null);
        } else {
            //check new super class import
            String[] sSuperClassInfo = ClassUtil.splitName(sSuperClass);

            if (sSuperClassInfo[0] != null) {
                boolean bFound = false;

                if (sCurrentPackage != null) {
                    bFound = sSuperClassInfo[0].equals(sCurrentPackage);
                }

                if (!bFound) {
                    if (liImports != null) {
                        for (int i = 0, anz = liImports.size(); !bFound && i < anz; i++) {
                            bFound = sSuperClass.equals(liImports.get(i).getName().getFullyQualifiedName());
                        }
                    }
                }

                //class not already importet -> add import
               if (!bFound) {
                    ImportDeclaration idSuper = ast.newImportDeclaration();
                    idSuper.setName(ast.newName(sSuperClass));

                    ListRewrite lrwImp = arw.getListRewrite(cuSource, CompilationUnit.IMPORTS_PROPERTY);
                    lrwImp.insertLast(idSuper, null);
                }
            }

            arw.set(tdClass, TypeDeclaration.SUPERCLASS_TYPE_PROPERTY, ast.newSimpleType(ast.newName(sSuperClassInfo[1])), null);
        }

        //----------------------------------------------------
        // Change Javadoc of the class
        //----------------------------------------------------

        if ("".equals(jdoc.toString())) {
            //no javadoc
            arw.set(tdClass, TypeDeclaration.JAVADOC_PROPERTY, null, null);
        } else {
            arw.set(tdClass, TypeDeclaration.JAVADOC_PROPERTY, jdoc.createJavadoc(tdClass.getAST()), null);
        }

        //----------------------------------------------------
        // Modify AST
        //----------------------------------------------------

        TextEdit edit = arw.rewriteAST(docOrig, null);
        edit.apply(docOrig);

        ASTUtil.applyFormat(docOrig);

        //simple reparse (important for the comments)
        parser.setSource(docOrig.get().toCharArray());

        cuSource = (CompilationUnit) parser.createAST(null);

        //----------------------------------------------------
        // Modify comments
        //----------------------------------------------------

        //Class comment after the last bracket

        int iCorr = 0;

        List < Comment > liComments = cuSource.getCommentList();

        if (liComments != null) {
            String sOldName = tdClass.getName().getFullyQualifiedName();
            String sText;

            for (Comment com: liComments) {
                sText = docOrig.get(com.getStartPosition() - iCorr, com.getLength());

                int iPos = 0;
                int iClassLen = sClassInfo[1].length();
                int iLenDiff = sOldName.length() - iClassLen;

                int iNextPos;

                CharacterType ctypePrev;
                CharacterType ctypeNext;

                while ((iPos = sText.indexOf(sOldName, iPos)) >= 0) {
                    if (iPos > 0) {
                        ctypePrev = StringUtil.getCharacterType("" + sText.charAt(iPos - 1));
                    } else {
                        ctypePrev = CharacterType.OnlySpecial;
                    }

                    iNextPos = iPos + sOldName.length();

                    if (iNextPos <= sText.length() - 1) {
                        ctypeNext = StringUtil.getCharacterType("" + sText.charAt(iNextPos));
                    } else {
                        ctypeNext = CharacterType.OnlySpecial;
                    }

                    //don't replace word parts e.g. ContactsWorkScreen should not be replaced with Contacts
                    if ((ctypePrev == CharacterType.OnlySpecial || ctypePrev == CharacterType.OnlyWhitespace) && (ctypeNext == CharacterType.OnlySpecial || ctypeNext == CharacterType.OnlyWhitespace)) {
                        docOrig.replace(com.getStartPosition() + iPos - iCorr, sOldName.length(), sClassInfo[1]);

                        iCorr += iLenDiff;
                        iPos += iClassLen;
                    } else {
                        iPos = iNextPos;
                    }
                }
            }
        }

        //----------------------------------------------------
        // Update
        //----------------------------------------------------

        load(docOrig.get());

        bChanged = false;
    }

    /**
     * Stores the changed source into the given file.
     *
     * @param pFile the target file for the source
     * @throws Exception if an error occurs during saving
     */

    public void saveAs(File pFile) throws Exception {
        apply();

        JavaSource source = JavaSource.get(pFile.getAbsolutePath());

        if (source != null) {
            synchronized(source) {
                FileUtil.save(pFile, docOrig.get().getBytes());

                source.reload();
            }
        } else {
            FileUtil.save(pFile, docOrig.get().getBytes());
        }
    }

    /**
     * Stores the changed source into the given stream.
     *
     * @param pStream the target stream for the source
     * @throws Exception if an error occurs during saving
     */

    public void saveAs(OutputStream pStream) throws Exception {
        apply();

        pStream.write(docOrig.get().getBytes());
        pStream.flush();
    }

    /**
     * Returns the modified source code.
     *  
     * @return the modified source code
     * @throws Exception if the source modification fails
     */

    protected String getSource() throws Exception {
        apply();

        return docOrig.get();
    }

} // SimpleSourceFile

Important imports:

import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.Comment;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.ImportDeclaration;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.PackageDeclaration;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.SimpleType;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ListRewrite;
import org.eclipse.jface.text.Document;
import org.eclipse.text.edits.TextEdit;

The class contains references to other classes like CommonUtil, FileUtil, StringUtil. These classes are included in JVx. There are two classes (JavaSource, ClassUtil) that are not free, but you should find out how it works without these classes.

If you'll set a new superclass, the superclass and its import, if available, will be updated. If you'll set a new classname, all constructors will be updated.

And once again: The best documentation is the source code :)