This Bugzilla instance is a read-only archive of historic NetBeans bug reports. To report a bug in NetBeans please follow the project's instructions for reporting issues.

View | Details | Raw Unified | Return to bug 149136
Collapse All | Expand All

(-)a/openide.filesystems/src/org/openide/filesystems/annotations/LayerBuilder.java (+546 lines)
Line 0 Link Here
1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
4
 * Copyright 2008 Sun Microsystems, Inc. All rights reserved.
5
 *
6
 * The contents of this file are subject to the terms of either the GNU
7
 * General Public License Version 2 only ("GPL") or the Common
8
 * Development and Distribution License("CDDL") (collectively, the
9
 * "License"). You may not use this file except in compliance with the
10
 * License. You can obtain a copy of the License at
11
 * http://www.netbeans.org/cddl-gplv2.html
12
 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
13
 * specific language governing permissions and limitations under the
14
 * License.  When distributing the software, include this License Header
15
 * Notice in each file and include the License file at
16
 * nbbuild/licenses/CDDL-GPL-2-CP.  Sun designates this
17
 * particular file as subject to the "Classpath" exception as provided
18
 * by Sun in the GPL Version 2 section of the License file that
19
 * accompanied this code. If applicable, add the following below the
20
 * License Header, with the fields enclosed by brackets [] replaced by
21
 * your own identifying information:
22
 * "Portions Copyrighted [year] [name of copyright owner]"
23
 *
24
 * If you wish your version of this file to be governed by only the CDDL
25
 * or only the GPL Version 2, indicate your decision by adding
26
 * "[Contributor] elects to include this software in this distribution
27
 * under the [CDDL or GPL Version 2] license." If you do not indicate a
28
 * single choice of license, a recipient has the option to distribute
29
 * your version of this file under either the CDDL, the GPL Version 2 or
30
 * to extend the choice of license to its licensees as provided above.
31
 * However, if you add GPL Version 2 code and therefore, elected the GPL
32
 * Version 2 license, then the option applies only if the new code is
33
 * made subject to such option by the copyright holder.
34
 *
35
 * Contributor(s):
36
 *
37
 * Portions Copyrighted 2008 Sun Microsystems, Inc.
38
 */
39
40
package org.openide.filesystems.annotations;
41
42
import java.io.IOException;
43
import java.io.InputStream;
44
import java.net.URL;
45
import java.util.Arrays;
46
import java.util.LinkedHashMap;
47
import java.util.Locale;
48
import java.util.Map;
49
import java.util.Properties;
50
import java.util.regex.Matcher;
51
import java.util.regex.Pattern;
52
import javax.annotation.processing.Filer;
53
import javax.annotation.processing.ProcessingEnvironment;
54
import javax.lang.model.element.ElementKind;
55
import javax.lang.model.element.ExecutableElement;
56
import javax.lang.model.element.Modifier;
57
import javax.lang.model.element.PackageElement;
58
import javax.lang.model.element.TypeElement;
59
import javax.lang.model.type.TypeMirror;
60
import javax.lang.model.util.ElementFilter;
61
import javax.tools.Diagnostic.Kind;
62
import javax.tools.StandardLocation;
63
import org.w3c.dom.Document;
64
import org.w3c.dom.Element;
65
import org.w3c.dom.NodeList;
66
67
/**
68
 * Convenience class for generating fragments of an XML layer.
69
 * @see LayerGeneratingProcessor#layer
70
 * @since XXX #150447
71
 */
