Commits

Jan Lahoda committed c7ace12

Ability to specify hint settings in a separate configuration file, the command line tool is able to use that file to configure itself.

Comments (0)

Files changed (9)

cmdline/tool/nbproject/genfiles.properties

 build.xml.stylesheet.CRC32=a56c6a5b@1.44
 # This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml.
 # Do not edit this file. You may delete it but then the IDE will never regenerate such files for you.
-nbproject/build-impl.xml.data.CRC32=73e63f71
+nbproject/build-impl.xml.data.CRC32=61136162
 nbproject/build-impl.xml.script.CRC32=08f1fb11
-nbproject/build-impl.xml.stylesheet.CRC32=238281d1@2.49
+nbproject/build-impl.xml.stylesheet.CRC32=238281d1@2.50

cmdline/tool/nbproject/project.xml

                     </run-dependency>
                 </dependency>
                 <dependency>
+                    <code-name-base>org.netbeans.modules.jackpot30.ui</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <implementation-version/>
+                    </run-dependency>
+                </dependency>
+                <dependency>
                     <code-name-base>org.netbeans.modules.java.hints</code-name-base>
                     <run-dependency>
                         <release-version>1</release-version>

cmdline/tool/src/org/netbeans/modules/jackpot30/cmdline/Main.java

 import org.netbeans.api.java.source.CompilationController;
 import org.netbeans.api.java.source.ModificationResult;
 import org.netbeans.core.startup.MainLookup;
+import org.netbeans.modules.jackpot30.ui.settings.XMLHintPreferences;
 import org.netbeans.modules.java.hints.providers.spi.HintDescription;
 import org.netbeans.modules.java.hints.providers.spi.HintMetadata;
 import org.netbeans.modules.java.hints.spiimpl.MessageImpl;
 import org.openide.filesystems.FileStateInvalidException;
 import org.openide.filesystems.FileUtil;
 import org.openide.util.Lookup;
+import org.openide.util.NbPreferences;
 import org.openide.util.RequestProcessor;
 import org.openide.util.lookup.Lookups;
 import org.openide.util.lookup.ProxyLookup;
  */
 public class Main {
 
+    private static final String OPTION_APPLY = "apply";
     private static final String OPTION_NO_APPLY = "no-apply";
     private static final String SOURCE_LEVEL_DEFAULT = "1.7";
     private static final String ACCEPTABLE_SOURCE_LEVEL_PATTERN = "(1\\.)?[2-9][0-9]*";
         ArgumentAcceptingOptionSpec<File> sourcepath = parser.accepts("sourcepath", "sourcepath").withRequiredArg().withValuesSeparatedBy(File.pathSeparatorChar).ofType(File.class);
         ArgumentAcceptingOptionSpec<File> cache = parser.accepts("cache", "a cache directory to store working data").withRequiredArg().ofType(File.class);
         ArgumentAcceptingOptionSpec<File> out = parser.accepts("out", "output diff").withRequiredArg().ofType(File.class);
+        ArgumentAcceptingOptionSpec<File> configFile = parser.accepts("config-file", "configuration file").withRequiredArg().ofType(File.class);
         ArgumentAcceptingOptionSpec<String> hint = parser.accepts("hint", "hint name").withRequiredArg().ofType(String.class);
         ArgumentAcceptingOptionSpec<String> config = parser.accepts("config", "configurations").withRequiredArg().ofType(String.class);
         ArgumentAcceptingOptionSpec<String> source = parser.accepts("source", "source level").withRequiredArg().ofType(String.class).defaultsTo(SOURCE_LEVEL_DEFAULT);
         parser.accepts("debug", "enable debugging loggers");
         parser.accepts("help", "prints this help");
         parser.accepts(OPTION_NO_APPLY, "do not apply changes - only print locations were the hint would be applied");
+        parser.accepts(OPTION_APPLY, "apply changes");
 
         OptionSet parsed;
 
                 return 0;
             }
 
