# HG changeset patch # Parent c16ef76ef0c7c6a834ddfeff5d2546ae3ae11dae #192750: @NbBundle.Messages. diff --git a/openide.util/src/org/netbeans/modules/openide/util/NbBundleProcessor.java b/openide.util/src/org/netbeans/modules/openide/util/NbBundleProcessor.java new file mode 100644 --- /dev/null +++ b/openide.util/src/org/netbeans/modules/openide/util/NbBundleProcessor.java @@ -0,0 +1,209 @@ +/* + * 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.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +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 NbBundleProcessor extends AbstractProcessor { + + public @Override Set getSupportedAnnotationTypes() { + return Collections.singleton(NbBundle.Messages.class.getCanonicalName()); + } + + public @Override boolean process(Set annotations, RoundEnvironment roundEnv) { + if (roundEnv.processingOver()) { + return false; + } + Map> pairs = new HashMap>(); + Map> identifiers = new HashMap>(); + Map> originatingElements = new HashMap>(); + for (Element e : roundEnv.getElementsAnnotatedWith(NbBundle.Messages.class)) { + String pkg = findPackage(e); + try { + processingEnv.getFiler().getResource(StandardLocation.SOURCE_PATH, pkg, "Bundle.properties").openInputStream().close(); + processingEnv.getMessager().printMessage(Kind.ERROR, "Bundle.properties already exists in " + pkg, e); + continue; + } catch (IOException x) { + // fine, does not exist + } + for (String keyValue : e.getAnnotation(NbBundle.Messages.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 (key.isEmpty() || !key.equals(key.trim())) { + processingEnv.getMessager().printMessage(Kind.ERROR, "Whitespace not permitted in key: " + keyValue, e); + continue; + } + Set identifiersByPackage = identifiers.get(pkg); + if (identifiersByPackage == null) { + identifiersByPackage = new HashSet(); + identifiers.put(pkg, identifiersByPackage); + } + if (!identifiersByPackage.add(toIdentifier(key))) { + processingEnv.getMessager().printMessage(Kind.ERROR, "Duplicate key: " + key, e); + continue; + } + String value = keyValue.substring(i + 1); + Map pairsByPackage = pairs.get(pkg); + if (pairsByPackage == null) { + pairsByPackage = new HashMap(); + pairs.put(pkg, pairsByPackage); + } + pairsByPackage.put(key, value); + List originatingElementsByPackage = originatingElements.get(pkg); + if (originatingElementsByPackage == null) { + originatingElementsByPackage = new ArrayList(); + originatingElements.put(pkg, originatingElementsByPackage); + } + originatingElementsByPackage.add(e); + } + } + for (Map.Entry> entry : pairs.entrySet()) { + String pkg = entry.getKey(); + Map keysAndValues = entry.getValue(); + Element[] elements = originatingElements.get(pkg).toArray(new Element[0]); + try { + OutputStream os = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, pkg, "Bundle.properties", elements).openOutputStream(); + try { + EditableProperties p = new EditableProperties(true); + p.putAll(keysAndValues); + p.store(os); + } finally { + os.close(); + } + String fqn = pkg + ".Bundle"; + 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 + "}. */"); + pw.println("class Bundle {"); + for (Map.Entry entry2 : keysAndValues.entrySet()) { + String key = entry2.getKey(); + String value = entry2.getValue(); + pw.println(" /** " + value.replace("&", "&").replace("<", "<").replace("*/", "*/").replace("\n", "
").replace("@", "@") + " */"); + pw.print(" static String " + toIdentifier(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(") {"); + pw.print(" return org.openide.util.NbBundle.getMessage(Bundle.class, \"" + key + "\""); + for (int i = 0; i < params; i++) { + pw.print(", arg" + i); + } + pw.println(");"); + pw.println(" }"); + } + pw.println(" private void Bundle() {}"); + 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 toIdentifier(String key) { + if (Utilities.isJavaIdentifier(key)) { + return key; + } else { + String i = key.replaceAll("[^\\p{javaJavaIdentifierPart}]", "_"); + if (Utilities.isJavaIdentifier(i)) { + return i; + } else { + return "_" + i; + } + } + } + +} 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,65 @@ } /** + * Creates a helper class with static definitions of bundle keys. + *

+ * The generated class will be called {@code Bundle} and be in the same package. + * Each key is placed in a {@code Bundle.properties} file also in the same package, + * and the helper class gets a method with the same name as the key + * (converted to a valid Java identifier as needed) + * which loads the key from the (possibly now localized) bundle using {@link NbBundle#getMessage(Class, String)}. + * The method will have as many arguments (of type {@code Object}) as there are message format parameters. + *

+ *

It is an error to duplicate a key within a package, even if the duplicates are from different compilation units.

+ *

Since a file {@code Bundle.properties} is generated, if converting code to use this annotation, you must do a whole package at a time.

+ *

Example usage:

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

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

+ *
+     * class Bundle {
+     *     static String dialog_title() {...}
+     *     static String dialog_message(Object arg0) {...}
+     * }
+     * 
+ *

and {@code Bundle.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 Messages { + /** + * 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/NbBundleProcessorTest.java b/openide.util/test/unit/src/org/netbeans/modules/openide/util/NbBundleProcessorTest.java new file mode 100644 --- /dev/null +++ b/openide.util/test/unit/src/org/netbeans/modules/openide/util/NbBundleProcessorTest.java @@ -0,0 +1,160 @@ +/* + * 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.File; +import java.io.ByteArrayOutputStream; +import org.openide.util.test.AnnotationProcessorTestUtils; +import org.netbeans.junit.NbTestCase; +import org.openide.util.NbBundle.Messages; +import org.openide.util.test.TestFileUtils; +import static org.netbeans.modules.openide.util.Bundle.*; + +@Messages("k3=value #3") +public class NbBundleProcessorTest extends NbTestCase { + + public NbBundleProcessorTest(String n) { + super(n); + } + + protected @Override void setUp() throws Exception { + clearWorkDir(); + } + + @Messages({ + "k1=value #1", + "k2=value #2" + }) + public void testBasicUsage() throws Exception { + assertEquals("value #1", k1()); + assertEquals("value #2", k2()); + assertEquals("value #3", k3()); + } + + @Messages({ + "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")); + } + + @Messages({ + "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()); + } + + @Messages({ + "some key=some value", + "public=property", + "2+2=4" + }) + public void testNonIdentifierKeys() throws Exception { + assertEquals("some value", some_key()); + assertEquals("property", _public()); + assertEquals("4", _2_2()); + } + + public void testPackageKeys() throws Exception { + assertEquals("stuff", org.netbeans.modules.openide.util.Bundle.general()); + } + + public void testDupeErrorSimple() throws Exception { + AnnotationProcessorTestUtils.makeSource(getWorkDir(), "p.C", "@org.openide.util.NbBundle.Messages({\"k=v1\", \"k=v2\"})", "class C {}"); + ByteArrayOutputStream err = new ByteArrayOutputStream(); + assertFalse(AnnotationProcessorTestUtils.runJavac(getWorkDir(), "C.java", getWorkDir(), null, err)); + assertTrue(err.toString(), err.toString().contains("uplicate")); + } + + public void testDupeErrorByIdentifier() throws Exception { + AnnotationProcessorTestUtils.makeSource(getWorkDir(), "p.C", "@org.openide.util.NbBundle.Messages({\"k.=v1\", \"k,=v2\"})", "class C {}"); + ByteArrayOutputStream err = new ByteArrayOutputStream(); + assertFalse(AnnotationProcessorTestUtils.runJavac(getWorkDir(), "C.java", getWorkDir(), null, err)); + assertTrue(err.toString(), err.toString().contains("uplicate")); + } + + // XXX key conflicts among multiple classes in same package + + public void testNoEqualsError() throws Exception { + AnnotationProcessorTestUtils.makeSource(getWorkDir(), "p.C", "@org.openide.util.NbBundle.Messages(\"whatever\")", "class C {}"); + ByteArrayOutputStream err = new ByteArrayOutputStream(); + assertFalse(AnnotationProcessorTestUtils.runJavac(getWorkDir(), "C.java", getWorkDir(), null, err)); + assertTrue(err.toString(), err.toString().contains("=")); + } + + public void testWhitespaceError() throws Exception { + AnnotationProcessorTestUtils.makeSource(getWorkDir(), "p.C", "@org.openide.util.NbBundle.Messages(\"key = value\")", "class C {}"); + ByteArrayOutputStream err = new ByteArrayOutputStream(); + assertFalse(AnnotationProcessorTestUtils.runJavac(getWorkDir(), "C.java", getWorkDir(), null, err)); + assertTrue(err.toString(), err.toString().contains("=")); + } + + public void testExistingBundle() throws Exception { + AnnotationProcessorTestUtils.makeSource(getWorkDir(), "p.C", "@org.openide.util.NbBundle.Messages(\"k=v\")", "class C {}"); + TestFileUtils.writeFile(new File(getWorkDir(), "p/Bundle.properties"), "old=stuff"); + ByteArrayOutputStream err = new ByteArrayOutputStream(); + assertFalse(AnnotationProcessorTestUtils.runJavac(getWorkDir(), "C.java", getWorkDir(), null, err)); + assertTrue(err.toString(), err.toString().contains("Bundle.properties")); + } + + // XXX test incremental compilation of multiple classes in the same package + +} 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. + */ + +@Messages("general=stuff") +package org.netbeans.modules.openide.util; +import org.openide.util.NbBundle.Messages;