72
public final class LayerBuilder {
73
74
    private final Document doc;
75
76
    /**
77
     * Creates a new builder.
78
     * @param document a DOM representation of an XML layer which will be modified
79
     */
80
    public LayerBuilder(Document document) {
81
        this.doc = document;
82
    }
83
84
    /**
85
     * Adds a file to the layer.
86
     * You need to {@link File#write} it in order to finalize the effect.
87
     * @param path the full path to the desired file in resource format, e.g. {@code "Menu/File/exit.instance"}
88
     * @return a file builder
89
     */
90
    public File file(String path) {
91
        return new File(path);
92
    }
93
94
    /**
95
     * Generates an instance file whose {@code InstanceCookie} would load a given class or method.
96
     * Useful for {@link LayerGeneratingProcessor}s which define layer fragments which instantiate Java objects from the annotated code.
97
     * <p>While you can pick a specific instance file name, if possible you should pass null for {@code name}
98
     * as using the generated name will help avoid accidental name collisions between annotations.
99
     * @param annotationTarget an annotated {@linkplain TypeElement class} or {@linkplain ExecutableElement method}
100
     * @param path path to folder of instance file, e.g. {@code "Menu/File"}
101
     * @param name instance file basename, e.g. {@code "my-menu-Item"}, or null to pick a name according to the element
102
     * @param type a type to which the instance ought to be assignable, or null to skip this check
103
     * @param processingEnv a processor environment used for {@link ProcessingEnvironment#getElementUtils} and {@link ProcessingEnvironment#getTypeUtils}
104
     * @return an instance file (call {@link File#write} to finalize)
105
     * @throws IllegalArgumentException if the annotationTarget is not of a suitable sort
106
     *                                  (detail message can be reported as a {@link Kind#ERROR})
107
     */
108
    public File instanceFile(javax.lang.model.element.Element annotationTarget, String path, String name, Class type,
109
            ProcessingEnvironment processingEnv) throws IllegalArgumentException {
110
        String[] clazzOrMethod = instantiableClassOrMethod(annotationTarget, type, processingEnv);
111
        String clazz = clazzOrMethod[0];
112
        String method = clazzOrMethod[1];
113
        String basename;
114
        if (name == null) {
115
            basename = clazz.replace('.', '-');
116
            if (method != null) {
117
                basename += "-" + method;
118
            }
119
        } else {
120
            basename = name;
121
        }
122
        LayerBuilder.File f = file(path + "/" + basename + ".instance");
123
        if (method != null) {
124
            f.methodvalue("instanceCreate", clazz, method);
125
        } else if (name != null) {
126
            f.stringvalue("instanceClass", clazz);
127
        } // else name alone suffices
128
        return f;
129
    }
130
131
    private static String[] instantiableClassOrMethod(javax.lang.model.element.Element annotationTarget, Class type,
132
            ProcessingEnvironment processingEnv) throws IllegalArgumentException {
133
        TypeMirror typeMirror = type != null ? processingEnv.getElementUtils().getTypeElement(type.getName().replace('$', '.')).asType() : null;
134
        switch (annotationTarget.getKind()) {
135
            case CLASS: {
136
                String clazz = processingEnv.getElementUtils().getBinaryName((TypeElement) annotationTarget).toString();
137
                if (annotationTarget.getModifiers().contains(Modifier.ABSTRACT)) {
138
                    throw new IllegalArgumentException(clazz + " must not be abstract");
139
                }
140
                {
141
                    boolean hasDefaultCtor = false;
142
                    for (ExecutableElement constructor : ElementFilter.constructorsIn(annotationTarget.getEnclosedElements())) {
143
                        if (constructor.getParameters().isEmpty()) {
144
                            hasDefaultCtor = true;
145
                            break;
146
                        }
147
                    }
148
                    if (!hasDefaultCtor) {
149
                        throw new IllegalArgumentException(clazz + " must have a no-argument constructor");
150
                    }
151
                }
152
                if (typeMirror != null && !processingEnv.getTypeUtils().isAssignable(annotationTarget.asType(), typeMirror)) {
153
                    throw new IllegalArgumentException(clazz + " is not assignable to " + typeMirror);
154
                }
155
                return new String[] {clazz, null};
156
            }
157
            case METHOD: {
158
                String clazz = processingEnv.getElementUtils().getBinaryName((TypeElement) annotationTarget.getEnclosingElement()).toString();
159
                String method = annotationTarget.getSimpleName().toString();
160
                if (!annotationTarget.getModifiers().contains(Modifier.STATIC)) {
161
                    throw new IllegalArgumentException(clazz + "." + method + " must be static");
162
                }
163
                if (!((ExecutableElement) annotationTarget).getParameters().isEmpty()) {
164
                    throw new IllegalArgumentException(clazz + "." + method + " must not take arguments");
165
                }
166
                if (typeMirror != null && !processingEnv.getTypeUtils().isAssignable(((ExecutableElement) annotationTarget).getReturnType(), typeMirror)) {
167
                    throw new IllegalArgumentException(clazz + "." + method + " is not assignable to " + typeMirror);
168
                }
169
                return new String[] {clazz, method};
170
            }
171
            default:
172
                throw new IllegalArgumentException("Annotated element is not loadable as an instance: " + annotationTarget);
173
        }
174
    }
175
176
    /**
177
     * Convenience method to create a shadow file (like a symbolic link).
178
     * <p>While you can pick a specific shadow file name, if possible you should pass null for {@code name}
179
     * as using the generated name will help avoid accidental name collisions between annotations.
180
     * @param target the complete path to the original file (use {@link File#getPath} if you just made it)
181
     * @param folder the folder path in which to create the shadow, e.g. {@code "Menu/File"}
182
     * @param name the basename of the shadow file sans extension, e.g. {@code "my-Action"}, or null to pick a default
183
     * @return a shadow file (call {@link File#write} to finalize)
184
     */
185
    public File shadowFile(String target, String folder, String name) {
186
        if (name == null) {
187
            name = target.replaceFirst("^.+/", "").replaceFirst("\\.[^./]+$", "");
188
        }
189
        return file(folder + "/" + name + ".shadow").stringvalue("originalFile", target);
190
    }
191
192
    /**
193
     * Builder for creating a single file entry.
194
     */
195
    public final class File {
196
197
        private final String path;
198
        private final Map<String,String[]> attrs = new LinkedHashMap<String,String[]>();
199
        private String contents;
200
        private String url;
201
202
        File(String path) {
203
            this.path = path;
204
        }
205
206
        /**
207
         * Gets the path this file is to be created under.
208
         * @return the configured path, as in {@link #file}
209
         */
210
        public String getPath() {
211
            return path;
212
        }
213
214
        /**
215
         * Configures the file to have inline text contents.
216
         * @param contents text to use as the body of the file
217
         * @return this builder
218
         */
219
        public File contents(String contents) {
220
            if (this.contents != null || url != null || contents == null) {
221
                throw new IllegalArgumentException();
222
            }
223
            this.contents = contents;
224
            return this;
225
        }
226
227
        /**
228
         * Configures the file to have external contents.
229
         * @param url a URL to the body of the file, e.g. {@code "nbresloc:/org/my/module/resources/definition.xml"}
230
         *            or more commonly an absolute resource path such as {@code "/org/my/module/resources/definition.xml"}
231
         * @return this builder
232
         */
233
        public File url(String url) {
234
            if (contents != null || this.url != null || url == null) {
235
                throw new IllegalArgumentException();
236
            }
237
            this.url = url;
238
            return this;
239
        }
240
241
        /**
242
         * Adds a string-valued attribute.
243
         * @param attr the attribute name
244
         * @param value the attribute value
245
         * @return this builder
246
         */
247
        public File stringvalue(String attr, String value) {
248
            attrs.put(attr, new String[] {"stringvalue", value});
249
            return this;
250
        }
251
252
        /**
253
         * Adds a byte-valued attribute.
254
         * @param attr the attribute name
255
         * @param value the attribute value
256
         * @return this builder
257
         */
258
        public File bytevalue(String attr, byte value) {
259
            attrs.put(attr, new String[] {"bytevalue", Byte.toString(value)});
260
            return this;
261
        }
262
263
        /**
264
         * Adds a short-valued attribute.
265
         * @param attr the attribute name
266
         * @param value the attribute value
267
         * @return this builder
268
         */
269
        public File shortvalue(String attr, short value) {
270
            attrs.put(attr, new String[] {"shortvalue", Short.toString(value)});
271
            return this;
272
        }
273
274
        /**
275
         * Adds an int-valued attribute.
276
         * @param attr the attribute name
277
         * @param value the attribute value
278
         * @return this builder
279
         */
280
        public File intvalue(String attr, int value) {
281
            attrs.put(attr, new String[] {"intvalue", Integer.toString(value)});
282
            return this;
283
        }
284
285
        /**
286
         * Adds a long-valued attribute.
287
         * @param attr the attribute name
288
         * @param value the attribute value
289
         * @return this builder
290
         */
291
        public File longvalue(String attr, long value) {
292
            attrs.put(attr, new String[] {"longvalue", Long.toString(value)});
293
            return this;
294
        }
295
296
        /**
297
         * Adds a float-valued attribute.
298
         * @param attr the attribute name
299
         * @param value the attribute value
300
         * @return this builder
301
         */
302
        public File floatvalue(String attr, float value) {
303
            attrs.put(attr, new String[] {"floatvalue", Float.toString(value)});
304
            return this;
305
        }
306
307
        /**
308
         * Adds a double-valued attribute.
309
         * @param attr the attribute name
310
         * @param value the attribute value
311
         * @return this builder
312
         */
313
        public File doublevalue(String attr, double value) {
314
            attrs.put(attr, new String[] {"doublevalue", Double.toString(value)});
315
            return this;
316
        }
317
318
        /**
319
         * Adds a boolean-valued attribute.
320
         * @param attr the attribute name
321
         * @param value the attribute value
322
         * @return this builder
323
         */
324
        public File boolvalue(String attr, boolean value) {
325
            attrs.put(attr, new String[] {"boolvalue", Boolean.toString(value)});
326
            return this;
327
        }
328
329
        /**
330
         * Adds a character-valued attribute.
331
         * @param attr the attribute name
332
         * @param value the attribute value
333
         * @return this builder
334
         */
335
        public File charvalue(String attr, char value) {
336
            attrs.put(attr, new String[] {"charvalue", Character.toString(value)});
337
            return this;
338
        }
339
340
        /**
341
         * Adds a URL-valued attribute.
342
         * @param attr the attribute name
343
         * @param value the attribute value
344
         * @return this builder
345
         */
346
        public File urlvalue(String attr, URL value) {
347
            attrs.put(attr, new String[] {"urlvalue", value.toString()});
348
            return this;
349
        }
350
351
        /**
352
         * Adds an attribute loaded from a Java method.
353
         * @param attr the attribute name
354
         * @param clazz the fully-qualified name of the factory class
355
         * @param method the name of a static method
356
         * @return this builder
357
         */
358
        public File methodvalue(String attr, String clazz, String method) {
359
            attrs.put(attr, new String[] {"methodvalue", clazz + "." + method});
360
            return this;
361
        }
362
363
        /**
364
         * Adds an attribute loaded from a Java constructor.
365
         * @param attr the attribute name
366
         * @param clazz the fully-qualified name of a class with a no-argument constructor
367
         * @return this builder
368
         */
369
        public File newvalue(String attr, String clazz) {
370
            attrs.put(attr, new String[] {"newvalue", clazz});
371
            return this;
372
        }
373
374
        /**
375
         * Adds an attribute to load a given class or method.
376
         * Useful for {@link LayerGeneratingProcessor}s which define layer fragments which instantiate Java objects from the annotated code.
377
         * @param attr the attribute name
378
         * @param annotationTarget an annotated {@linkplain TypeElement class} or {@linkplain ExecutableElement method}
379
         * @param type a type to which the instance ought to be assignable, or null to skip this check
380
         * @param processingEnv a processor environment used for {@link ProcessingEnvironment#getElementUtils} and {@link ProcessingEnvironment#getTypeUtils}
381
         * @return this builder
382
         * @throws IllegalArgumentException if the annotationTarget is not of a suitable sort
383
         *                                  (detail message can be reported as a {@link Kind#ERROR})
384
         */
385
        public File instanceAttribute(String attr, javax.lang.model.element.Element annotationTarget, Class type,
386
            ProcessingEnvironment processingEnv) throws IllegalArgumentException {
387
            String[] clazzOrMethod = instantiableClassOrMethod(annotationTarget, type, processingEnv);
388
            if (clazzOrMethod[1] == null) {
389
                newvalue(attr, clazzOrMethod[0]);
390
            } else {
391
                methodvalue(attr, clazzOrMethod[0], clazzOrMethod[1]);
392
            }
393
            return this;
394
        }
395
396
        /**
397
         * Adds an attribute loaded from a resource bundle.
398
         * @param attr the attribute name
399
         * @param bundle the full name of the bundle, e.g. {@code "org.my.module.Bundle"}
400
         * @param key the key to look up inside the bundle
401
         * @return this builder
402
         */
403
        public File bundlevalue(String attr, String bundle, String key) {
404
            attrs.put(attr, new String[] {"bundlevalue", bundle + "#" + key});
405
            return this;
406
        }
407
408
        /**
409
         * Adds an attribute for a possibly localized string.
410
         * @param attr the attribute name
411
         * @param label either a general string to store as is, or a resource bundle reference
412
         *              such as {@code "my.module.Bundle#some_key"},
413
         *              or just {@code "#some_key"} to load from a {@code "Bundle"} in the same package
414
         * @param referenceElement if not null, a source element to determine the package
415
         * @param filer if not null, a way to look up the source bundle to verify that it exists and has the specified key
416
         * @return this builder
417
         * @throws IllegalArgumentException if a bundle key is requested but it cannot be found in sources
418
         *                                  (detail message can be reported as a {@link Kind#ERROR})
419
         */
420
        public File bundlevalue(String attr, String label, javax.lang.model.element.Element referenceElement, Filer filer) throws IllegalArgumentException {
421
            String javaIdentifier = "(?:\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*)";
422
            Matcher m = Pattern.compile("((?:" + javaIdentifier + "\\.)+[^\\s.#]+)?#(\\S+)").matcher(label);
423
            if (m.matches()) {
424
                String bundle = m.group(1);
425
                String key = m.group(2);
426
                if (bundle == null) {
427
                    while (referenceElement != null && referenceElement.getKind() != ElementKind.PACKAGE) {
428
                        referenceElement = referenceElement.getEnclosingElement();
429
                    }
430
                    if (referenceElement == null) {
431
                        throw new IllegalArgumentException("No reference element to determine package in '" + label + "'");
432
                    }
433
                    bundle = ((PackageElement) referenceElement).getQualifiedName() + ".Bundle";
434
                }
435
                if (filer != null) {
436
                    try {
437
                        InputStream is = filer.getResource(StandardLocation.SOURCE_PATH, "", bundle.replace('.', '/') + ".properties").openInputStream();
438
                        try {
439
                            Properties p = new Properties();
440
                            p.load(is);
441
                            if (p.getProperty(key) == null) {
442
                                throw new IllegalArgumentException("No key '" + key + "' found in " + bundle);
443
                            }
444
                        } finally {
445
                            is.close();
446
                        }
447
                    } catch (IOException x) {
448
                        throw new IllegalArgumentException("Could not open " + bundle + ": " + x);
449
                    }
450
                }
451
                bundlevalue(attr, bundle, key);
452
            } else {
453
                stringvalue(attr, label);
454
            }
455
            return this;
456
        }
457
458
        /**
459
         * Adds an attribute which deserializes a Java value.
460
         * @param attr the attribute name
461
         * @param data the serial data as created by {@link ObjectOutputStream}
462
         * @return this builder
463
         */
464
        public File serialvalue(String attr, byte[] data) {
465
            StringBuilder buf = new StringBuilder(data.length * 2);
466
            for (byte b : data) {
467
                if (b >= 0 && b < 16) {
468
                    buf.append('0');
469
                }
470
                buf.append(Integer.toHexString(b < 0 ? b + 256 : b));
471
            }
472
            attrs.put(attr, new String[] {"serialvalue", buf.toString().toUpperCase(Locale.ENGLISH)});
473
            return this;
474
        }
475
476
        /**
477
         * Sets a position attribute.
478
         * This is a convenience method so you can define in your annotation:
479
         * <code>int position() default Integer.MAX_VALUE;</code>
480
         * and later call:
481
         * <code>fileBuilder.position(annotation.position())</code>
482
         * @param position a numeric position for this file, or {@link Integer#MAX_VALUE} to not define any position
483
         * @return this builder
484
         */
485
        public File position(int position) {
486
            if (position != Integer.MAX_VALUE) {
487
                intvalue("position", position);
488
            }
489
            return this;
490
        }
491
492
        /**
493
         * Writes the file to the layer.
494
         * Any intervening parent folders are created automatically.
495
         * If the file already exists, the old copy is replaced.
496
         * @return the originating layer builder, in case you want to add another file
497
         */
498
        public LayerBuilder write() {
499
            Element e = doc.getDocumentElement();
500
            String[] pieces = path.split("/");
501
            for (String piece : Arrays.asList(pieces).subList(0, pieces.length - 1)) {
502
                Element kid = find(e, piece);
503
                if (kid != null) {
504
                    if (!kid.getNodeName().equals("folder")) {
505
                        throw new IllegalArgumentException(path);
506
                    }
507
                    e = kid;
508
                } else {
509
                    e = (Element) e.appendChild(doc.createElement("folder"));
510
                    e.setAttribute("name", piece);
511
                }
512
            }
513
            String piece = pieces[pieces.length - 1];
514
            Element file = find(e,piece);
515
            if (file != null) {
516
                e.removeChild(file);
517
            }
518
            file = (Element) e.appendChild(doc.createElement("file"));
519
            file.setAttribute("name", piece);
520
            for (Map.Entry<String,String[]> entry : attrs.entrySet()) {
521
                Element attr = (Element) file.appendChild(doc.createElement("attr"));
522
                attr.setAttribute("name", entry.getKey());
523
                attr.setAttribute(entry.getValue()[0], entry.getValue()[1]);
524
            }
525
            if (url != null) {
526
                file.setAttribute("url", url);
527
            } else if (contents != null) {
528
                file.appendChild(doc.createCDATASection(contents));
529
            }
530
            return LayerBuilder.this;
531
        }
532
533
        private Element find(Element parent, String name) {
534
            NodeList nl = parent.getElementsByTagName("*");
535
            for (int i = 0; i < nl.getLength(); i++) {
536
                Element e = (Element) nl.item(i);
537
                if (e.getAttribute("name").equals(name)) {
538
                    return e;
539
                }
540
            }
541
            return null;
542
        }
543
544
    }
545
546
}
(-)a/openide.filesystems/src/org/openide/filesystems/annotations/LayerGeneratingProcessor.java (+210 lines)
Line 0 Link Here
1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
4
 * Copyright 2008 Sun Microsystems, Inc. All rights reserved.