+            Preferences settingsFromConfigFile;
+            Preferences hintSettings;
+            boolean apply;
+
+            if (parsed.has(configFile)) {
+                settingsFromConfigFile = XMLHintPreferences.from(parsed.valueOf(configFile));
+                hintSettings = settingsFromConfigFile.node("settings");
+                apply = settingsFromConfigFile.getBoolean("apply", false);
+            } else {
+                settingsFromConfigFile = null;
+                hintSettings = NbPreferences.root().node("tempSettings");
+                apply = false;
+            }
+
             if (parsed.has(hint)) {
-                hints = findHints(sourceCP, binaryCP, parsed.valueOf(hint));
+                if (settingsFromConfigFile != null) {
+                    System.err.println("cannot specify --hint and --configFile together");
+                    return 1;
+                }
+                hints = findHints(sourceCP, binaryCP, parsed.valueOf(hint), hintSettings);
+            } else if (settingsFromConfigFile == null) {
+                hints = allHints(sourceCP, binaryCP, hintSettings);
             } else {
-                hints = allHints(sourceCP, binaryCP);
+                assert settingsFromConfigFile != null;
+                hints = readHints(sourceCP, binaryCP, hintSettings, settingsFromConfigFile.getBoolean("runDeclarative", true));
             }
 
             if (!hints.iterator().hasNext()) {
                 System.err.println("unrecognized source level specification: " + sourceLevel);
                 return 1;
             }
