001/*
002 * Copyright (C) 2009-2014 The Project Lombok Authors.
003 *
004 * Permission is hereby granted, free of charge, to any person obtaining a copy
005 * of this software and associated documentation files (the "Software"), to deal
006 * in the Software without restriction, including without limitation the rights
007 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
008 * copies of the Software, and to permit persons to whom the Software is
009 * furnished to do so, subject to the following conditions:
010 *
011 * The above copyright notice and this permission notice shall be included in
012 * all copies or substantial portions of the Software.
013 *
014 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
015 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
016 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
017 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
018 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
019 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
020 * THE SOFTWARE.
021 */
022package hm.binkley.lombok.javac;
023
024import com.sun.tools.javac.tree.JCTree;
025import com.sun.tools.javac.tree.JCTree.JCAnnotation;
026import com.sun.tools.javac.tree.JCTree.JCAssign;
027import com.sun.tools.javac.tree.JCTree.JCBlock;
028import com.sun.tools.javac.tree.JCTree.JCCompilationUnit;
029import com.sun.tools.javac.tree.JCTree.JCExpression;
030import com.sun.tools.javac.tree.JCTree.JCMethodDecl;
031import com.sun.tools.javac.tree.JCTree.JCMethodInvocation;
032import com.sun.tools.javac.tree.JCTree.JCStatement;
033import com.sun.tools.javac.tree.JCTree.JCTry;
034import com.sun.tools.javac.tree.JCTree.JCVariableDecl;
035import com.sun.tools.javac.util.Context;
036import com.sun.tools.javac.util.List;
037import hm.binkley.lombok.ThreadNamed;
038import lombok.core.AnnotationValues;
039import lombok.core.HandlerPriority;
040import lombok.core.configuration.ConfigurationKey;
041import lombok.core.configuration.FlagUsageType;
042import lombok.javac.Javac;
043import lombok.javac.JavacAnnotationHandler;
044import lombok.javac.JavacNode;
045import lombok.javac.JavacTreeMaker;
046import org.kohsuke.MetaInfServices;
047
048import static com.sun.tools.javac.code.Flags.ABSTRACT;
049import static com.sun.tools.javac.code.Flags.FINAL;
050import static com.sun.tools.javac.util.List.nil;
051import static lombok.core.handlers.HandlerUtil.handleFlagUsage;
052import static lombok.javac.handlers.JavacHandlerUtil
053        .deleteAnnotationIfNeccessary;
054import static lombok.javac.handlers.JavacHandlerUtil.genJavaLangTypeRef;
055import static lombok.javac.handlers.JavacHandlerUtil.inNetbeansEditor;
056import static lombok.javac.handlers.JavacHandlerUtil.isConstructorCall;
057import static lombok.javac.handlers.JavacHandlerUtil.setGeneratedBy;
058
059/**
060 * Handles the {@link ThreadNamed} annotation for javac.
061 *
062 * @author <a href="mailto:binkley@alumni.rice.edu">B. K. Oxley (binkley)</a>
063 */
064@MetaInfServices(JavacAnnotationHandler.class)
065@HandlerPriority(1024)
066// 2^10; @NonNull must have run first, so that we wrap around the statements
067// generated by it.
068public class HandleThreadNamed
069        extends JavacAnnotationHandler<ThreadNamed> {
070    /**
071     * lombok configuration: {@code hm.binkley.lombok.threadNamed.flagUsage} =
072     * {@code WARNING} | {@code ERROR}.
073     * <p>
074     * If set, <em>any</em> usage of {@link @ThreadNamed} results in a warning /
075     * error.
076     */
077    public static final ConfigurationKey<FlagUsageType> THREAD_NAMED_FLAG_USAGE
078            = new ConfigurationKey<FlagUsageType>(
079            "hm.binkley.lombok.threadNamed.flagUsage",
080            "Emit a warning or error if @ThreadNamed is used.") {
081    };
082
083    @Override
084    public void handle(final AnnotationValues<ThreadNamed> annotation,
085            final JCAnnotation ast, final JavacNode annotationNode) {
086        handleFlagUsage(annotationNode, THREAD_NAMED_FLAG_USAGE,
087                "@ThreadNamed");
088
089        deleteAnnotationIfNeccessary(annotationNode, ThreadNamed.class);
090        if (annotation.getInstance().value().isEmpty()) {
091            annotationNode.addError("threadName cannot be the empty string.");
092            return;
093        }
094
095        final JCAnnotation anno = (JCAnnotation) annotationNode.get();
096        final List<JCExpression> args = anno.args;
097        final JCExpression threadName = ((JCAssign) args.get(0))
098                .getExpression(); // Must be present
099
100        final JavacNode owner = annotationNode.up();
101        switch (owner.getKind()) {
102        case METHOD:
103            handleMethod(annotationNode, (JCMethodDecl) owner.get(),
104                    threadName);
105            break;
106        default:
107            annotationNode.addError(
108                    "@ThreadNamed is legal only on methods and constructors.");
109            break;
110        }
111    }
112
113    private static void handleMethod(final JavacNode annotation,
114            final JCMethodDecl method, final JCExpression threadName) {
115        final JavacNode methodNode = annotation.up();
116
117        if (0 != (method.mods.flags & ABSTRACT)) {
118            annotation.addError(
119                    "@ThreadNamed can only be used on concrete methods.");
120            return;
121        }
122
123        if (null == method.body || method.body.stats.isEmpty()) {
124            generateEmptyBlockWarning(annotation, false);
125            return;
126        }
127
128        final JCStatement constructorCall = method.body.stats.get(0);
129        final boolean isConstructorCall = isConstructorCall(constructorCall);
130        final List<JCStatement> contents = isConstructorCall
131                ? method.body.stats.tail : method.body.stats;
132
133        if (null == contents || contents.isEmpty()) {
134            generateEmptyBlockWarning(annotation, true);
135            return;
136        }
137
138        final List<JCStatement> wrapped = List
139                .of(renameThreadWhileIn(methodNode, contents, threadName,
140                        method.params, annotation.get()));
141        method.body.stats = isConstructorCall ? List.of(constructorCall)
142                .appendList(wrapped) : wrapped;
143        methodNode.rebuild();
144    }
145
146    private static void generateEmptyBlockWarning(final JavacNode annotation,
147            final boolean hasConstructorCall) {
148        if (hasConstructorCall)
149            annotation.addWarning(
150                    "Calls to sibling / super constructors are always "
151                            + "excluded from @ThreadNamed;"
152                            + " @ThreadNamed has been ignored because there "
153                            + "is no other code in "
154                            + "this constructor.");
155        else
156            annotation.addWarning(
157                    "This method or constructor is empty; @ThreadNamed has "
158                            + "been ignored.");
159    }
160
161    private static JCStatement renameThreadWhileIn(final JavacNode node,
162            final List<JCStatement> contents,
163            final JCExpression threadNameFormat,
164            final List<JCVariableDecl> params, final JCTree source) {
165        final String currentThreadVarName = "$currentThread";
166        final String oldThreadNameVarName = "$oldThreadName";
167
168        final JavacTreeMaker maker = node.getTreeMaker();
169        final Context context = node.getContext();
170
171        final JCVariableDecl saveCurrentThread = createCurrentThreadVar(node,
172                maker, currentThreadVarName);
173        final JCVariableDecl saveOldThreadName = createOldThreadNameVar(node,
174                maker, currentThreadVarName, oldThreadNameVarName);
175
176        final JCExpression threadName = threadName(node, maker,
177                threadNameFormat, params);
178        final JCStatement changeThreadName = setThreadName(node, maker,
179                threadName, currentThreadVarName);
180        final JCStatement restoreOldThreadName = setThreadName(node, maker,
181                maker.Ident(node.toName(oldThreadNameVarName)),
182                currentThreadVarName);
183
184        final JCBlock tryBlock = setGeneratedBy(maker.Block(0, contents),
185                source, context);
186        final JCTry wrapMethod = maker.Try(tryBlock, nil(),
187                maker.Block(0, List.of(restoreOldThreadName)));
188
189        if (inNetbeansEditor(node)) {
190            //set span (start and end position) of the try statement and the
191            // main block
192            //this allows NetBeans to dive into the statement correctly:
193            final JCCompilationUnit top = (JCCompilationUnit) node.top().get();
194            final int startPos = contents.head.pos;
195            final int endPos = Javac.getEndPosition(contents.last().pos(), top);
196            tryBlock.pos = startPos;
197            wrapMethod.pos = startPos;
198            Javac.storeEnd(tryBlock, endPos, top);
199            Javac.storeEnd(wrapMethod, endPos, top);
200        }
201
202        return setGeneratedBy(maker.Block(0,
203                        List.of(saveCurrentThread, saveOldThreadName,
204                                changeThreadName, wrapMethod)), source,
205                context);
206    }
207
208    private static JCVariableDecl createCurrentThreadVar(final JavacNode node,
209            final JavacTreeMaker maker, final String currentThreadVarName) {
210        return maker.VarDef(maker.Modifiers(FINAL),
211                node.toName(currentThreadVarName),
212                genJavaLangTypeRef(node, "Thread"), maker.Apply(nil(),
213                genJavaLangTypeRef(node, "Thread", "currentThread"), nil()));
214    }
215
216    private static JCVariableDecl createOldThreadNameVar(final JavacNode node,
217            final JavacTreeMaker maker, final String currentThreadVarName,
218            final String oldThreadNameVarName) {
219        return maker.VarDef(maker.Modifiers(FINAL),
220                node.toName(oldThreadNameVarName),
221                genJavaLangTypeRef(node, "String"),
222                getThreadName(node, maker, currentThreadVarName));
223    }
224
225    private static JCExpression threadName(final JavacNode node,
226            final JavacTreeMaker maker, final JCExpression threadNameFormat,
227            final List<JCVariableDecl> params) {
228        if (params.isEmpty())
229            return threadNameFormat;
230
231        List<JCExpression> formatArgs = List.of(threadNameFormat);
232        for (final JCVariableDecl param : params)
233            formatArgs = formatArgs.append(maker.Ident(param));
234
235        return maker.Apply(nil(),
236                maker.Select(genJavaLangTypeRef(node, "String"),
237                        node.toName("format")), formatArgs);
238    }
239
240    private static JCMethodInvocation getThreadName(final JavacNode node,
241            final JavacTreeMaker maker, final String currentThreadVarNAme) {
242        return maker.Apply(nil(),
243                maker.Select(maker.Ident(node.toName(currentThreadVarNAme)),
244                        node.toName("getName")), nil());
245    }
246
247    private static JCStatement setThreadName(final JavacNode node,
248            final JavacTreeMaker maker, final JCExpression threadName,
249            final String currentThreadVarName) {
250        return maker.Exec(maker.Apply(nil(),
251                maker.Select(maker.Ident(node.toName(currentThreadVarName)),
252                        node.toName("setName")), List.of(threadName)));
253    }
254}