5
 *
6
 * The contents of this file are subject to the terms of either the GNU
7
 * General Public License Version 2 only ("GPL") or the Common
8
 * Development and Distribution License("CDDL") (collectively, the
9
 * "License"). You may not use this file except in compliance with the
10
 * License. You can obtain a copy of the License at
11
 * http://www.netbeans.org/cddl-gplv2.html
12
 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
13
 * specific language governing permissions and limitations under the
14
 * License.  When distributing the software, include this License Header
15
 * Notice in each file and include the License file at
16
 * nbbuild/licenses/CDDL-GPL-2-CP.  Sun designates this
17
 * particular file as subject to the "Classpath" exception as provided
18
 * by Sun in the GPL Version 2 section of the License file that
19
 * accompanied this code. If applicable, add the following below the
20
 * License Header, with the fields enclosed by brackets [] replaced by
21
 * your own identifying information:
22
 * "Portions Copyrighted [year] [name of copyright owner]"
23
 *
24
 * If you wish your version of this file to be governed by only the CDDL
25
 * or only the GPL Version 2, indicate your decision by adding
26
 * "[Contributor] elects to include this software in this distribution
27
 * under the [CDDL or GPL Version 2] license." If you do not indicate a
28
 * single choice of license, a recipient has the option to distribute