+
+            if (parsed.has(OPTION_NO_APPLY)) {
+                apply = false;
+            } else if (parsed.has(OPTION_APPLY)) {
+                apply = true;
+            }
             
             try {
                 MainLookup.register(new ClassPathProviderImpl(bootCP, compileCP, sourceCP));
                 MainLookup.register(new JavaPathRecognizer());
                 MainLookup.register(new SourceLevelQueryImpl(sourceCP, sourceLevel));
+
+                setHintPreferences(hintSettings);
                 
                 ProgressHandleWrapper progress = parsed.has("progress") ? new ProgressHandleWrapper(new ConsoleProgressHandleAbstraction(), 1) : new ProgressHandleWrapper(1);
 
-                if (parsed.has(OPTION_NO_APPLY)) {
+                if (apply) {
+                    apply(hints, rootFolders.toArray(new Folder[0]), progress, parsed.valueOf(out));
+                } else {
                     findOccurrences(hints, rootFolders.toArray(new Folder[0]), progress, parsed.valueOf(out));
-                } else {
-                    apply(hints, rootFolders.toArray(new Folder[0]), progress, parsed.valueOf(out));
                 }
             } catch (Throwable e) {
                 e.printStackTrace();
         return 0;
     }
 
+    private static void setHintPreferences(final Preferences prefs) {
+        HintsSettings.setPreferencesOverride(new Map<String, Preferences>() {
+            @Override public int size() {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+            @Override public boolean isEmpty() {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+            @Override public boolean containsKey(Object key) {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+            @Override public boolean containsValue(Object value) {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+            @Override public Preferences get(Object key) {
+                Preferences res = prefs.node((String) key);
+
+                if (res.get("enabled", null) == null) {
+                    res.putBoolean("enabled", false);
+                }
+                
+                return res;
+            }
+            @Override public Preferences put(String key, Preferences value) {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+            @Override public Preferences remove(Object key) {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+            @Override public void putAll(Map<? extends String, ? extends Preferences> m) {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+            @Override public void clear() {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+            @Override public Set<String> keySet() {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+            @Override public Collection<Preferences> values() {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+            @Override public Set<Entry<String, Preferences>> entrySet() {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+        });
+    }
+
     private static Map<HintMetadata, Collection<? extends HintDescription>> listHints(ClassPath sourceFrom, ClassPath binaryFrom) {
         Map<HintMetadata, Collection<? extends HintDescription>> result = new HashMap<HintMetadata, Collection<? extends HintDescription>>();
 
         return result;
     }
     
-    private static Iterable<? extends HintDescription> findHints(ClassPath sourceFrom, ClassPath binaryFrom, String name) {
+    private static Iterable<? extends HintDescription> findHints(ClassPath sourceFrom, ClassPath binaryFrom, String name, Preferences toEnableIn) {
         List<HintDescription> descs = new LinkedList<HintDescription>();
 
         for (Entry<HintMetadata, Collection<? extends HintDescription>> e : listHints(sourceFrom, binaryFrom).entrySet()) {
             if (e.getKey().displayName.equals(name)) {
                 descs.addAll(e.getValue());
+                HintsSettings.setEnabled(toEnableIn.node(e.getKey().id), true);
             }
         }
 
         return descs;
     }
 
-    private static Iterable<? extends HintDescription> allHints(ClassPath sourceFrom, ClassPath binaryFrom) {
+    private static Iterable<? extends HintDescription> allHints(ClassPath sourceFrom, ClassPath binaryFrom, Preferences toEnableIn) {
         List<HintDescription> descs = new LinkedList<HintDescription>();
 
         for (Entry<HintMetadata, Collection<? extends HintDescription>> e : listHints(sourceFrom, binaryFrom).entrySet()) {
             if (e.getKey().kind != Kind.INSPECTION) continue;
             if (!e.getKey().enabled) continue;
             descs.addAll(e.getValue());
+            HintsSettings.setEnabled(toEnableIn.node(e.getKey().id), true);
+        }
+
+        return descs;
+    }
+
+    private static Iterable<? extends HintDescription> readHints(ClassPath sourceFrom, ClassPath binaryFrom, Preferences toEnableIn, boolean declarative) {
+        Map<HintMetadata, ? extends Collection<? extends HintDescription>> hardcoded = RulesManager.getInstance().readHints(null, Arrays.<ClassPath>asList(), null);
+        Map<HintMetadata, ? extends Collection<? extends HintDescription>> all = declarative ? RulesManager.getInstance().readHints(null, Arrays.asList(sourceFrom, binaryFrom), null) : hardcoded;
+        List<HintDescription> descs = new LinkedList<HintDescription>();
+
+        for (Entry<HintMetadata, ? extends Collection<? extends HintDescription>> entry: all.entrySet()) {
+            if (hardcoded.containsKey(entry.getKey())) {
+                if (HintsSettings.isEnabled(toEnableIn.node(entry.getKey().id), entry.getKey().enabled)) {
+                    descs.addAll(entry.getValue());
+                }
+            } else {
+                assert declarative;
+                descs.addAll(entry.getValue());
+            }
         }
 
         return descs;

cmdline/tool/test/unit/src/org/netbeans/modules/jackpot30/cmdline/MainTest.java

                       "    }\n" +
                       "}\n",
                       null,
+                      "--apply",
                       "--hint",
                       "Usage of .size() == 0");
     }
                       "    }\n" +
                       "}\n",
                       null,
+                      "--apply",
                       "--hint",
                       "Usage of .size() == 0",
                       "--source",
                       "1.6");
     }
 
+    public void testConfigurationFile() throws Exception {
+        String golden =
+            "package test;\n" +
+            "public class Test {\n" +
+            "    private void test(java.util.Collection c) {\n" +
+            "        boolean b = c.isEmpty();\n" +
+            "    }\n" +
+            "}\n";
+
+        doRunCompiler(golden,
+                      null,
+                      null,
+                      "src/test/Test.java",
+                      "package test;\n" +
+                      "public class Test {\n" +
+                      "    private void test(java.util.Collection c) {\n" +
+                      "        boolean b = c.size() == 0;\n" +
+                      "    }\n" +
+                      "}\n",
+                      "settings.xml",
+                      "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+                      "<hints apply=\"true\">\n" +
+                      "    <settings>\n" +
+                      "        <org.netbeans.modules.java.hints.perf.SizeEqualsZero check.not.equals=\"false\" enabled=\"true\" hintSeverity=\"VERIFIER\"/>\n" +
+                      "    </settings>\n" +
+                      "</hints>\n",
+                      null,
+                      "--config-file",
+                      "${workdir}/settings.xml",
+                      "--source",
+                      "1.6");
+    }
+
+    public void testConfigurationFileCmdLineOverride() throws Exception {
+        String golden =
+            "package test;\n" +
+            "public class Test {\n" +
+            "    private void test(java.util.Collection c) {\n" +
+            "        boolean b = c.size() == 0;\n" +
+            "    }\n" +
+            "}\n";
+
+        doRunCompiler(golden,
+                      "${workdir}/src/test/Test.java:4: warning: Usage of .size() == 0 can be replaced with .isEmpty()\n" +
+                      "        boolean b = c.size() == 0;\n" +
+                      "                    ^\n",
+                      null,
+                      "src/test/Test.java",
+                      "package test;\n" +
+                      "public class Test {\n" +
+                      "    private void test(java.util.Collection c) {\n" +
+                      "        boolean b = c.size() == 0;\n" +
+                      "    }\n" +
+                      "}\n",
+                      "settings.xml",
+                      "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+                      "<hints apply=\"true\">\n" +
+                      "    <settings>\n" +
+                      "        <org.netbeans.modules.java.hints.perf.SizeEqualsZero check.not.equals=\"false\" enabled=\"true\" hintSeverity=\"VERIFIER\"/>\n" +
+                      "    </settings>\n" +
+                      "</hints>\n",
+                      null,
+                      "--config-file",
+                      "${workdir}/settings.xml",
+                      "--source",
+                      "1.6",
+                      "--no-apply");
+    }
+
     private void doRunCompiler(String golden, String stdOut, String stdErr, String... fileContentAndExtraOptions) throws Exception {
         List<String> fileAndContent = new LinkedList<String>();
         List<String> extraOptions = new LinkedList<String>();
 
         options.add("--cache");
         options.add("/tmp/cachex");
-        options.addAll(extraOptions);
+        for (String extraOption : extraOptions) {
+            options.add(extraOption.replace("${workdir}", wd.getAbsolutePath()));
+        }
         options.add(wd.getAbsolutePath());
 
         String[] output = new String[2];

language/ide/ui/nbproject/genfiles.properties

-build.xml.data.CRC32=8131ef5a
+build.xml.data.CRC32=8268870d
 build.xml.script.CRC32=4ac38acd
 build.xml.stylesheet.CRC32=a56c6a5b@2.49
 # This file is used by a NetBeans-based IDE to track changes in generated files such as build-impl.xml.
 # Do not edit this file. You may delete it but then the IDE will never regenerate such files for you.
-nbproject/build-impl.xml.data.CRC32=8131ef5a
+nbproject/build-impl.xml.data.CRC32=be2f2804
 nbproject/build-impl.xml.script.CRC32=e59dabeb
-nbproject/build-impl.xml.stylesheet.CRC32=238281d1@2.49
+nbproject/build-impl.xml.stylesheet.CRC32=238281d1@2.50

language/ide/ui/nbproject/project.xml

                     </run-dependency>
                 </dependency>
                 <dependency>
+                    <code-name-base>org.netbeans.modules.java.hints.ui</code-name-base>
+                    <run-dependency>
+                        <specification-version>1.3.0.1.4</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
                     <code-name-base>org.netbeans.modules.java.source</code-name-base>
                     <build-prerequisite/>
                     <compile-dependency/>
                     </run-dependency>
                 </dependency>
                 <dependency>
+                    <code-name-base>org.openide.dialogs</code-name-base>
+                    <build-prerequisite/>
+                    <compile-dependency/>
+                    <run-dependency>
+                        <specification-version>7.24</specification-version>
+                    </run-dependency>
+                </dependency>
+                <dependency>
                     <code-name-base>org.openide.filesystems</code-name-base>
                     <build-prerequisite/>
                     <compile-dependency/>

language/ide/ui/src/org/netbeans/modules/jackpot30/ui/settings/HintSettingsAction.java

+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2012 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 2012 Sun Microsystems, Inc.
+ */
+
+package org.netbeans.modules.jackpot30.ui.settings;
+
+import java.awt.BorderLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.prefs.BackingStoreException;
+import java.util.prefs.Preferences;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JFileChooser;
+import javax.swing.JPanel;
+import javax.swing.filechooser.FileFilter;
+import org.openide.DialogDescriptor;
+import org.openide.DialogDisplayer;
+import org.openide.awt.ActionID;
+import org.openide.awt.ActionReference;
+import org.openide.awt.ActionReferences;
+import org.openide.awt.ActionRegistration;
+import org.openide.util.Exceptions;
+import org.openide.util.Lookup;
+import org.openide.util.NbBundle.Messages;
+
+@ActionID(
+    category = "Tools",
+    id = "org.netbeans.modules.jackpot30.ui.settings.HintSettingsAction")
+@ActionRegistration(
+    displayName = "#CTL_HintSettingsAction")
+@ActionReferences({
+    @ActionReference(path = "Menu/Tools", position = 1250)
+})
+@Messages("CTL_HintSettingsAction=Hint Settings")
+public final class HintSettingsAction implements ActionListener {
+
+    public void actionPerformed(ActionEvent e) {
+        JFileChooser f = new JFileChooser();
+
+        f.setFileFilter(new FileFilter() {
+            @Override public boolean accept(File f) {
+                return f.getName().endsWith(".xml");
+            }
+            @Override public String getDescription() {
+                return "XML Files";
+            }
+        });
+
+        if (f.showDialog(null, "Open") == JFileChooser.APPROVE_OPTION) {
+            try {
+                File settings = f.getSelectedFile();
+                final Preferences p = XMLHintPreferences.from(settings);
+                JPanel hintPanel = createHintPanel(p.node("settings"));
+
+                if (hintPanel == null) {
+                    //TODO: warn the user
+                    return ;
+                }
+
+                final JCheckBox runDeclarativeHints = new JCheckBox("Run Declarative Rules");
+
+                runDeclarativeHints.setToolTipText("Should the declarative rules found on classpath be run?");
+                runDeclarativeHints.setSelected(p.getBoolean("runDeclarative", true));
+                runDeclarativeHints.addActionListener(new ActionListener() {
+                    @Override public void actionPerformed(ActionEvent e) {
+                        p.putBoolean("runDeclarative", runDeclarativeHints.isSelected());
+                    }
+                });
+
+                JPanel customizer = new JPanel(new BorderLayout());
+
+                customizer.add(hintPanel, BorderLayout.CENTER);
+                customizer.add(runDeclarativeHints, BorderLayout.SOUTH);
+
+                JButton save = new JButton("Save");
+                DialogDescriptor dd = new DialogDescriptor(customizer, "Settings", true, new Object[] {save, DialogDescriptor.CANCEL_OPTION}, save, DialogDescriptor.DEFAULT_ALIGN, null, null);
+
+                if (DialogDisplayer.getDefault().notify(dd) == save) {
+                    p.flush();
+                }
+            } catch (BackingStoreException ex) {
+                Exceptions.printStackTrace(ex);
+            } catch (IOException ex) {
+                Exceptions.printStackTrace(ex);
+            }
+        }
+    }
+
+    private static Class<?> loadGlobalClass(String loadClass) {
+        ClassLoader l = Lookup.getDefault().lookup(ClassLoader.class);
+        
+        if (l == null) {
+            l = HintSettingsAction.class.getClassLoader();
+        }
+
+        try {
+            if (l != null) {
+                return l.loadClass(loadClass);
+            } else {
+                return Class.forName(loadClass);
+            }
+        } catch (ClassNotFoundException ex) {
+            Logger.getLogger(HintSettingsAction.class.getName()).log(Level.FINE, null, ex);
+            return null;
+        }
+    }
+    
+    private static JPanel createHintPanel(Preferences p) {
+        //XXX: constructing through reflection, so that we don't need to have too broad implementation dependencies:
+        //new HintsPanel(p, new ClassPathBasedHintWrapper())
+        try {
+            Class<?> classPathBasedHintsWrapper = loadGlobalClass("org.netbeans.modules.java.hints.spiimpl.refactoring.Utilities$ClassPathBasedHintWrapper");
+            Class<?> hintsPanel = loadGlobalClass("org.netbeans.modules.java.hints.spiimpl.options.HintsPanel");
+            
+            if (classPathBasedHintsWrapper == null || hintsPanel == null) return null;
+
+            Constructor<?> newCPBHW = hintsPanel.getConstructor(Preferences.class, classPathBasedHintsWrapper);
+
+            newCPBHW.setAccessible(true);
+            
+            return (JPanel) newCPBHW.newInstance(p, classPathBasedHintsWrapper.newInstance());
+        } catch (InstantiationException ex) {
+            Exceptions.printStackTrace(ex);
+        } catch (IllegalAccessException ex) {
+            Exceptions.printStackTrace(ex);
+        } catch (IllegalArgumentException ex) {
+            Exceptions.printStackTrace(ex);
+        } catch (InvocationTargetException ex) {
+            Exceptions.printStackTrace(ex);
+        } catch (NoSuchMethodException ex) {
+            Exceptions.printStackTrace(ex);
+        } catch (SecurityException ex) {
+            Exceptions.printStackTrace(ex);
+        }
+
+        return null;
+    }
+}

language/ide/ui/src/org/netbeans/modules/jackpot30/ui/settings/XMLHintPreferences.java

+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2012 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 2012 Sun Microsystems, Inc.
+ */
+package org.netbeans.modules.jackpot30.ui.settings;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.prefs.AbstractPreferences;
+import java.util.prefs.BackingStoreException;
+import java.util.prefs.Preferences;
+import org.openide.util.Exceptions;
+import org.openide.xml.XMLUtil;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+/**
+ *
+ * @author lahvac
+ */
+public class XMLHintPreferences extends AbstractPreferences {
+
+    private final Element node;
+
+    private XMLHintPreferences(XMLHintPreferences parent, String nodeName, Element node) {
+        super(parent, nodeName);
+        this.node = node;
+    }
+
+    @Override
+    protected void putSpi(String key, String value) {
+        node.setAttribute(escape(key), value);
+    }
+
+    @Override
+    protected String getSpi(String key) {
+        return node.hasAttribute(escape(key)) ? node.getAttribute(escape(key)) : null;
+    }
+
+    @Override
+    protected void removeSpi(String key) {
+        node.removeAttribute(key);
+    }
+
+    @Override
+    protected void removeNodeSpi() throws BackingStoreException {
+        node.getParentNode().removeChild(node);
+    }
+
+    @Override
+    protected String[] keysSpi() throws BackingStoreException {
+        List<String> keys = new ArrayList<String>();
+        NamedNodeMap nnm = node.getAttributes();
+        
+        for (int i = 0; i < nnm.getLength(); i++) {
+            keys.add(resolve(((Attr) nnm.item(i)).getName()));
+        }
+
+        return keys.toArray(new String[keys.size()]);
+    }
+
+    @Override
+    protected String[] childrenNamesSpi() throws BackingStoreException {
+        List<String> names = new ArrayList<String>();
+        NodeList nl = node.getChildNodes();
+
+        for (int i = 0; i < nl.getLength(); i++) {
+            Node n = nl.item(i);
+
+            if (n instanceof Element) {
+                names.add(resolve(((Element) n).getNodeName()));
+            }
+        }
+
+        return names.toArray(new String[names.size()]);
+    }
+
+    @Override
+    protected AbstractPreferences childSpi(String name) {
+        String escapedName = escape(name);
+        NodeList nl = node.getChildNodes();
+
+        for (int i = 0; i < nl.getLength(); i++) {
+            Node n = nl.item(i);
+
+            if (n instanceof Element && escapedName.equals(((Element) n).getNodeName())) {
+                return new XMLHintPreferences(this, name, (Element) n);
+            }
+        }
+
+        Element nue = node.getOwnerDocument().createElement(escapedName);
+
+        node.appendChild(nue);
+
+        return new XMLHintPreferences(this, name, nue);
+    }
+
+    @Override
+    protected void syncSpi() throws BackingStoreException {
+        //TODO:
+    }
+
+    @Override
+    public void flush() throws BackingStoreException {
+        synchronized (lock) {
+            parent().flush();
+        }
+    }
+
+    @Override
+    protected void flushSpi() throws BackingStoreException {
+        throw new IllegalStateException();
+    }
+
+    private static String escape(String what) {
+        return what.replace("__", "___").replace("$", "__d");
+    }
+
+    private static String resolve(String what) {
+        return what.replace("__d", "$").replace("___", "__");
+    }
+
+    public static Preferences from(File file) throws IOException {
+        if (!file.canRead()) {
+            Document nueDocument = XMLUtil.createDocument("hints", null, null, null);
+            
+            return new RootXMLHintPreferences(nueDocument, file);
+        }
+
+        InputStream in = new BufferedInputStream(new FileInputStream(file));
+
+        try {
+            Document parsed = XMLUtil.parse(new InputSource(in), false, false, null, null);
+
+            return new RootXMLHintPreferences(parsed, file);
+        } catch (SAXException ex) {
+            throw new IOException(ex);
+        } finally {
+            in.close();
+        }
+    }
+
+    private static final class RootXMLHintPreferences extends XMLHintPreferences {
+        private final Document doc;
+        private final File file;
+
+        public RootXMLHintPreferences(Document doc, File file) {
+            super(null, "", doc.getDocumentElement());
+            this.doc = doc;
+            this.file = file;
+        }
+
+        @Override
+        public void flush() throws BackingStoreException {
+            synchronized (lock) {
+                OutputStream out = null;
+                try {
+                    out = new BufferedOutputStream(new FileOutputStream(file));
+                    XMLUtil.write(doc, out, "UTF-8");
+                } catch (IOException ex) {
+                    throw new BackingStoreException(ex);
+                } finally {
+                    try {
+                        if (out != null) {
+                            out.close();
+                        }
+                    } catch (IOException ex) {
+                        Exceptions.printStackTrace(ex);
+                    }
+                }
+            }
+        }
+
+    }
+
+}

language/ide/ui/test/unit/src/org/netbeans/modules/jackpot30/ui/settings/XMLHintPreferencesTest.java

+/*
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
+ *
+ * Copyright 2012 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 2012 Sun Microsystems, Inc.
+ */
+package org.netbeans.modules.jackpot30.ui.settings;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.prefs.Preferences;
+import org.netbeans.junit.NbTestCase;
+
+/**
+ *
+ * @author lahvac
+ */
+public class XMLHintPreferencesTest extends NbTestCase {
+
+    public XMLHintPreferencesTest(String name) {
+        super(name);
+    }
+
+    public void testStorage() throws Exception {
+        clearWorkDir();
+        
+        File storage = new File(getWorkDir(), "test.xml");
+        Preferences p = XMLHintPreferences.from(storage);
+
+        p.put("key", "value");
+        p.node("subnode").put("innerkey", "innervalue");
+
+        for (String phase : new String[] {"before-reload", "after-reload"}) {
+            assertTrue(Arrays.equals(new String[] {"key"}, p.keys()));
+            assertEquals("value", p.get("key", null));
+            assertTrue(Arrays.equals(new String[] {"subnode"}, p.childrenNames()));
+
+            Preferences subnode = p.node("subnode");
+
+            assertEquals("innervalue", subnode.get("innerkey", null));
+
+            p.flush();
+            p = XMLHintPreferences.from(storage);
+        }
+
+        p.remove("key");
+        p.put("key2", "value2");
+        p.node("subnode").removeNode();
+        p.node("subnode2").put("innerkey2", "innervalue2");
+
+        for (String phase : new String[] {"before-reload", "after-reload"}) {
+            assertTrue(Arrays.equals(new String[] {"key2"}, p.keys()));
+            assertEquals("value2", p.get("key2", null));
+            assertTrue(Arrays.equals(new String[] {"subnode2"}, p.childrenNames()));
+
+            Preferences subnode = p.node("subnode2");
+
+            assertEquals("innervalue2", subnode.get("innerkey2", null));
+
+            p.flush();
+            p = XMLHintPreferences.from(storage);
+        }
+    }
+
+    public void testEscaping() throws Exception {
+        clearWorkDir();
+
+        File storage = new File(getWorkDir(), "test.xml");
+        Preferences p = XMLHintPreferences.from(storage);
+
+        p.put("a$b", "a$b");
+        p.node("a$b").put("a$b", "a$b");
+
+        for (String phase : new String[] {"before-reload", "after-reload"}) {
+            assertTrue(Arrays.equals(new String[] {"a$b"}, p.keys()));
+            assertEquals("a$b", p.get("a$b", null));
+            assertTrue(Arrays.equals(new String[] {"a$b"}, p.childrenNames()));
+
+            Preferences subnode = p.node("a$b");
+
+            assertEquals("a$b", subnode.get("a$b", null));
+
+            p.flush();
+            p = XMLHintPreferences.from(storage);
+        }
+    }
+
+}