();
+ int i = 0;
+ while (value.contains("{" + i)) {
+ params.add("arg" + i++);
+ }
+ String[] commentLines = comments.get(pkg).get(key);
+ if (commentLines != null) {
+ for (String comment : commentLines) {
+ Matcher m = Pattern.compile("# [{](\\d+)[}] - (.+)").matcher(comment);
+ if (m.matches()) {
+ i = Integer.parseInt(m.group(1));
+ String desc = m.group(2);
+ params.set(i, toIdentifier(desc));
+ method.append(" * @param ").append(params.get(i)).append(" ").append(toJavadoc(desc)).append("\n");
+ }
+ }
+ }
+ method.append(" * @return ").append(toJavadoc(value)).append("\n");
+ method.append(" */\n");
+ String name = toIdentifier(key);
+ method.append(" static String ").append(name).append("(");
+ boolean first = true;
+ for (String param : params) {
+ if (first) {
+ first = false;
+ } else {
+ method.append(", ");
+ }
+ method.append("Object ").append(param);
+ }
+ method.append(") {\n");
+ method.append(" return org.openide.util.NbBundle.getMessage(Bundle.class, \"").append(key).append("\"");
+ for (String param : params) {
+ method.append(", ").append(param);
+ }
+ method.append(");\n");
+ method.append(" }\n");
+ methods.put(name, method.toString());
+ }
+ 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 (String method : methods.values()) {
+ pw.print(method);
+ }
+ 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;
+ }
+ }
+ }
+
+ private String toJavadoc(String text) {
+ return text.replace("&", "&").replace("<", "<").replace("*/", "*/").replace("\n", "
").replace("@", "@");
+ }
+
+}
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;
@@ -81,6 +85,7 @@
*
* will in German locale look for the key {@code Foo.displayName} in
* {@code com/mycom/Bundle_de.properties} and then {@code com/mycom/Bundle.properties} (in that order).
+ * Usually however it is easiest to use {@link org.openide.util.NbBundle.Messages}.
*/
public class NbBundle extends Object {
@@ -771,6 +776,68 @@
}
/**
+ * 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.
+ * 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",
+ * "# {0} - file path",
+ * "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 file_path) {...}
+ * }
+ *
+ * and {@code Bundle.properties}:
+ *
+ * title=Bad File
+ * # {0} - file path
+ * message=The file {0} was invalid.
+ *
+ * @since org.openide.util 8.10
+ */
+ @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}.
+ * 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 '}.
+ * A line may also be a comment if it starts with {@code #}, which may be useful for translators;
+ * it is recommended to use the format {@code # {0} - summary of param}.
+ */
+ 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,205 @@
+/*
+ * 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.net.URL;
+import java.io.File;
+import java.io.ByteArrayOutputStream;
+import java.net.URLClassLoader;
+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);
+ }
+
+ private File src;
+ private File dest;
+ protected @Override void setUp() throws Exception {
+ clearWorkDir();
+ src = new File(getWorkDir(), "src");
+ dest = new File(getWorkDir(), "classes");
+ }
+
+ @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}",
+ "# {0} - input file",
+ "# {1} - pattern",
+ "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(), null, 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(), null, getWorkDir(), null, err));
+ assertTrue(err.toString(), err.toString().contains("uplicate"));
+ }
+
+ public void testDupeErrorAcrossClasses() throws Exception {
+ AnnotationProcessorTestUtils.makeSource(getWorkDir(), "p.C1", "@org.openide.util.NbBundle.Messages({\"k=v\"})", "class C1 {}");
+ AnnotationProcessorTestUtils.makeSource(getWorkDir(), "p.C2", "@org.openide.util.NbBundle.Messages({\"k=v\"})", "class C2 {}");
+ ByteArrayOutputStream err = new ByteArrayOutputStream();
+ assertFalse(AnnotationProcessorTestUtils.runJavac(getWorkDir(), null, getWorkDir(), null, err));
+ assertTrue(err.toString(), err.toString().contains("uplicate"));
+ }
+
+ 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(), null, 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(), null, getWorkDir(), null, err));
+ assertTrue(err.toString(), err.toString().contains("="));
+ }
+
+ public void testExistingBundle() throws Exception {
+ AnnotationProcessorTestUtils.makeSource(src, "p.C", "@org.openide.util.NbBundle.Messages(\"k=v\")", "class C {}");
+ TestFileUtils.writeFile(new File(src, "p/Bundle.properties"), "# original comment\nold=stuff\n");
+ assertTrue(AnnotationProcessorTestUtils.runJavac(src, null, dest, null, null));
+ assertEquals("k=v\n# original comment\nold=stuff\n", TestFileUtils.readFile(new File(dest, "p/Bundle.properties")));
+ // Also check that we can recompile:
+ assertTrue(AnnotationProcessorTestUtils.runJavac(src, null, dest, null, null));
+ assertEquals("k=v\n# original comment\nold=stuff\n", TestFileUtils.readFile(new File(dest, "p/Bundle.properties")));
+ }
+
+ public void testDupeErrorWithExistingBundle() throws Exception {
+ AnnotationProcessorTestUtils.makeSource(src, "p.C", "@org.openide.util.NbBundle.Messages(\"k=v\")", "class C {}");
+ TestFileUtils.writeFile(new File(src, "p/Bundle.properties"), "k=v\n");
+ ByteArrayOutputStream err = new ByteArrayOutputStream();
+ assertFalse(AnnotationProcessorTestUtils.runJavac(src, null, dest, null, err));
+ assertTrue(err.toString(), err.toString().contains("uplicate"));
+ }
+
+ public void testIncrementalCompilation() throws Exception {
+ AnnotationProcessorTestUtils.makeSource(src, "p.C1", "@org.openide.util.NbBundle.Messages(\"k1=v1\")", "public class C1 {public @Override String toString() {return Bundle.k1();}}");
+ AnnotationProcessorTestUtils.makeSource(src, "p.C2", "@org.openide.util.NbBundle.Messages(\"k2=v2\")", "public class C2 {public @Override String toString() {return Bundle.k2();}}");
+ assertTrue(AnnotationProcessorTestUtils.runJavac(src, null, dest, null, null));
+ ClassLoader l = new URLClassLoader(new URL[] {dest.toURI().toURL()});
+ assertEquals("v1", l.loadClass("p.C1").newInstance().toString());
+ assertEquals("v2", l.loadClass("p.C2").newInstance().toString());
+ AnnotationProcessorTestUtils.makeSource(src, "p.C1", "@org.openide.util.NbBundle.Messages(\"k1=v3\")", "public class C1 {public @Override String toString() {return Bundle.k1();}}");
+ assertTrue(AnnotationProcessorTestUtils.runJavac(src, "C1.java", dest, null, null));
+ l = new URLClassLoader(new URL[] {dest.toURI().toURL()});
+ assertEquals("v3", l.loadClass("p.C1").newInstance().toString());
+ assertEquals("v2", l.loadClass("p.C2").newInstance().toString());
+ }
+
+ public void testComments() throws Exception {
+ AnnotationProcessorTestUtils.makeSource(src, "p.C", "@org.openide.util.NbBundle.Messages({\"# Something skvělý to note.\", \"k=v\"})", "class C {}");
+ assertTrue(AnnotationProcessorTestUtils.runJavac(src, null, dest, null, null));
+ assertEquals("# Something skv\\u011bl\\u00fd to note.\nk=v\n", TestFileUtils.readFile(new File(dest, "p/Bundle.properties")));
+ // Also check that we can recompile:
+ assertTrue(AnnotationProcessorTestUtils.runJavac(src, null, dest, null, null));
+ assertEquals("# Something skv\\u011bl\\u00fd to note.\nk=v\n", TestFileUtils.readFile(new File(dest, "p/Bundle.properties")));
+ }
+
+}
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;