29
 * your version of this file under either the CDDL, the GPL Version 2 or
30
 * to extend the choice of license to its licensees as provided above.
31
 * However, if you add GPL Version 2 code and therefore, elected the GPL
32
 * Version 2 license, then the option applies only if the new code is
33
 * made subject to such option by the copyright holder.
34
 *
35
 * Contributor(s):
36
 *
37
 * Portions Copyrighted 2008 Sun Microsystems, Inc.
38
 */
39
40
package org.openide.filesystems.annotations;
41
42
import java.io.ByteArrayInputStream;
43
import java.io.ByteArrayOutputStream;
44
import java.io.FileNotFoundException;
45
import java.io.IOException;
46
import java.io.InputStream;
47
import java.io.OutputStream;
48
import java.util.ArrayList;
49
import java.util.Arrays;
50
import java.util.List;
51
import java.util.Map;
52
import java.util.Set;
53
import java.util.SortedSet;
54
import java.util.TreeSet;
55
import java.util.WeakHashMap;
56
import javax.annotation.processing.AbstractProcessor;
57
import javax.annotation.processing.Filer;
58
import javax.annotation.processing.ProcessingEnvironment;
59
import javax.annotation.processing.RoundEnvironment;
60
import javax.lang.model.element.Element;
61
import javax.lang.model.element.TypeElement;
62
import javax.tools.Diagnostic.Kind;
63
import javax.tools.FileObject;
64
import javax.tools.StandardLocation;
65
import org.openide.filesystems.XMLFileSystem;
66
import org.openide.xml.XMLUtil;
67
import org.w3c.dom.Document;
68
import org.w3c.dom.NodeList;
69
import org.xml.sax.EntityResolver;
70
import org.xml.sax.ErrorHandler;
71
import org.xml.sax.InputSource;
72
import org.xml.sax.SAXException;
73
import org.xml.sax.SAXParseException;
74
75
/**
76
 * Convenience base class for an annotation processor which creates XML layer entries.
77
 * @see XMLFileSystem
78
 * @since XXX #150447
79
 */
