# HG changeset patch # Parent 57a8eb7c61136db95893da9ff5b776346e538959 #192750: @NbBundle.Keys. diff --git a/openide.util/src/org/netbeans/modules/openide/util/NbBundleKeysProcessor.java b/openide.util/src/org/netbeans/modules/openide/util/NbBundleKeysProcessor.java new file mode 100644 --- /dev/null +++ b/openide.util/src/org/netbeans/modules/openide/util/NbBundleKeysProcessor.java @@ -0,0 +1,219 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2010 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2010 Sun Microsystems, Inc. + */ + +package org.netbeans.modules.openide.util; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.Processor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.tools.Diagnostic.Kind; +import javax.tools.StandardLocation; +import org.openide.util.EditableProperties; +import org.openide.util.NbBundle; +import org.openide.util.Utilities; +import org.openide.util.lookup.ServiceProvider; + +@ServiceProvider(service = Processor.class) +@SupportedSourceVersion(SourceVersion.RELEASE_6) +public class NbBundleKeysProcessor extends AbstractProcessor { + + public @Override Set getSupportedAnnotationTypes() { + return Collections.singleton(NbBundle.Keys.class.getCanonicalName()); + } + + public @Override boolean process(Set annotations, RoundEnvironment roundEnv) { + if (roundEnv.processingOver()) { + return false; + } + Map>> pairs = new HashMap>>(); + Map>> originatingElements = new HashMap>>(); + for (Element e : roundEnv.getElementsAnnotatedWith(NbBundle.Keys.class)) { + for (String keyValue : e.getAnnotation(NbBundle.Keys.class).value()) { + int i = keyValue.indexOf('='); + if (i == -1) { + processingEnv.getMessager().printMessage(Kind.ERROR, "Bad key=value: " + keyValue, e); + continue; + } + String key = keyValue.substring(0, i); + if (!Utilities.isJavaIdentifier(key)) { + processingEnv.getMessager().printMessage(Kind.ERROR, "Not a Java identifier: " + key, e); + continue; + } + String value = keyValue.substring(i + 1); + String pkg = findPackage(e); + String basename = findBasename(e); + Map> pairsByPackage = pairs.get(pkg); + if (pairsByPackage == null) { + pairsByPackage = new HashMap>(); + pairs.put(pkg, pairsByPackage); + } + Map pairsByBasename = pairsByPackage.get(basename); + if (pairsByBasename == null) { + pairsByBasename = new TreeMap(); + pairsByPackage.put(basename, pairsByBasename); + } + if (pairsByBasename.containsKey(key)) { + processingEnv.getMessager().printMessage(Kind.ERROR, "Duplicate key: " + key, e); + continue; + } + pairsByBasename.put(key, value); + Map> originatingElementsByPackage = originatingElements.get(pkg); + if (originatingElementsByPackage == null) { + originatingElementsByPackage = new HashMap>(); + originatingElements.put(pkg, originatingElementsByPackage); + } + List originatingElementsByBasename = originatingElementsByPackage.get(basename); + if (originatingElementsByBasename == null) { + originatingElementsByBasename = new ArrayList(); + originatingElementsByPackage.put(basename, originatingElementsByBasename); + } + originatingElementsByBasename.add(e); + } + } + for (Map.Entry>> entry : pairs.entrySet()) { + String pkg = entry.getKey(); + for (Map.Entry> entry2 : entry.getValue().entrySet()) { + String basename = entry2.getKey(); + Map keysAndValues = entry2.getValue(); + Element[] elements = originatingElements.get(pkg).get(basename).toArray(new Element[0]); + try { + OutputStream os = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, pkg, basename + ".properties", elements).openOutputStream(); + try { + EditableProperties p = new EditableProperties(true); + p.putAll(keysAndValues); + p.store(os); + } finally { + os.close(); + } + String fqn = pkg + "." + basename; + Writer w = processingEnv.getFiler().createSourceFile(fqn, elements).openWriter(); + try { + PrintWriter pw = new PrintWriter(w); + pw.println("package " + pkg + ";"); + pw.println("/** Localizable strings for {@link " + pkg + (basename.equals("Bundle") ? "" : "." + basename.substring(0, basename.length() - 6)) + "}. */"); + pw.println("class " + basename + " {"); + for (Map.Entry entry3 : keysAndValues.entrySet()) { + String key = entry3.getKey(); + String value = entry3.getValue(); + pw.println(" /** " + value.replace("&", "&").replace("<", "<").replace("*/", "*/").replace("\n", "
").replace("@", "@") + " */"); + pw.print(" static String " + key + "("); + int params = 0; + while (value.contains("{" + params)) { + params++; + } + for (int i = 0; i < params; i++) { + if (i > 0) { + pw.print(", "); + } + pw.print("Object arg" + i); + } + pw.println(") {"); + if (params > 0) { + pw.print(" return java.text.MessageFormat.format($bundle().getString(\"" + key + "\")"); + for (int i = 0; i < params; i++) { + pw.print(", arg" + i); + } + pw.println(");"); + } else { + pw.println(" return $bundle().getString(\"" + key + "\");"); + } + pw.println(" }"); + } + pw.println(" private static java.util.ResourceBundle $bundle() {"); + pw.println(" return org.openide.util.NbBundle.getBundle(\"" + fqn + "\", java.util.Locale.getDefault(), " + basename + ".class.getClassLoader());"); + pw.println(" }"); + pw.println(" private void " + basename + "() {}"); + pw.println("}"); + pw.flush(); + pw.close(); + } finally { + w.close(); + } + } catch (IOException x) { + processingEnv.getMessager().printMessage(Kind.ERROR, "Could not generate files: " + x, elements[0]); + } + } + } + return true; + } + + private String findPackage(Element e) { + switch (e.getKind()) { + case PACKAGE: + return ((PackageElement) e).getQualifiedName().toString(); + default: + return findPackage(e.getEnclosingElement()); + } + } + + private String findBasename(Element e) { + switch (e.getKind()) { + case PACKAGE: + return "Bundle"; + default: + Element outer = e.getEnclosingElement(); + switch (outer.getKind()) { + case PACKAGE: + return e.getSimpleName() + "Bundle"; + default: + return findBasename(outer); + } + } + } + +} diff --git a/openide.util/src/org/openide/util/NbBundle.java b/openide.util/src/org/openide/util/NbBundle.java --- a/openide.util/src/org/openide/util/NbBundle.java +++ b/openide.util/src/org/openide/util/NbBundle.java @@ -46,6 +46,10 @@ import java.io.IOException; import java.io.InputStream; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.lang.ref.Reference; import java.lang.ref.WeakReference; import java.net.URL; @@ -771,6 +775,68 @@ } /** + * Creates a helper class with static definitions of bundle keys. + *
    + *
  • If placed on a top-level type {@code Something}, creates {@code SomethingBundle} in the same package. + *
  • If placed on a method, constructor, or nested class, creates/appends to the bundle corresponding to the top-level type. + * (It is an error to duplicate a key within a helper class, even if the duplicates are from different nested elements.) + *
  • If placed on a package (not recommended), creates a class {@code Bundle} in that package. + *
+ *

+ * Each key is placed in a {@code *.properties} file matching the helper class, + * and the helper class gets a method with the same name as the key + * which loads the key from the (possibly now localized) bundle using {@code NbBundle} with the default + * locale and the same class loader as the helper class. + * The method will have as many arguments (of type {@code Object}) as there are message format parameters. + *

+ *

Example usage:

+ *
+     * package some.where;
+     * import org.openide.util.NbBundle.Keys;
+     * import static some.where.SomethingBundle.*;
+     * import org.openide.DialogDisplayer;
+     * import org.openide.NotifyDescriptor;
+     * class Something {
+     *     @Keys({
+     *         "title=Bad File",
+     *         "message=The file {0} was invalid."
+     *     })
+     *     void showError(File f) {
+     *         NotifyDescriptor d = new NotifyDescriptor.Message(
+     *             message(f), NotifyDescriptor.ERROR_MESSAGE);
+     *         d.setTitle(title());
+     *         DialogDisplayer.getDefault().notify(d);
+     *     }
+     * }
+     * 
+ *

which generates during compilation {@code SomethingBundle.java}:

+ *
+     * class SomethingBundle {
+     *     static String title() {...}
+     *     static String message(Object arg0) {...}
+     * }
+     * 
+ *

and {@code SomethingBundle.properties}:

+ *
+     * title=Bad File
+     * message=The file {0} was invalid.
+     * 
+ * @since XXX + */ + @Retention(RetentionPolicy.SOURCE) + @Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR}) + public @interface Keys { + /** + * List of key/value pairs. + * Each must be of the form {@code key=Some Value} where {@code key} is a valid Java identifier. + * Anything is permitted in the value, including newlines. + * Unlike in a properties file, there should be no whitespace before the key or around the equals sign. + * Values containing {0} etc. are assumed to be message formats and so may need escapes for metacharacters such as {@code '}. + */ + String[] value(); + } + + /** * Do not use. * @deprecated Useless. */ diff --git a/openide.util/test/unit/src/org/netbeans/modules/openide/util/NbBundleKeysProcessorTest.java b/openide.util/test/unit/src/org/netbeans/modules/openide/util/NbBundleKeysProcessorTest.java new file mode 100644 --- /dev/null +++ b/openide.util/test/unit/src/org/netbeans/modules/openide/util/NbBundleKeysProcessorTest.java @@ -0,0 +1,119 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2010 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2010 Sun Microsystems, Inc. + */ + +package org.netbeans.modules.openide.util; + +import java.io.ByteArrayOutputStream; +import org.openide.util.test.AnnotationProcessorTestUtils; +import org.netbeans.junit.NbTestCase; +import org.openide.util.NbBundle.Keys; +import static org.netbeans.modules.openide.util.NbBundleKeysProcessorTestBundle.*; + +@Keys("k3=value #3") +public class NbBundleKeysProcessorTest extends NbTestCase { + + public NbBundleKeysProcessorTest(String n) { + super(n); + } + + @Keys({ + "k1=value #1", + "k2=value #2" + }) + public void testBasicUsage() throws Exception { + assertEquals("value #1", k1()); + assertEquals("value #2", k2()); + assertEquals("value #3", k3()); + } + + @Keys({ + "f1=problem with {0}", + "f2={0} did not match {1}", + "LBL_BuildMainProjectAction_Name=&Build {0,choice,-1#Main Project|0#Project|1#Project ({1})|1<{0} Projects}" + }) + public void testMessageFormats() throws Exception { + assertEquals("problem with stuff", f1("stuff")); + assertEquals("1 did not match 2", f2(1, 2)); + assertEquals("&Build Main Project", LBL_BuildMainProjectAction_Name(-1, "whatever")); + assertEquals("&Build Project", LBL_BuildMainProjectAction_Name(0, "whatever")); + assertEquals("&Build Project (whatever)", LBL_BuildMainProjectAction_Name(1, "whatever")); + assertEquals("&Build 2 Projects", LBL_BuildMainProjectAction_Name(2, "whatever")); + } + + @Keys({ + "s1=Don't worry", + "s2=Don''t worry about {0}", + "s3=@camera Say \"cheese\"", + "s4=", + "s5=Operators: +-*/=", + "s6=One thing.\nAnd another." + }) + public void testSpecialCharacters() throws Exception { + assertEquals("Don't worry", s1()); + assertEquals("Don't worry about me", s2("me")); + assertEquals("@camera Say \"cheese\"", s3()); + assertEquals("", s4()); + assertEquals("Operators: +-*/=", s5()); + assertEquals("One thing.\nAnd another.", s6()); + } + + public void testPackageKeys() throws Exception { + assertEquals("stuff", org.netbeans.modules.openide.util.Bundle.general()); + } + + public void testErrors() throws Exception { + clearWorkDir(); + AnnotationProcessorTestUtils.makeSource(getWorkDir(), "p.C1", "@org.openide.util.NbBundle.Keys({\"k=v1\", \"k=v2\"})", "class C1 {}"); + ByteArrayOutputStream err = new ByteArrayOutputStream(); + assertFalse(AnnotationProcessorTestUtils.runJavac(getWorkDir(), "C1.java", getWorkDir(), null, err)); + assertTrue(err.toString(), err.toString().contains("uplicate")); + AnnotationProcessorTestUtils.makeSource(getWorkDir(), "p.C2", "@org.openide.util.NbBundle.Keys(\"not a key=v\")", "class C2 {}"); + err = new ByteArrayOutputStream(); + assertFalse(AnnotationProcessorTestUtils.runJavac(getWorkDir(), "C2.java", getWorkDir(), null, err)); + assertTrue(err.toString(), err.toString().contains("Java identifier")); + AnnotationProcessorTestUtils.makeSource(getWorkDir(), "p.C3", "@org.openide.util.NbBundle.Keys(\"whatever\")", "class C3 {}"); + err = new ByteArrayOutputStream(); + assertFalse(AnnotationProcessorTestUtils.runJavac(getWorkDir(), "C3.java", getWorkDir(), null, err)); + assertTrue(err.toString(), err.toString().contains("=")); + } + +} diff --git a/openide.util/test/unit/src/org/netbeans/modules/openide/util/package-info.java b/openide.util/test/unit/src/org/netbeans/modules/openide/util/package-info.java new file mode 100644 --- /dev/null +++ b/openide.util/test/unit/src/org/netbeans/modules/openide/util/package-info.java @@ -0,0 +1,45 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2010 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2010 Sun Microsystems, Inc. + */ + +@Keys("general=stuff") +package org.netbeans.modules.openide.util; +import org.openide.util.NbBundle.Keys;