diff -r a0cd15490d3a settings/apichanges.xml --- a/settings/apichanges.xml Wed Mar 13 18:31:34 2013 +0100 +++ b/settings/apichanges.xml Thu Mar 14 12:27:21 2013 +0100 @@ -108,6 +108,20 @@ + + + Instances may have factory methods + + + + + + No need for the instances to have default constructor anymore. + One can tell the system to use different factory method. + + + + Factory methods can be private diff -r a0cd15490d3a settings/manifest.mf --- a/settings/manifest.mf Wed Mar 13 18:31:34 2013 +0100 +++ b/settings/manifest.mf Thu Mar 14 12:27:21 2013 +0100 @@ -3,5 +3,5 @@ OpenIDE-Module-Layer: org/netbeans/modules/settings/resources/mf-layer.xml OpenIDE-Module-Localizing-Bundle: org/netbeans/modules/settings/resources/Bundle.properties AutoUpdate-Essential-Module: true -OpenIDE-Module-Specification-Version: 1.39 +OpenIDE-Module-Specification-Version: 1.40 diff -r a0cd15490d3a settings/src/org/netbeans/api/settings/FactoryMethod.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/settings/src/org/netbeans/api/settings/FactoryMethod.java Thu Mar 14 12:27:21 2013 +0100 @@ -0,0 +1,69 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013 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 2013 Sun Microsystems, Inc. + */ +package org.netbeans.api.settings; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.netbeans.spi.settings.Convertor; + +/** Specifies an alternative factory method to use (rather than constructor) + * to create the instance. Use in orchestration with + * {@link ConvertAsProperties} or on any class that is processed by serializing + * {@link Convertor}s. + * + * @since 1.40 + * @author Jaroslav Tulach + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface FactoryMethod { + /** + * Name of factory method to use instead of default constructor. Sometimes, for + * example when dealing with singletons, it may be desirable to control how + * an instance of given class is created. In such case one can create a + * factory method (takes no arguments and returns instance of desired type) + * in the class annotated by {@link ConvertAsProperties} annotation. + */ + String value(); +} diff -r a0cd15490d3a settings/src/org/netbeans/modules/settings/convertors/ConvertorProcessor.java --- a/settings/src/org/netbeans/modules/settings/convertors/ConvertorProcessor.java Wed Mar 13 18:31:34 2013 +0100 +++ b/settings/src/org/netbeans/modules/settings/convertors/ConvertorProcessor.java Thu Mar 14 12:27:21 2013 +0100 @@ -53,6 +53,7 @@ import javax.annotation.processing.SupportedSourceVersion; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; @@ -61,6 +62,7 @@ import javax.lang.model.util.ElementFilter; import org.netbeans.api.settings.ConvertAsJavaBean; import org.netbeans.api.settings.ConvertAsProperties; +import org.netbeans.api.settings.FactoryMethod; import org.netbeans.modules.settings.Env; import org.openide.filesystems.annotations.LayerBuilder.File; import org.openide.filesystems.annotations.LayerGeneratingProcessor; @@ -78,7 +80,8 @@ public @Override Set getSupportedAnnotationTypes() { return new HashSet(Arrays.asList( ConvertAsProperties.class.getCanonicalName(), - ConvertAsJavaBean.class.getCanonicalName() + ConvertAsJavaBean.class.getCanonicalName(), + FactoryMethod.class.getCanonicalName() )); } @@ -158,6 +161,45 @@ } f.write(); } + + for (Element e : env.getElementsAnnotatedWith(FactoryMethod.class)) { + FactoryMethod m = e.getAnnotation(FactoryMethod.class); + if (m == null) { + continue; + } + + boolean found = false; + for (Element ch : e.getEnclosedElements()) { + if (ch.getKind() != ElementKind.METHOD) { + continue; + } + + if (!m.value().equals(ch.getSimpleName().toString())) { + continue; + } + + ExecutableElement ee = (ExecutableElement)ch; + + if (ee.getParameters().size() > 0) { + throw new LayerGenerationException("Factory method " + m.value() + " must have no parameters", ee); + } + + if (!ee.getModifiers().contains(Modifier.STATIC)) { + throw new LayerGenerationException("Factory method " + m.value() + " has to be static", ee); + } + + if (!processingEnv.getTypeUtils().isSameType(ee.getReturnType(), e.asType())) { + throw new LayerGenerationException("Factory method " + m.value() + " must return " + e.getSimpleName(), ee); + } + + found = true; + } + + if (!found) { + throw new LayerGenerationException("Method named " + m.value() + " was not found in this class", e); + } + } + return true; } diff -r a0cd15490d3a settings/src/org/netbeans/modules/settings/convertors/XMLPropertiesConvertor.java --- a/settings/src/org/netbeans/modules/settings/convertors/XMLPropertiesConvertor.java Wed Mar 13 18:31:34 2013 +0100 +++ b/settings/src/org/netbeans/modules/settings/convertors/XMLPropertiesConvertor.java Thu Mar 14 12:27:21 2013 +0100 @@ -225,7 +225,7 @@ Class c = getInstanceClass(); try { - return c.newInstance(); + return XMLSettingsSupport.newInstance(c); } catch (Exception ex) { // IllegalAccessException, InstantiationException IOException ioe = new IOException("Cannot create instance of " + c.getName()); // NOI18N ioe.initCause(ex); diff -r a0cd15490d3a settings/src/org/netbeans/modules/settings/convertors/XMLSettingsSupport.java --- a/settings/src/org/netbeans/modules/settings/convertors/XMLSettingsSupport.java Wed Mar 13 18:31:34 2013 +0100 +++ b/settings/src/org/netbeans/modules/settings/convertors/XMLSettingsSupport.java Thu Mar 14 12:27:21 2013 +0100 @@ -46,10 +46,12 @@ import java.io.*; import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; +import org.netbeans.api.settings.FactoryMethod; import org.openide.filesystems.*; import org.openide.modules.ModuleInfo; @@ -78,6 +80,26 @@ /** Logging for events in XML settings system. */ static final Logger err = Logger.getLogger(XMLSettingsSupport.class.getName()); // NOI18N + + static Object newInstance(Class clazz) throws IllegalArgumentException, + InstantiationException, NoSuchMethodException, InvocationTargetException, + IllegalAccessException { + FactoryMethod fm = clazz.getAnnotation(FactoryMethod.class); + if (fm != null) { + Method method; + try { + method = clazz.getMethod(fm.value()); + } catch (NoSuchMethodException ex) { + method = clazz.getDeclaredMethod(fm.value()); + } + method.setAccessible(true); + return method.invoke(null); + } + Constructor c = clazz.getDeclaredConstructor(); + c.setAccessible(true); + return c.newInstance(); + } + /** Store instanceof elements. * @param classes everything what class extends or implements @@ -603,9 +625,7 @@ } } else { try { - Constructor c = clazz.getDeclaredConstructor(); - c.setAccessible(true); - inst = c.newInstance(); + inst = newInstance(clazz); } catch (Exception ex) { IOException ioe = new IOException(); ioe.initCause(ex); diff -r a0cd15490d3a settings/test/unit/src/org/netbeans/modules/settings/convertors/FactoryMethodTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/settings/test/unit/src/org/netbeans/modules/settings/convertors/FactoryMethodTest.java Thu Mar 14 12:27:21 2013 +0100 @@ -0,0 +1,152 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013 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 2013 Sun Microsystems, Inc. + */ +package org.netbeans.modules.settings.convertors; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import org.netbeans.junit.NbTestCase; +import org.openide.util.test.AnnotationProcessorTestUtils; + +/** + * + * @author Jaroslav Tulach + */ +public class FactoryMethodTest extends NbTestCase { + private File src; + private File dst; + + public FactoryMethodTest(String name) { + super(name); + } + + @Override + protected void setUp() throws Exception { + clearWorkDir(); + + src = new File(getWorkDir(), "src"); + src.mkdirs(); + + dst = new File(getWorkDir(), "dst"); + dst.mkdirs(); + } + + + + public void testMethodOfGivenNameMustExists() throws IOException { + AnnotationProcessorTestUtils.makeSource(src, "tst.Bean", + "import org.netbeans.api.settings.FactoryMethod;", + "@FactoryMethod(\"create\")", + "public class Bean {", + "}" + ); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + boolean res = AnnotationProcessorTestUtils.runJavac(src, null, dst, null, os); + + assertFalse("Compilation fails", res); + String out = new String(os.toByteArray(), "UTF-8"); + + if (out.indexOf(" Method named create was not found") == -1) { + fail("Should warn about missing method\n" + out); + } + } + + public void testMethodOfGivenNameMustHaveNoArguments() throws IOException { + AnnotationProcessorTestUtils.makeSource(src, "tst.Bean", + "import org.netbeans.api.settings.FactoryMethod;", + "@FactoryMethod(\"create\")", + "public class Bean {", + " public static Bean create(boolean x) { return null; }", + "}" + ); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + boolean res = AnnotationProcessorTestUtils.runJavac(src, null, dst, null, os); + + assertFalse("Compilation fails", res); + String out = new String(os.toByteArray(), "UTF-8"); + + if (out.indexOf("no parameters") == -1) { + fail("Should warn about arguments of the method\n" + out); + } + } + public void testMethodOfGivenNameMustBeStatic() throws IOException { + AnnotationProcessorTestUtils.makeSource(src, "tst.Bean", + "import org.netbeans.api.settings.FactoryMethod;", + "@FactoryMethod(\"create\")", + "public class Bean {", + " public Bean create() { return null; }", + "}" + ); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + boolean res = AnnotationProcessorTestUtils.runJavac(src, null, dst, null, os); + + assertFalse("Compilation fails", res); + String out = new String(os.toByteArray(), "UTF-8"); + + if (out.indexOf("has to be static") == -1) { + fail("Should warn method not being static\n" + out); + } + } + + public void testMethodOfGivenNameMustHaveTheSameReturnType() throws IOException { + AnnotationProcessorTestUtils.makeSource(src, "tst.Bean", + "import org.netbeans.api.settings.FactoryMethod;", + "@FactoryMethod(\"create\")", + "public class Bean {", + " public static String create() { return null; }", + "}" + ); + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + boolean res = AnnotationProcessorTestUtils.runJavac(src, null, dst, null, os); + + assertFalse("Compilation fails", res); + String out = new String(os.toByteArray(), "UTF-8"); + + if (out.indexOf("must return Bean") == -1) { + fail("Should about wrong return type\n" + out); + } + } +} diff -r a0cd15490d3a settings/test/unit/src/org/netbeans/modules/settings/convertors/SerialDataConvertorTest.java --- a/settings/test/unit/src/org/netbeans/modules/settings/convertors/SerialDataConvertorTest.java Wed Mar 13 18:31:34 2013 +0100 +++ b/settings/test/unit/src/org/netbeans/modules/settings/convertors/SerialDataConvertorTest.java Thu Mar 14 12:27:21 2013 +0100 @@ -56,6 +56,7 @@ import java.util.*; import junit.framework.Test; import junit.framework.TestSuite; +import org.netbeans.api.settings.FactoryMethod; import org.netbeans.junit.*; @@ -657,4 +658,36 @@ assertNotNull("Missing InstanceCookie", ic); assertNotNull("the persisted object cannot be read", ic.instanceCreate()); } + + public void testFactoryMethod() throws IOException, ClassNotFoundException { + DataFolder df = DataFolder.findFolder(FileUtil.getConfigRoot().createFolder("testFactoryMethod")); + FileObject fo = df.getPrimaryFile().createData("test.settings"); + OutputStream os = fo.getOutputStream(); + os.write(( +"\n" + +"\n" + +"\n" + +" \n" + +"\n" + ).getBytes("UTF-8")); + os.close(); + + InstanceCookie ido = DataObject.find(fo).getCookie(InstanceCookie.class); + FactoryBase fb = (FactoryBase) ido.instanceCreate(); + assertNotNull("Re-created OK!", fb); + } + + @FactoryMethod("create") + public static class FactoryBase implements Serializable { + private FactoryBase() { + throw new IllegalStateException("Don't call my default constructor"); + } + + FactoryBase(boolean ok) { + } + + static FactoryBase create() { + return new FactoryBase(true); + } + } } diff -r a0cd15490d3a settings/test/unit/src/org/netbeans/modules/settings/convertors/XMLPropertiesConvertorTest.java --- a/settings/test/unit/src/org/netbeans/modules/settings/convertors/XMLPropertiesConvertorTest.java Wed Mar 13 18:31:34 2013 +0100 +++ b/settings/test/unit/src/org/netbeans/modules/settings/convertors/XMLPropertiesConvertorTest.java Thu Mar 14 12:27:21 2013 +0100 @@ -45,6 +45,13 @@ package org.netbeans.modules.settings.convertors; import java.io.*; +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.util.Properties; +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertNotNull; +import org.netbeans.api.settings.ConvertAsProperties; +import org.netbeans.api.settings.FactoryMethod; import org.netbeans.junit.NbTestCase; import org.netbeans.junit.RandomlyFails; @@ -58,6 +65,7 @@ import org.openide.filesystems.FileObject; import org.openide.filesystems.FileSystem; import org.openide.filesystems.FileUtil; +import org.openide.filesystems.Repository; import org.openide.filesystems.XMLFileSystem; import org.openide.loaders.*; import org.openide.modules.ModuleInfo; @@ -352,6 +360,50 @@ assertEquals("Listener not deregistered", 0, obj.getListenerCount()); assertNull(filename + ".settings was not deleted!", root.getFileObject(filename)); } + + public void testFactoryMethod() throws Exception { + FileObject dtdFO = Repository.getDefault().getDefaultFileSystem(). + findResource("/xml/lookups/abc/x.instance"); + assertNotNull("Provider not found", dtdFO); + Convertor c = XMLPropertiesConvertor.create(dtdFO); + assertNotNull("Convertor created", c); + + DataFolder folder = DataFolder.findFolder(root); + + FactoryBase inst = FactoryBase.create(); + InstanceDataObject ido = InstanceDataObject.create(folder, null, inst, null); + + assertSame("Instance is there", inst, ido.instanceCreate()); + + Reference ref = new WeakReference(inst); + inst = null; + + assertGC("Instance can disappear", ref); + + Object obj = ido.instanceCreate(); + assertEquals("One can re-create it without default constructor", FactoryBase.class, obj.getClass()); + } + + @ConvertAsProperties(dtd = "-//abc/x") + @FactoryMethod("create") + public static class FactoryBase implements Serializable { + public FactoryBase() { + throw new IllegalStateException("Don't call my default constructor"); + } + + FactoryBase(boolean ok) { + } + + public static FactoryBase create() { + return new FactoryBase(true); + } + + void readProperties(Properties p) { + } + + void writeProperties(Properties p) { + } + } public void testModuleDisabling() throws Exception { FileObject dtd = FileUtil.getConfigFile("xml/lookups/NetBeans_org_netbeans_modules_settings_testModuleDisabling/DTD_XML_FooSetting_1_0.instance");