80
public abstract class LayerGeneratingProcessor extends AbstractProcessor {
81
82
    private static final String GENERATED_LAYER = "META-INF/generated-layer.xml";
83
    private static final String PUBLIC_DTD_ID = "-//NetBeans//DTD Filesystem 1.2//EN";
84
    private static final String NETWORK_DTD_URL = "http://www.netbeans.org/dtds/filesystem-1_2.dtd";
85
    private static final String LOCAL_DTD_RESOURCE = "/org/openide/filesystems/filesystem1_2.dtd";
86
87
    private static final ErrorHandler ERROR_HANDLER = new ErrorHandler() {
88
        public void warning(SAXParseException exception) throws SAXException {throw exception;}
89
        public void error(SAXParseException exception) throws SAXException {throw exception;}
90
        public void fatalError(SAXParseException exception) throws SAXException {throw exception;}
91
    };
92
93
    private static final EntityResolver ENTITY_RESOLVER = new EntityResolver() {
94
        public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException {
95
            if (PUBLIC_DTD_ID.equals(publicId)) {
96
                return new InputSource(LayerGeneratingProcessor.class.getResource(LOCAL_DTD_RESOURCE).toString());
97
            } else {
98
                return null;
99
            }
100
        }
101
    };
102
103
    private static final Map<ProcessingEnvironment,Document> generatedLayerByProcessor = new WeakHashMap<ProcessingEnvironment,Document>();
104
    private static final Map<ProcessingEnvironment,List<Element>> originatingElementsByProcessor = new WeakHashMap<ProcessingEnvironment,List<Element>>();
105
106
    /** For access by subclasses. */
107
    protected LayerGeneratingProcessor() {}
108
109
    @Override
110
    public final boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
111
        boolean ret = doProcess(annotations, roundEnv);
112
        if (roundEnv.processingOver() && !roundEnv.errorRaised()) {
113
            Document doc = generatedLayerByProcessor.remove(processingEnv);
114
            if (doc != null) {
115
                Element[] originatingElementsA = new Element[0];
116
                List<Element> originatingElementsL = originatingElementsByProcessor.remove(processingEnv);
117
                if (originatingElementsL != null) {
118
                    originatingElementsA = originatingElementsL.toArray(originatingElementsA);
119
                }
120
                try {
121
                    // Write to memory and reparse to make sure it is valid according to DTD before writing to disk.
122
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
123
                    XMLUtil.write(doc, baos, "UTF-8");
124
                    byte[] data = baos.toByteArray();
125
                    XMLUtil.parse(new InputSource(new ByteArrayInputStream(data)), true, true, ERROR_HANDLER, ENTITY_RESOLVER);
126
                    FileObject layer = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", GENERATED_LAYER, originatingElementsA);
127
                    OutputStream os = layer.openOutputStream();
128
                    try {
129
                        os.write(data);
130
                    } finally {
131
                        os.close();
132
                    }
133
                    {
134
                        SortedSet<String> files = new TreeSet<String>();
135
                        NodeList nl = doc.getElementsByTagName("file");
136
                        for (int i = 0; i < nl.getLength(); i++) {
137
                            org.w3c.dom.Element e = (org.w3c.dom.Element) nl.item(i);
138
                            String name = e.getAttribute("name");
139
                            while ((e = (org.w3c.dom.Element) e.getParentNode()).getTagName().equals("folder")) {
140
                                name = e.getAttribute("name") + "/" + name;
141
                            }
142
                            files.add(name);
143
                        }
144
                        for (String file : files) {
145
                            processingEnv.getMessager().printMessage(Kind.NOTE, "generated layer entry: " + file);
146
                        }
147
                    }
148
                } catch (IOException x) {
149
                    processingEnv.getMessager().printMessage(Kind.ERROR, "Failed to write generated-layer.xml: " + x.toString());
150
                } catch (SAXException x) {
151
                    processingEnv.getMessager().printMessage(Kind.ERROR, "Refused to write invalid generated-layer.xml: " + x.toString());
152
                }
153
            }
154
        }
155
        return ret;
156
    }
