Line 0
Link Here
|
|
|
1 |
/* |
2 |
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. |
3 |
* |
4 |
* Copyright 2008 Sun Microsystems, Inc. All rights reserved. |
5 |
* |
6 |
* The contents of this file are subject to the terms of either the GNU |
7 |
* General Public License Version 2 only ("GPL") or the Common |
8 |
* Development and Distribution License("CDDL") (collectively, the |
9 |
* "License"). You may not use this file except in compliance with the |
10 |
* License. You can obtain a copy of the License at |
11 |
* http://www.netbeans.org/cddl-gplv2.html |
12 |
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the |
13 |
* specific language governing permissions and limitations under the |
14 |
* License. When distributing the software, include this License Header |
15 |
* Notice in each file and include the License file at |
16 |
* nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this |
17 |
* particular file as subject to the "Classpath" exception as provided |
18 |
* by Sun in the GPL Version 2 section of the License file that |
19 |
* accompanied this code. If applicable, add the following below the |
20 |
* License Header, with the fields enclosed by brackets [] replaced by |
21 |
* your own identifying information: |
22 |
* "Portions Copyrighted [year] [name of copyright owner]" |
23 |
* |
24 |
* Contributor(s): |
25 |
* |
26 |
* The Original Software is NetBeans. The Initial Developer of the Original |
27 |
* Software is Sun Microsystems, Inc. Portions Copyright 2008 Sun |
28 |
* Microsystems, Inc. All Rights Reserved. |
29 |
* |
30 |
* If you wish your version of this file to be governed by only the CDDL |
31 |
* or only the GPL Version 2, indicate your decision by adding |
32 |
* "[Contributor] elects to include this software in this distribution |
33 |
* under the [CDDL or GPL Version 2] license." If you do not indicate a |
34 |
* single choice of license, a recipient has the option to distribute |
35 |
* your version of this file under either the CDDL, the GPL Version 2 or |
36 |
* to extend the choice of license to its licensees as provided above. |
37 |
* However, if you add GPL Version 2 code and therefore, elected the GPL |
38 |
* Version 2 license, then the option applies only if the new code is |
39 |
* made subject to such option by the copyright holder. |
40 |
*/ |
41 |
|
42 |
package org.netbeans.modules.projectapi; |
43 |
|
44 |
import java.io.IOException; |
45 |
import java.lang.ref.Reference; |
46 |
import java.lang.ref.WeakReference; |
47 |
import java.util.Collection; |
48 |
import java.util.Collections; |
49 |
import java.util.HashMap; |
50 |
import java.util.HashSet; |
51 |
import java.util.LinkedHashSet; |
52 |
import java.util.LinkedList; |
53 |
import java.util.List; |
54 |
import java.util.Map; |
55 |
import java.util.Map.Entry; |
56 |
import java.util.Set; |
57 |
import java.util.prefs.AbstractPreferences; |
58 |
import java.util.prefs.BackingStoreException; |
59 |
import java.util.prefs.Preferences; |
60 |
import org.netbeans.api.project.Project; |
61 |
import org.netbeans.api.project.ProjectManager; |
62 |
import org.netbeans.spi.project.AuxiliaryConfiguration; |
63 |
import org.openide.filesystems.FileStateInvalidException; |
64 |
import org.openide.util.Exceptions; |
65 |
import org.openide.util.NbPreferences; |
66 |
import org.openide.util.Parameters; |
67 |
import org.openide.util.RequestProcessor; |
68 |
import org.openide.util.RequestProcessor.Task; |
69 |
import org.openide.util.Utilities; |
70 |
import org.openide.xml.XMLUtil; |
71 |
import org.w3c.dom.DOMException; |
72 |
import org.w3c.dom.Element; |
73 |
import org.w3c.dom.Node; |
74 |
import org.w3c.dom.NodeList; |
75 |
|
76 |
/** |
77 |
* @author Jan Lahoda |
78 |
*/ |
79 |
public class AuxiliaryConfigBasedPreferencesProvider { |
80 |
|
81 |
private static Map<Project, Reference<AuxiliaryConfigBasedPreferencesProvider>> projects = new HashMap<Project, Reference<AuxiliaryConfigBasedPreferencesProvider>>(); |
82 |
|
83 |
static synchronized AuxiliaryConfigBasedPreferencesProvider findProvider(Project p) { |
84 |
Reference<AuxiliaryConfigBasedPreferencesProvider> provRef = projects.get(p); |
85 |
AuxiliaryConfigBasedPreferencesProvider prov = provRef != null ? provRef.get() : null; |
86 |
|
87 |
if (prov != null) { |
88 |
return prov; |
89 |
} |
90 |
|
91 |
AuxiliaryConfiguration ac = p.getLookup().lookup(AuxiliaryConfiguration.class); |
92 |
|
93 |
if (ac != null) { |
94 |
projects.put(p, new CleaningWeakReference<AuxiliaryConfigBasedPreferencesProvider>(prov = new AuxiliaryConfigBasedPreferencesProvider(p, ac), projects, p)); |
95 |
} |
96 |
|
97 |
return prov; |
98 |
} |
99 |
|
100 |
public static Preferences getPreferences(Project p, Class c, boolean shared) { |
101 |
Parameters.notNull("p", p); |
102 |
Parameters.notNull("c", c); |
103 |
|
104 |
return shared ? getSharedSettings(p, c) : getPrivateSettings(p, c); |
105 |
} |
106 |
|
107 |
private static Preferences getSharedSettings(Project p, Class c) { |
108 |
AuxiliaryConfigBasedPreferencesProvider provider = findProvider(p); |
109 |
|
110 |
if (provider == null) { |
111 |
return null; |
112 |
} |
113 |
|
114 |
return provider.findModule(AuxiliaryConfigBasedPreferencesProvider.findCNBForClass(c)); |
115 |
} |
116 |
|
117 |
private static synchronized Preferences getPrivateSettings(Project p, Class c) { |
118 |
Preferences prefs = NbPreferences.forModule(AuxiliaryConfigBasedPreferencesProvider.class); |
119 |
|
120 |
prefs = prefs.node(PREF_NODE_AUXILIARY_CONFIG_BASE_PREFERENCES); |
121 |
|
122 |
try { |
123 |
String projectDir = encodeString(p.getProjectDirectory().getURL().toExternalForm()); |
124 |
String projectPrivateKey = prefs.get(projectDir, null); |
125 |
|
126 |
if (projectPrivateKey == null) { |
127 |
//find one: |
128 |
int key = 0; |
129 |
|
130 |
while (prefs.getBoolean(projectPrivateKey = "__id" + Integer.toHexString(key), false)) |
131 |
key++; |
132 |
|
133 |
prefs.put(projectDir, projectPrivateKey); |
134 |
prefs.putBoolean(projectPrivateKey, true); |
135 |
} |
136 |
|
137 |
return prefs.node(projectPrivateKey); |
138 |
} catch (FileStateInvalidException ex) { |
139 |
Exceptions.printStackTrace(ex); |
140 |
return null; |
141 |
} |
142 |
} |
143 |
|
144 |
private static String encodeString(String s) { |
145 |
StringBuilder result = new StringBuilder(); |
146 |
|
147 |
for (char c : s.toCharArray()) { |
148 |
if (VALID_KEY_CHARACTERS.indexOf(c) != (-1)) { |
149 |
result.append(c); |
150 |
} else { |
151 |
result.append("_"); |
152 |
result.append(Integer.toHexString((int) c)); |
153 |
result.append("_"); |
154 |
} |
155 |
} |
156 |
|
157 |
return result.toString(); |
158 |
} |
159 |
|
160 |
private static final String NAMESPACE = "http://www.netbeans.org/ns/auxiliary-configuration-preferences/1"; |
161 |
|
162 |
private static final String EL_PREFERENCES = "preferences"; |
163 |
private static final String EL_MODULE = "module"; |
164 |
private static final String EL_PROPERTY = "property"; |
165 |
private static final String EL_NODE = "node"; |
166 |
|
167 |
private static final String ATTR_NAME = "name"; |
168 |
private static final String ATTR_VALUE = "value"; |
169 |
|
170 |
private static final String PREF_NODE_AUXILIARY_CONFIG_BASE_PREFERENCES = "auxiliaryConfigBasedPreferences"; |
171 |
private static final String VALID_KEY_CHARACTERS = "ABCDEFGHIJKLMNOPQRSTVUWXYZabcdefghijklmnopqrstvuwxyz0123456789"; |
172 |
|
173 |
private static final RequestProcessor WORKER = new RequestProcessor("AuxiliaryConfigBasedPreferencesProvider worker", 1); |
174 |
private static final int AUTOFLUSH_TIMEOUT = 5000; |
175 |
|
176 |
private final Project project; |
177 |
private final AuxiliaryConfiguration ac; |
178 |
private final Map<String, Reference<AuxiliaryConfigBasedPreferences>> module2Preferences = new HashMap<String, Reference<AuxiliaryConfigBasedPreferences>>(); |
179 |
private Element configRoot; |
180 |
private boolean modified; |
181 |
private final Task autoFlushTask = WORKER.create(new Runnable() { |
182 |
public void run() { |
183 |
flush(); |
184 |
} |
185 |
}); |
186 |
|
187 |
private final Map<String, Map<String, String>> path2Data = new HashMap<String, Map<String, String>>(); |
188 |
private final Map<String, Set<String>> path2Removed = new HashMap<String, Set<String>>(); |
189 |
private final Set<String> removedNodes = new HashSet<String>(); |
190 |
private final Set<String> createdNodes = new HashSet<String>(); |
191 |
|
192 |
AuxiliaryConfigBasedPreferencesProvider(Project project, AuxiliaryConfiguration ac) { |
193 |
this.project = project; |
194 |
this.ac = ac; |
195 |
loadConfigRoot(); |
196 |
} |
197 |
|
198 |
private void loadConfigRoot() { |
199 |
Element configRootLoc = ac.getConfigurationFragment(EL_PREFERENCES, NAMESPACE, true); |
200 |
|
201 |
if (configRootLoc == null) { |
202 |
configRootLoc = XMLUtil.createDocument(EL_PREFERENCES, NAMESPACE, null, null).createElementNS(NAMESPACE, |
203 |
EL_PREFERENCES); |
204 |
} |
205 |
|
206 |
this.configRoot = configRootLoc; |
207 |
} |
208 |
|
209 |
synchronized void flush() { |
210 |
if (!modified) { |
211 |
return ; |
212 |
} |
213 |
|
214 |
for (String removedNode : removedNodes) { |
215 |
Element el = findRelative(removedNode, false); |
216 |
|
217 |
if (el != null) { |
218 |
el.getParentNode().removeChild(el); |
219 |
} |
220 |
} |
221 |
|
222 |
for (String createdNode : createdNodes) { |
223 |
findRelative(createdNode, true); |
224 |
} |
225 |
|
226 |
for (Entry<String, Map<String, String>> e : path2Data.entrySet()) { |
227 |
Element el = findRelative(e.getKey(), false); |
228 |
|
229 |
assert el != null; |
230 |
|
231 |
for (Entry<String, String> value : e.getValue().entrySet()) { |
232 |
Element p = find(el, value.getKey(), EL_PROPERTY, true); |
233 |
|
234 |
p.setAttribute(ATTR_VALUE, value.getValue()); |
235 |
} |
236 |
} |
237 |
|
238 |
for (Entry<String, Set<String>> e : path2Removed.entrySet()) { |
239 |
Element el = findRelative(e.getKey(), false); |
240 |
|
241 |
assert el != null; |
242 |
|
243 |
for (String removed : e.getValue()) { |
244 |
Element p = find(el, removed, EL_PROPERTY, true); |
245 |
|
246 |
el.removeChild(p); |
247 |
} |
248 |
} |
249 |
|
250 |
ac.putConfigurationFragment(configRoot, true); |
251 |
|
252 |
try { |
253 |
ProjectManager.getDefault().saveProject(project); |
254 |
} catch (IOException ex) { |
255 |
Exceptions.printStackTrace(ex); |
256 |
} |
257 |
|
258 |
path2Data.clear(); |
259 |
path2Removed.clear(); |
260 |
removedNodes.clear(); |
261 |
createdNodes.clear(); |
262 |
modified = false; |
263 |
} |
264 |
|
265 |
synchronized void sync() { |
266 |
loadConfigRoot(); |
267 |
flush(); |
268 |
} |
269 |
|
270 |
private void markModified() { |
271 |
autoFlushTask.cancel(); |
272 |
autoFlushTask.schedule(AUTOFLUSH_TIMEOUT); |
273 |
modified = true; |
274 |
} |
275 |
|
276 |
private static String findCNBForClass(Class c) { |
277 |
Preferences p = NbPreferences.forModule(c); |
278 |
|
279 |
return p.absolutePath().replaceFirst("/", "").replace('/', '.'); |
280 |
} |
281 |
|
282 |
public synchronized Preferences findModule(String moduleName) { |
283 |
Reference<AuxiliaryConfigBasedPreferences> prefRef = module2Preferences.get(moduleName); |
284 |
AuxiliaryConfigBasedPreferences pref = prefRef != null ? prefRef.get() : null; |
285 |
|
286 |
if (pref == null) { |
287 |
module2Preferences.put(moduleName, new CleaningWeakReference<AuxiliaryConfigBasedPreferences>(pref = new AuxiliaryConfigBasedPreferences(null, "", moduleName), module2Preferences, moduleName)); |
288 |
} |
289 |
|
290 |
return pref; |
291 |
} |
292 |
|
293 |
private Element findRelative(String path, boolean createIfMissing) { |
294 |
String[] sep = path.split("/"); |
295 |
|
296 |
assert sep.length > 0; |
297 |
|
298 |
Element e = find(configRoot, sep[0], EL_MODULE, createIfMissing); |
299 |
|
300 |
for (int cntr = 1; cntr < sep.length && e != null; cntr++) { |
301 |
e = find(e, sep[cntr], EL_NODE, createIfMissing); |
302 |
} |
303 |
|
304 |
return e; |
305 |
} |
306 |
|
307 |
private Map<String, String> getData(String path) { |
308 |
Map<String, String> data = path2Data.get(path); |
309 |
|
310 |
if (data == null) { |
311 |
path2Data.put(path, data = new HashMap<String, String>()); |
312 |
} |
313 |
|
314 |
return data; |
315 |
} |
316 |
|
317 |
private Set<String> getRemoved(String path) { |
318 |
Set<String> removed = path2Removed.get(path); |
319 |
|
320 |
if (removed == null) { |
321 |
path2Removed.put(path, removed = new HashSet<String>()); |
322 |
} |
323 |
|
324 |
return removed; |
325 |
} |
326 |
|
327 |
private void removeNode(String path) { |
328 |
path2Data.remove(path); |
329 |
path2Removed.remove(path); |
330 |
createdNodes.remove(path); |
331 |
removedNodes.add(path); |
332 |
} |
333 |
|
334 |
private boolean isRemovedNode(String path) { |
335 |
return removedNodes.contains(path); |
336 |
} |
337 |
|
338 |
private static Element find(Element dom, String key, String elementName, boolean createIfMissing) { |
339 |
NodeList nl = dom.getChildNodes(); |
340 |
|
341 |
for (int cntr = 0; cntr < nl.getLength(); cntr++) { |
342 |
Node n = nl.item(cntr); |
343 |
|
344 |
if (n.getNodeType() == Node.ELEMENT_NODE && NAMESPACE.equals(n.getNamespaceURI()) && elementName.equals(n.getLocalName())) { |
345 |
if (key.equals(((Element) n).getAttribute(ATTR_NAME))) { |
346 |
return (Element) n; |
347 |
} |
348 |
} |
349 |
} |
350 |
|
351 |
if (!createIfMissing) { |
352 |
return null; |
353 |
} |
354 |
|
355 |
Element el = dom.getOwnerDocument().createElementNS(NAMESPACE, elementName); |
356 |
|
357 |
el.setAttribute(ATTR_NAME, key); |
358 |
|
359 |
dom.appendChild(el); |
360 |
|
361 |
return el; |
362 |
} |
363 |
|
364 |
private class AuxiliaryConfigBasedPreferences extends AbstractPreferences { |
365 |
|
366 |
private final String path; |
367 |
|
368 |
public AuxiliaryConfigBasedPreferences(AbstractPreferences parent, String name, String path) { |
369 |
super(parent, name); |
370 |
this.path = path; |
371 |
} |
372 |
|
373 |
@Override |
374 |
protected void putSpi(String key, String value) { |
375 |
synchronized (AuxiliaryConfigBasedPreferencesProvider.this) { |
376 |
getData(path).put(key, value); |
377 |
getRemoved(path).remove(key); |
378 |
|
379 |
markModified(); |
380 |
} |
381 |
} |
382 |
|
383 |
@Override |
384 |
protected String getSpi(String key) { |
385 |
synchronized (AuxiliaryConfigBasedPreferencesProvider.this) { |
386 |
if (getRemoved(path).contains(key)) { |
387 |
return null; |
388 |
} |
389 |
|
390 |
if (getData(path).containsKey(key)) { |
391 |
return getData(path).get(key); |
392 |
} |
393 |
|
394 |
if (isRemovedNode(path)) { |
395 |
return null; |
396 |
} |
397 |
|
398 |
Element p = findRelative(path, false); |
399 |
|
400 |
p = p != null ? AuxiliaryConfigBasedPreferencesProvider.find(p, key, EL_PROPERTY, false) : null; |
401 |
|
402 |
if (p == null) { |
403 |
return null; |
404 |
} |
405 |
|
406 |
return p.getAttribute(ATTR_VALUE); |
407 |
} |
408 |
} |
409 |
|
410 |
@Override |
411 |
protected void removeSpi(String key) { |
412 |
synchronized (AuxiliaryConfigBasedPreferencesProvider.this) { |
413 |
getData(path).remove(key); |
414 |
getRemoved(path).add(key); |
415 |
|
416 |
markModified(); |
417 |
} |
418 |
} |
419 |
|
420 |
@Override |
421 |
protected void removeNodeSpi() throws BackingStoreException { |
422 |
synchronized (AuxiliaryConfigBasedPreferencesProvider.this) { |
423 |
AuxiliaryConfigBasedPreferencesProvider.this.removeNode(path); |
424 |
markModified(); |
425 |
} |
426 |
} |
427 |
|
428 |
@Override |
429 |
protected String[] keysSpi() throws BackingStoreException { |
430 |
synchronized (AuxiliaryConfigBasedPreferencesProvider.this) { |
431 |
Collection<String> result = new LinkedHashSet<String>(); |
432 |
|
433 |
if (!isRemovedNode(path)) { |
434 |
result.addAll(list(EL_PROPERTY)); |
435 |
} |
436 |
|
437 |
result.addAll(getData(path).keySet()); |
438 |
result.removeAll(getRemoved(path)); |
439 |
|
440 |
return result.toArray(new String[0]); |
441 |
} |
442 |
} |
443 |
|
444 |
@Override |
445 |
protected String[] childrenNamesSpi() throws BackingStoreException { |
446 |
synchronized (AuxiliaryConfigBasedPreferencesProvider.this) { |
447 |
return getChildrenNames().toArray(new String[0]); |
448 |
} |
449 |
} |
450 |
|
451 |
@Override |
452 |
protected AbstractPreferences childSpi(String name) { |
453 |
synchronized (AuxiliaryConfigBasedPreferencesProvider.this) { |
454 |
String nuePath = path + "/" + name; |
455 |
if (!getChildrenNames().contains("name")) { |
456 |
AuxiliaryConfigBasedPreferencesProvider.this.createdNodes.add(nuePath); |
457 |
} |
458 |
|
459 |
return new AuxiliaryConfigBasedPreferences(this, name, nuePath); |
460 |
} |
461 |
} |
462 |
|
463 |
@Override |
464 |
public void sync() throws BackingStoreException { |
465 |
AuxiliaryConfigBasedPreferencesProvider.this.sync(); |
466 |
} |
467 |
|
468 |
@Override |
469 |
protected void syncSpi() throws BackingStoreException { |
470 |
throw new UnsupportedOperationException("Should never be called."); |
471 |
} |
472 |
|
473 |
@Override |
474 |
public void flush() throws BackingStoreException { |
475 |
AuxiliaryConfigBasedPreferencesProvider.this.flush(); |
476 |
} |
477 |
|
478 |
@Override |
479 |
protected void flushSpi() throws BackingStoreException { |
480 |
throw new UnsupportedOperationException("Should never be called."); |
481 |
} |
482 |
|
483 |
private Collection<String> getChildrenNames() { |
484 |
Collection<String> result = new LinkedHashSet<String>(); |
485 |
|
486 |
if (!isRemovedNode(path)) { |
487 |
result.addAll(list(EL_NODE)); |
488 |
} |
489 |
|
490 |
for (String removed : removedNodes) { |
491 |
int slash = removed.lastIndexOf('/'); |
492 |
|
493 |
if (path.equals(removed.substring(slash))) { |
494 |
result.remove(removed.substring(slash + 1)); |
495 |
} |
496 |
} |
497 |
for (String created : createdNodes) { |
498 |
int slash = created.lastIndexOf('/'); |
499 |
|
500 |
if (path.equals(created.substring(slash))) { |
501 |
result.add(created.substring(slash + 1)); |
502 |
} |
503 |
} |
504 |
|
505 |
return result; |
506 |
} |
507 |
|
508 |
private Collection<String> list(String elementName) throws DOMException { |
509 |
Element dom = findRelative(path, false); |
510 |
|
511 |
if (dom == null) { |
512 |
return Collections.emptyList(); |
513 |
} |
514 |
|
515 |
List<String> names = new LinkedList<String>(); |
516 |
NodeList nl = dom.getElementsByTagNameNS(NAMESPACE, elementName); |
517 |
|
518 |
for (int cntr = 0; cntr < nl.getLength(); cntr++) { |
519 |
Node n = nl.item(cntr); |
520 |
|
521 |
names.add(((Element) n).getAttribute(ATTR_NAME)); |
522 |
} |
523 |
|
524 |
return names; |
525 |
} |
526 |
|
527 |
} |
528 |
|
529 |
private static final class CleaningWeakReference<T> extends WeakReference<T> implements Runnable { |
530 |
private final Map<?, ?> map; |
531 |
private final Object key; |
532 |
|
533 |
public CleaningWeakReference(T data, Map<?, ?> map, Object key) { |
534 |
super(data, Utilities.activeReferenceQueue()); |
535 |
this.map = map; |
536 |
this.key = key; |
537 |
} |
538 |
|
539 |
public void run() { |
540 |
map.remove(key); |
541 |
} |
542 |
} |
543 |
} |