001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * If you wish your version of this file to be governed by only the CDDL
025: * or only the GPL Version 2, indicate your decision by adding
026: * "[Contributor] elects to include this software in this distribution
027: * under the [CDDL or GPL Version 2] license." If you do not indicate a
028: * single choice of license, a recipient has the option to distribute
029: * your version of this file under either the CDDL, the GPL Version 2 or
030: * to extend the choice of license to its licensees as provided above.
031: * However, if you add GPL Version 2 code and therefore, elected the GPL
032: * Version 2 license, then the option applies only if the new code is
033: * made subject to such option by the copyright holder.
034: *
035: * Contributor(s):
036: *
037: * Portions Copyrighted 2007 Sun Microsystems, Inc.
038: */
039:
040: package org.netbeans.modules.editor.settings.storage;
041:
042: import java.io.IOException;
043: import java.util.Collections;
044: import java.util.HashMap;
045: import java.util.List;
046: import java.util.Map;
047: import java.util.Set;
048: import java.util.WeakHashMap;
049: import java.util.logging.Level;
050: import java.util.logging.Logger;
051: import org.netbeans.api.editor.mimelookup.MimePath;
052: import org.netbeans.modules.editor.settings.storage.spi.StorageDescription;
053: import org.netbeans.modules.editor.settings.storage.spi.StorageReader;
054: import org.netbeans.modules.editor.settings.storage.spi.StorageWriter;
055: import org.openide.filesystems.FileObject;
056: import org.openide.filesystems.FileSystem;
057: import org.openide.filesystems.FileUtil;
058: import org.openide.filesystems.Repository;
059:
060: /**
061: *
062: * @author Vita Stejskal
063: */
064: public final class StorageImpl<K extends Object, V extends Object> {
065:
066: // -J-Dorg.netbeans.modules.editor.settings.storage.StorageImpl.level=FINE
067: private static final Logger LOG = Logger
068: .getLogger(StorageImpl.class.getName());
069:
070: public StorageImpl(StorageDescription<K, V> sd) {
071: this .storageDescription = sd;
072: this .sfs = Repository.getDefault().getDefaultFileSystem();
073: this .baseFolder = sfs.findResource("Editors"); //NOI18N
074: }
075:
076: public Map<K, V> load(MimePath mimePath, String profile,
077: boolean defaults) throws IOException {
078: assert mimePath != null : "The parameter mimePath must not be null"; //NOI18N
079: if (storageDescription.isUsingProfiles()) {
080: assert profile != null : "The parameter profile must not be null"; //NOI18N
081: } else {
082: assert profile == null : "The '"
083: + storageDescription.getId()
084: + "' settings type does not use profiles."; //NOI18N
085: }
086:
087: synchronized (lock) {
088: Map<K, V> data;
089: Map<CacheKey, Map<K, V>> profilesData = profilesCache
090: .get(mimePath);
091: CacheKey cacheKey = cacheKey(profile, defaults);
092:
093: if (profilesData == null) {
094: data = null;
095: profilesData = new HashMap<CacheKey, Map<K, V>>();
096: profilesCache.put(mimePath, profilesData);
097: } else {
098: data = profilesData.get(cacheKey);
099: }
100:
101: if (data == null) {
102: data = _load(mimePath, profile, defaults);
103: profilesData.put(cacheKey, data);
104: }
105:
106: return data;
107: }
108: }
109:
110: public void save(MimePath mimePath, String profile,
111: boolean defaults, Map<K, V> data) throws IOException {
112: assert mimePath != null : "The parameter mimePath must not be null"; //NOI18N
113: if (storageDescription.isUsingProfiles()) {
114: assert profile != null : "The parameter profile must not be null"; //NOI18N
115: } else {
116: assert profile == null : "The '"
117: + storageDescription.getId()
118: + "' settings type does not use profiles."; //NOI18N
119: }
120:
121: synchronized (lock) {
122: Map<CacheKey, Map<K, V>> profilesData = profilesCache
123: .get(mimePath);
124: if (profilesData == null) {
125: profilesData = new HashMap<CacheKey, Map<K, V>>();
126: profilesCache.put(mimePath, profilesData);
127: }
128:
129: boolean resetCache = _save(mimePath, profile, defaults,
130: data);
131: if (!resetCache) {
132: profilesData.put(cacheKey(profile, defaults), data);
133: } else {
134: profilesData.remove(cacheKey(profile, defaults));
135: }
136: }
137: }
138:
139: public void delete(MimePath mimePath, String profile,
140: boolean defaults) throws IOException {
141: assert mimePath != null : "The parameter mimePath must not be null"; //NOI18N
142: if (storageDescription.isUsingProfiles()) {
143: assert profile != null : "The parameter profile must not be null"; //NOI18N
144: } else {
145: assert profile == null : "The '"
146: + storageDescription.getId()
147: + "' settings type does not use profiles."; //NOI18N
148: }
149:
150: synchronized (lock) {
151: Map<CacheKey, Map<K, V>> profilesData = profilesCache
152: .get(mimePath);
153: if (profilesData != null) {
154: profilesData.remove(cacheKey(profile, defaults));
155: }
156: _delete(mimePath, profile, defaults);
157: }
158: }
159:
160: public static interface Operations<K extends Object, V extends Object> {
161: public Map<K, V> load(MimePath mimePath, String profile,
162: boolean defaults) throws IOException;
163:
164: public boolean save(MimePath mimePath, String profile,
165: boolean defaults, Map<K, V> data, Map<K, V> defaultData)
166: throws IOException;
167:
168: public void delete(MimePath mimePath, String profile,
169: boolean defaults) throws IOException;
170: } // End of Operations interface
171:
172: // ------------------------------------------
173: // private implementation
174: // ------------------------------------------
175:
176: private final StorageDescription<K, V> storageDescription;
177: private final FileSystem sfs;
178: private final FileObject baseFolder;
179:
180: private final Object lock = new String("StorageImpl.lock"); //NOI18N
181: private final Map<MimePath, Map<CacheKey, Map<K, V>>> profilesCache = new WeakHashMap<MimePath, Map<CacheKey, Map<K, V>>>();
182:
183: private List<Object[]> scan(MimePath mimePath, String profile,
184: boolean scanModules, boolean scanUsers) {
185: Map<String, List<Object[]>> files = new HashMap<String, List<Object[]>>();
186:
187: SettingsType.getLocator(storageDescription).scan(baseFolder,
188: mimePath.getPath(), profile, true, scanModules,
189: scanUsers, mimePath.size() > 1, files);
190: assert files.size() <= 1 : "Too many results in the scan"; //NOI18N
191:
192: return files.get(profile);
193: }
194:
195: private Map<K, V> _load(MimePath mimePath, String profile,
196: boolean defaults) throws IOException {
197: if (storageDescription instanceof Operations) {
198: @SuppressWarnings("unchecked")
199: Operations<K, V> operations = (Operations<K, V>) storageDescription;
200: if (LOG.isLoggable(Level.FINE)) {
201: LOG.fine("Forwarding loading of '"
202: + storageDescription.getId() + "' to: "
203: + operations); //NOI18N
204: }
205: return operations.load(mimePath, profile, defaults);
206: } else {
207: // Perform the operation
208: List<Object[]> profileInfos = scan(mimePath, profile, true,
209: !defaults);
210: Map<K, V> map = new HashMap<K, V>();
211:
212: if (profileInfos != null) {
213: for (Object[] info : profileInfos) {
214: assert info.length == 5;
215: FileObject profileHome = (FileObject) info[0];
216: FileObject settingFile = (FileObject) info[1];
217: boolean modulesFile = ((Boolean) info[2])
218: .booleanValue();
219: FileObject linkTarget = (FileObject) info[3];
220: boolean legacyFile = ((Boolean) info[4])
221: .booleanValue();
222:
223: if (linkTarget != null) {
224: // link to another mimetype
225: MimePath linkedMimePath = MimePath
226: .parse(linkTarget.getPath()
227: .substring(
228: baseFolder.getPath()
229: .length() + 1));
230: assert linkedMimePath != mimePath : "linkedMimePath should not be the same as the original one"; //NOI18N
231:
232: if (linkedMimePath.size() == 1) {
233: Map<K, V> linkedMap = load(linkedMimePath,
234: profile, defaults);
235: map.putAll(linkedMap);
236: LOG.fine("Adding linked '"
237: + storageDescription.getId()
238: + "' from: '"
239: + linkedMimePath.getPath() + "'"); //NOI18N
240: } else {
241: if (LOG.isLoggable(Level.WARNING)) {
242: LOG
243: .warning("Linking to other than top level mime types is prohibited. " //NOI18N
244: + "Ignoring editor settings link from '"
245: + mimePath.getPath() //NOI18N
246: + "' to '"
247: + linkedMimePath
248: .getPath()
249: + "'"); //NOI18N
250: }
251: }
252: } else {
253: // real settings file
254: StorageReader<? extends K, ? extends V> reader = storageDescription
255: .createReader(settingFile, mimePath
256: .getPath());
257:
258: // Load data from the settingFile
259: Utils.load(settingFile, reader, !legacyFile);
260: Map<? extends K, ? extends V> added = reader
261: .getAdded();
262: Set<? extends K> removed = reader.getRemoved();
263:
264: if (LOG.isLoggable(Level.FINE)) {
265: LOG.fine("Loading '"
266: + storageDescription.getId()
267: + "' from: '"
268: + settingFile.getPath() + "'"); //NOI18N
269: }
270:
271: if (LOG.isLoggable(Level.FINEST)) {
272: LOG.finest("--- Removing '"
273: + storageDescription.getId()
274: + "': " + removed); //NOI18N
275: }
276:
277: // First remove all entries marked as removed
278: for (K key : removed) {
279: map.remove(key);
280: }
281:
282: if (LOG.isLoggable(Level.FINEST)) {
283: LOG.finest("--- Adding '"
284: + storageDescription.getId()
285: + "': " + added); //NOI18N
286: }
287:
288: // Then add all new entries
289: for (K key : added.keySet()) {
290: V value = added.get(key);
291: V origValue = map.put(key, value);
292: if (LOG.isLoggable(Level.FINEST)
293: && origValue != null
294: && !origValue.equals(value)) {
295: LOG
296: .finest("--- Replacing old entry for '"
297: + key
298: + "', orig value = '"
299: + origValue
300: + "', new value = '"
301: + value + "'"); //NOI18N
302: }
303: }
304:
305: if (LOG.isLoggable(Level.FINEST)) {
306: LOG
307: .finest("-------------------------------------"); //NOI18N
308: }
309: }
310: }
311: }
312:
313: return Collections.unmodifiableMap(map);
314: }
315: }
316:
317: private boolean _save(MimePath mimePath, String profile,
318: boolean defaults, Map<K, V> data) throws IOException {
319: Map<K, V> defaultData = load(mimePath, profile, true);
320:
321: if (storageDescription instanceof Operations) {
322: @SuppressWarnings("unchecked")
323: Operations<K, V> operations = (Operations<K, V>) storageDescription;
324: if (LOG.isLoggable(Level.FINE)) {
325: LOG.fine("Forwarding saving of '"
326: + storageDescription.getId() + "' to: "
327: + operations); //NOI18N
328: }
329: return operations.save(mimePath, profile, defaults, data,
330: defaultData);
331: } else {
332: final Map<K, V> added = new HashMap<K, V>();
333: final Map<K, V> removed = new HashMap<K, V>();
334: Utils.diff(defaultData, data, added, removed);
335:
336: // Perform the operation
337: final String mimePathString = mimePath.getPath();
338: final String settingFileName = SettingsType.getLocator(
339: storageDescription).getWritableFileName(
340: mimePathString, profile, null, defaults);
341:
342: sfs.runAtomicAction(new FileSystem.AtomicAction() {
343: public void run() throws IOException {
344: if (added.size() > 0 || removed.size() > 0) {
345: FileObject f = FileUtil.createData(baseFolder,
346: settingFileName);
347: StorageWriter<K, V> writer = storageDescription
348: .createWriter(f, mimePathString);
349: writer.setAdded(added);
350: writer.setRemoved(removed.keySet());
351: Utils.save(f, writer);
352: if (LOG.isLoggable(Level.FINE)) {
353: LOG.fine("Saving '"
354: + storageDescription.getId()
355: + "' to: '" + f.getPath() + "'"); //NOI18N
356: }
357: } else {
358: FileObject f = baseFolder
359: .getFileObject(settingFileName);
360: if (f != null) {
361: f.delete();
362: if (LOG.isLoggable(Level.FINE)) {
363: LOG
364: .fine("Saving '"
365: + storageDescription
366: .getId()
367: + "', no changes from defaults therefore deleting: '"
368: + f.getPath() + "'"); //NOI18N
369: }
370: }
371: }
372: }
373: });
374:
375: return false;
376: }
377: }
378:
379: private void _delete(MimePath mimePath, String profile,
380: boolean defaults) throws IOException {
381: if (storageDescription instanceof Operations) {
382: @SuppressWarnings("unchecked")
383: Operations<K, V> operations = (Operations<K, V>) storageDescription;
384: if (LOG.isLoggable(Level.FINE)) {
385: LOG.fine("Forwarding deletion of '"
386: + storageDescription.getId() + "' to: "
387: + operations); //NOI18N
388: }
389: operations.delete(mimePath, profile, defaults);
390: } else {
391: // Perform the operation
392: final List<Object[]> profileInfos = scan(mimePath, profile,
393: defaults, !defaults);
394: if (profileInfos != null) {
395: sfs.runAtomicAction(new FileSystem.AtomicAction() {
396: public void run() throws IOException {
397: for (Object[] info : profileInfos) {
398: assert info.length == 5;
399: FileObject profileHome = (FileObject) info[0];
400: FileObject settingFile = (FileObject) info[1];
401: boolean modulesFile = ((Boolean) info[2])
402: .booleanValue();
403:
404: // will delete either a real settings file or a link file (.shadow)
405: settingFile.delete();
406:
407: if (LOG.isLoggable(Level.FINE)) {
408: LOG.fine("Deleting '"
409: + storageDescription.getId()
410: + "' file: '"
411: + settingFile.getPath() + "'"); //NOI18N
412: }
413: }
414: }
415: });
416: }
417: }
418: }
419:
420: private static CacheKey cacheKey(String profile, boolean defaults) {
421: return new CacheKey(profile, defaults);
422: }
423:
424: private static final class CacheKey {
425: private final String profile;
426: private final boolean defaults;
427:
428: public CacheKey(String profile, boolean defaults) {
429: this .profile = profile;
430: this .defaults = defaults;
431: }
432:
433: public @Override
434: boolean equals(Object obj) {
435: if (obj == null) {
436: return false;
437: }
438: if (getClass() != obj.getClass()) {
439: return false;
440: }
441: final CacheKey other = (CacheKey) obj;
442: if ((this .profile == null && other.profile != null)
443: || (this .profile != null && other.profile == null)
444: || (this .profile != null && !this .profile
445: .equals(other.profile))) {
446: return false;
447: }
448: if (this .defaults != other.defaults) {
449: return false;
450: }
451: return true;
452: }
453:
454: public @Override
455: int hashCode() {
456: return this .profile != null ? this .profile.hashCode() : 7;
457: }
458:
459: } // End of CacheKey class
460: }
|