157
158
    /**
159
     * The regular body of {@link #process}.
160
     * In the last round, one of the layer-generating processors will write out generated-layer.xml.
161
     * <p>Do not attempt to read or write the layer file directly; just use {@link #layer}.
162
     * You may however wish to create other resource files yourself: see {@link LayerBuilder.File#url} for syntax.
163
     * @param annotations as in {@link #process}
164
     * @param roundEnv as in {@link #process}
165
     * @return as in {@link #process}
166
     */
167
    protected abstract boolean doProcess(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv);
168
169
    /**
170
     * Access the generated XML layer document.
171
     * May already have content from a previous compilation run which should be overwritten.
172
     * May also have content from other layer-generated processors which should be appended to.
173
     * Simply make changes to the document and they will be written to disk at the end of the job.
174
     * <p>Use {@link LayerBuilder} to easily add file entries without working with the DOM directly.
175
     * @param originatingElements as in {@link Filer#createResource}, optional
176
     * @return the DOM document corresponding to the XML layer being created
177
     */
178
    protected final Document layer(Element... originatingElements) {
179
        List<Element> originatingElementsL = originatingElementsByProcessor.get(processingEnv);
180
        if (originatingElementsL == null) {
181
            originatingElementsL = new ArrayList<Element>();
182
            originatingElementsByProcessor.put(processingEnv, originatingElementsL);
183
        }
184
        originatingElementsL.addAll(Arrays.asList(originatingElements));
185
        Document doc = generatedLayerByProcessor.get(processingEnv);
186
        if (doc == null) {
187
            try {
188
                FileObject layer = processingEnv.getFiler().getResource(StandardLocation.CLASS_OUTPUT, "", GENERATED_LAYER);
189
                InputStream is = layer.openInputStream();
190
                try {
191
                    doc = XMLUtil.parse(new InputSource(is), true, true, ERROR_HANDLER, ENTITY_RESOLVER);
192
                } finally {
193
                    is.close();
194
                }
195
            } catch (FileNotFoundException fnfe) {
196
                // Fine, not yet created.
197
            } catch (IOException x) {
198
                processingEnv.getMessager().printMessage(Kind.ERROR, "Failed to read generated-layer.xml: " + x.toString());
199
            } catch (SAXException x) {
200
                processingEnv.getMessager().printMessage(Kind.ERROR, "Failed to parse generated-layer.xml: " + x.toString());
201
            }
202
            if (doc == null) {
203
                doc = XMLUtil.createDocument("filesystem", null, PUBLIC_DTD_ID, NETWORK_DTD_URL);
204
            }
205
            generatedLayerByProcessor.put(processingEnv, doc);
206
        }
207
        return doc;
208
    }
