001/**
002 * Copyright 2005-2016 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.devtools.jpa.eclipselink.conv.parser.helper;
017
018import japa.parser.ast.CompilationUnit;
019import japa.parser.ast.ImportDeclaration;
020import japa.parser.ast.Node;
021import japa.parser.ast.body.BodyDeclaration;
022import japa.parser.ast.body.ClassOrInterfaceDeclaration;
023import japa.parser.ast.body.FieldDeclaration;
024import japa.parser.ast.body.TypeDeclaration;
025import japa.parser.ast.body.VariableDeclarator;
026import japa.parser.ast.expr.AnnotationExpr;
027import org.apache.commons.lang.ClassUtils;
028import org.apache.commons.logging.Log;
029import org.apache.commons.logging.LogFactory;
030
031import java.util.ArrayList;
032import java.util.Collection;
033import java.util.List;
034
035/**
036 * This is a sort of visitor "helper" class that looks to see if a particular annotation is on a class and if not
037 * places it on the class.
038 */
039public class AnnotationHelper extends VoidVisitorHelperBase<String> {
040
041    private static final Log LOG = LogFactory.getLog(AnnotationHelper.class);
042
043    private final Collection<AnnotationResolver> resolvers;
044    private final boolean removeExisting;
045
046    public AnnotationHelper(Collection<AnnotationResolver> resolvers, boolean removeExisting) {
047        this.resolvers = resolvers;
048        this.removeExisting = removeExisting;
049    }
050
051    @Override
052    public void visitPre(final ClassOrInterfaceDeclaration n, final String mappedClass) {
053        addAnnotation(n, mappedClass, Level.CLASS);
054    }
055
056    @Override
057    public void visitPre(final FieldDeclaration n, final String mappedClass) {
058        addAnnotation(n, mappedClass, Level.FIELD);
059    }
060
061    /** walks up the tree until reaching the CompilationUnit. */
062    private CompilationUnit getCompilationUnit(Node n) {
063        Node unit = n;
064        while (!(unit instanceof CompilationUnit) && unit != null) {
065            unit = unit.getParentNode();
066        }
067        return (CompilationUnit) unit;
068    }
069
070    private void addAnnotation(final BodyDeclaration n, final String mappedClass, Level level) {
071        for (AnnotationResolver resolver : resolvers) {
072            if (resolver.getLevel() == level) {
073                LOG.debug("Evaluating resolver " + ClassUtils.getShortClassName(resolver.getClass()) + " for " + getTypeOrFieldNameForMsg(n) + ".");
074
075                final String fullyQualifiedName = resolver.getFullyQualifiedName();
076
077                //1 figure out if annotation is already imported either via star import or single import.
078                final CompilationUnit unit = getCompilationUnit(n);
079                final List<ImportDeclaration> imports = unit.getImports() != null ? unit.getImports() : new ArrayList<ImportDeclaration>();
080                final boolean foundAnnImport = imported(imports, fullyQualifiedName);
081
082                //2 check if annotation already exists...
083                final AnnotationExpr existingAnnotation = findAnnotation(n, fullyQualifiedName, foundAnnImport);
084
085                //3 if removeExisting is set and the annotation exists, then remove the annotation prior to calling the resolver
086                //Note: cannot remove the import without much more complex logic because the annotation may exist on other nodes in the CompilationUnit
087                //Could traverse the entire CompilationUnit searching for the annotation if we wanted to determine whether we can safely remove an import
088                if (removeExisting && existingAnnotation != null) {
089                    LOG.info("removing existing " + existingAnnotation + " from " + getTypeOrFieldNameForMsg(n) + ".");
090                    final List<AnnotationExpr> annotations = n.getAnnotations() != null ? n.getAnnotations() : new ArrayList<AnnotationExpr>();
091                    annotations.remove(existingAnnotation);
092                    n.setAnnotations(annotations);
093                }
094
095                //4 add annotation if it doesn't already exist or if replaceExisting is set
096                // and the annotation resolves (meaning the resolver determines if should be added by returning a non-null value)
097                if (existingAnnotation == null || (existingAnnotation != null && removeExisting)) {
098                    NodeData nodes = resolver.resolve(n, mappedClass);
099                    if (nodes != null && nodes.annotation != null) {
100                        LOG.info("adding " + nodes.annotation + " to " + getTypeOrFieldNameForMsg(n) + ".");
101                        final List<AnnotationExpr> annotations = n.getAnnotations() != null ? n.getAnnotations() : new ArrayList<AnnotationExpr>();
102                        annotations.add(nodes.annotation);
103                        n.setAnnotations(annotations);
104
105                        //5 add import for annotation
106                        if (!foundAnnImport) {
107                            LOG.info("adding import " + fullyQualifiedName + " to " + getTypeNameForMsg(n) + ".");
108                            imports.add(nodes.annotationImport);
109                        }
110
111                        //6 add additional imports if they are needed
112                        if (nodes.additionalImports != null) {
113                            for (ImportDeclaration aImport : nodes.additionalImports) {
114                                if (aImport.isStatic() || aImport.isAsterisk()) {
115                                    throw new IllegalStateException("The additional imports should not be static or star imports");
116                                }
117                                final boolean imported = imported(imports, aImport.getName().toString());
118                                if (!imported) {
119                                    LOG.info("adding import " + aImport.getName().toString() + " to " + getTypeNameForMsg(n) + ".");
120                                    imports.add(aImport);
121                                }
122                            }
123                        }
124
125                        unit.setImports(imports);
126
127                        if (nodes.nestedDeclaration != null) {
128                            final TypeDeclaration parent = unit.getTypes().get(0);
129
130                            final List<BodyDeclaration> members = parent.getMembers() != null ? parent.getMembers() : new ArrayList<BodyDeclaration>();
131                            final TypeDeclaration existingNestedDeclaration = findTypeDeclaration(members, nodes.nestedDeclaration.getName());
132
133                            //7 if removeExisting is set and the nested declaration exists, then remove the nested declaration
134                            if (removeExisting) {
135                                if (existingNestedDeclaration != null) {
136                                    LOG.info("removing existing nested declaration " + existingNestedDeclaration.getName() + " from " + getTypeOrFieldNameForMsg(n) + ".");
137                                    members.remove(existingNestedDeclaration);
138                                }
139                            }
140
141                            //8 add nested class
142                            if (existingNestedDeclaration == null || (existingNestedDeclaration != null && removeExisting)) {
143                                nodes.nestedDeclaration.setParentNode(parent);
144                                LOG.info("adding nested declaration " + nodes.nestedDeclaration.getName() + " to " + getTypeOrFieldNameForMsg(n) + ".");
145                                members.add(nodes.nestedDeclaration);
146                            }
147                            parent.setMembers(members);
148                        }
149                    }
150                }
151            }
152        }
153    }
154
155    private AnnotationExpr findAnnotation(final BodyDeclaration n, String fullyQualifiedName, boolean foundAnnImport) {
156        final String simpleName = ClassUtils.getShortClassName(fullyQualifiedName);
157        final List<AnnotationExpr> annotations = n.getAnnotations() != null ? n.getAnnotations() : new ArrayList<AnnotationExpr>();
158
159        for (AnnotationExpr ae : annotations) {
160            final String name = ae.getName().toString();
161            if ((simpleName.equals(name) && foundAnnImport)) {
162                LOG.info("found " + ae + " on " + getTypeOrFieldNameForMsg(n) + ".");
163                return ae;
164            }
165
166            if (fullyQualifiedName.equals(name)) {
167                LOG.info("found " + ae + " on " + getTypeOrFieldNameForMsg(n) + ".");
168                return ae;
169            }
170        }
171        return null;
172    }
173
174    private TypeDeclaration findTypeDeclaration(List<BodyDeclaration> members, String name) {
175        if (members != null) {
176            for (BodyDeclaration bd : members) {
177                if (bd instanceof TypeDeclaration) {
178                    if (((TypeDeclaration) bd).getName().equals(name)) {
179                        return (TypeDeclaration) bd;
180                    }
181                }
182            }
183        }
184        return null;
185    }
186
187    private boolean imported(List<ImportDeclaration> imports, String fullyQualifiedName) {
188        final String packageName = ClassUtils.getPackageName(fullyQualifiedName);
189
190        for (final ImportDeclaration i : imports) {
191            if (!i.isStatic()) {
192                final String importName = i.getName().toString();
193                if (i.isAsterisk()) {
194                    if (packageName.equals(importName)) {
195                        if ( LOG.isDebugEnabled() ) {
196                            LOG.debug("found import " + packageName + ".* on " + getTypeNameForMsg(i) + ".");
197                        }
198                        return true;
199                    }
200                } else {
201                    if (fullyQualifiedName.equals(importName)) {
202                        if ( LOG.isDebugEnabled() ) {
203                            LOG.debug("found import " + fullyQualifiedName + " on " + getTypeNameForMsg(i) + ".");
204                        }
205                        return true;
206                    }
207                }
208            }
209        }
210        return false;
211    }
212
213    private String getTypeOrFieldNameForMsg(final BodyDeclaration n) {
214        if (n instanceof TypeDeclaration) {
215            return ((TypeDeclaration) n).getName();
216        } else if (n instanceof FieldDeclaration) {
217            final FieldDeclaration fd = (FieldDeclaration) n;
218            //this wont work for nested classes but we should be in nexted classes at this point
219            final CompilationUnit unit = getCompilationUnit(n);
220            final TypeDeclaration parent = unit.getTypes().get(0);
221            Collection<String> variableNames = new ArrayList<String>();
222            if (fd.getVariables() != null) {
223                for (VariableDeclarator vd : fd.getVariables()) {
224                    variableNames.add(vd.getId().getName());
225                }
226            }
227            return variableNames.size() == 1 ?
228                    parent.getName() + "." + variableNames.iterator().next() :
229                    parent.getName() + "." + variableNames.toString();
230
231        }
232        return null;
233    }
234
235    private String getTypeNameForMsg(final Node n) {
236        final CompilationUnit unit = getCompilationUnit(n);
237        final TypeDeclaration parent = unit.getTypes().get(0);
238        return parent.getName();
239    }
240}