209
210
}
(-)a/openide.filesystems/test/unit/src/org/openide/filesystems/annotations/LayerBuilderTest.java (+120 lines)
Line 0 Link Here
1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
4
 * Copyright 2008 Sun Microsystems, Inc. All rights reserved.
5
 *
6
 * The contents of this file are subject to the terms of either the GNU
7
 * General Public License Version 2 only ("GPL") or the Common
8
 * Development and Distribution License("CDDL") (collectively, the
9
 * "License"). You may not use this file except in compliance with the
10
 * License. You can obtain a copy of the License at
11
 * http://www.netbeans.org/cddl-gplv2.html
12
 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
13
 * specific language governing permissions and limitations under the
14
 * License.  When distributing the software, include this License Header
15
 * Notice in each file and include the License file at
16
 * nbbuild/licenses/CDDL-GPL-2-CP.  Sun designates this
17
 * particular file as subject to the "Classpath" exception as provided
18
 * by Sun in the GPL Version 2 section of the License file that
19
 * accompanied this code. If applicable, add the following below the
20
 * License Header, with the fields enclosed by brackets [] replaced by
21
 * your own identifying information:
22
 * "Portions Copyrighted [year] [name of copyright owner]"
23
 *
24
 * If you wish your version of this file to be governed by only the CDDL
25
 * or only the GPL Version 2, indicate your decision by adding
26
 * "[Contributor] elects to include this software in this distribution
27
 * under the [CDDL or GPL Version 2] license." If you do not indicate a
28
 * single choice of license, a recipient has the option to distribute
29
 * your version of this file under either the CDDL, the GPL Version 2 or
30
 * to extend the choice of license to its licensees as provided above.
31
 * However, if you add GPL Version 2 code and therefore, elected the GPL
32
 * Version 2 license, then the option applies only if the new code is
33
 * made subject to such option by the copyright holder.
34
 *
35
 * Contributor(s):
36
 *
37
 * Portions Copyrighted 2008 Sun Microsystems, Inc.
38
 */
39
40
package org.openide.filesystems.annotations;
41
42
import java.io.ByteArrayOutputStream;
43
import java.io.IOException;
44
import org.netbeans.junit.NbTestCase;
45
import org.openide.xml.XMLUtil;
46
import org.w3c.dom.Document;
47
48
public class LayerBuilderTest extends NbTestCase {
49
50
    public LayerBuilderTest(String n) {
51
        super(n);
52
    }
53
54
    private Document doc;
55
    private LayerBuilder b;
56
57
    @Override
58
    protected void setUp() throws Exception {
59
        super.setUp();
60
        doc = XMLUtil.createDocument("filesystem", null, null, null);
61
        b = new LayerBuilder(doc);
62
        assertEquals("<filesystem/>", dump());
63
    }
64
65
    private String dump() throws IOException {
66
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
67
        XMLUtil.write(doc, baos, "UTF-8");
68
        return baos.toString("UTF-8").
69
                replace('"', '\'').
70
                replaceFirst("^<\\?xml version='1\\.0' encoding='UTF-8'\\?>\r?\n", "").
71
                replaceAll("\r?\n *", "");
72
    }
73
74
    public void testBasicFiles() throws Exception {
75
        b.file("Menu/File/x.instance").stringvalue("instanceClass", "some.X").write().
76
                file("Menu/Edit/y.instance").stringvalue("instanceClass", "some.Y").write();
77
        assertEquals("<filesystem><folder name='Menu'>" +
78
                "<folder name='File'><file name='x.instance'><attr name='instanceClass' stringvalue='some.X'/></file></folder>" +
79
                "<folder name='Edit'><file name='y.instance'><attr name='instanceClass' stringvalue='some.Y'/></file></folder>" +
80
                "</folder></filesystem>", dump());
81
    }
82
83
    public void testContent() throws Exception {
84
        b.file("a.txt").contents("some text here...").write().
85
                file("b.xml").url("/resources/b.xml").write();
86
        assertEquals("<filesystem><file name='a.txt'><![CDATA[some text here...]]></file>" +
87
                "<file name='b.xml' url='/resources/b.xml'/></filesystem>", dump());
88
    }
89
90
    public void testOverwriting() throws Exception {
91
        b.file("Menu/File/x.instance").stringvalue("instanceClass", "some.X").write();
92
        assertEquals("<filesystem><folder name='Menu'>" +
93
                "<folder name='File'><file name='x.instance'><attr name='instanceClass' stringvalue='some.X'/></file></folder>" +
94
                "</folder></filesystem>", dump());
95
        b.file("Menu/File/x.instance").write();
96
        assertEquals("<filesystem><folder name='Menu'>" +
97
                "<folder name='File'><file name='x.instance'/></folder>" +
98
                "</folder></filesystem>", dump());
99
    }
100
101
    public void testShadows() throws Exception {
102
        LayerBuilder.File orig = b.file("Actions/System/some-Action.instance");
103
        orig.write();
104
        b.shadowFile(orig.getPath(), "Menu/File", null).write();
105
        b.shadowFile(orig.getPath(), "Shortcuts", "C-F6").write();
106
        assertEquals("<filesystem>" +
107
                "<folder name='Actions'><folder name='System'><file name='some-Action.instance'/></folder></folder>" +
108
                "<folder name='Menu'><folder name='File'><file name='some-Action.shadow'>" +
109
                "<attr name='originalFile' stringvalue='Actions/System/some-Action.instance'/></file></folder></folder>" +
110
                "<folder name='Shortcuts'><file name='C-F6.shadow'>" +
111
                "<attr name='originalFile' stringvalue='Actions/System/some-Action.instance'/></file></folder>" +
112
                "</filesystem>", dump());
113
    }
114
115
    public void testSerialValue() throws Exception {
116
        b.file("x").serialvalue("a", new byte[] {0, 10, 100, (byte) 200}).write();
117
        assertEquals("<filesystem><file name='x'><attr name='a' serialvalue='000A64C8'/></file></filesystem>", dump());
118
    }
119
120
}

Return to bug 149136