/******************************************************************************* * Industry THTH ry. * Copyright (c) 2010- Association for Decentralized Information Management in * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * VTT Technical Research Centre of Finland - initial API and implementation *******************************************************************************/ package org.simantics.databoard.accessor.impl; import java.io.File; import java.io.FileFilter; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.Executor; import org.simantics.databoard.Bindings; import org.simantics.databoard.Datatypes; import org.simantics.databoard.accessor.Accessor; import org.simantics.databoard.accessor.CloseableAccessor; import org.simantics.databoard.accessor.MapAccessor; import org.simantics.databoard.accessor.VariantAccessor; import org.simantics.databoard.accessor.error.AccessorConstructionException; import org.simantics.databoard.accessor.error.AccessorException; import org.simantics.databoard.accessor.error.ReferenceException; import org.simantics.databoard.accessor.event.Event; import org.simantics.databoard.accessor.event.MapEntryAdded; import org.simantics.databoard.accessor.event.MapEntryRemoved; import org.simantics.databoard.accessor.event.ValueAssigned; import org.simantics.databoard.accessor.file.FileLibrary; import org.simantics.databoard.accessor.file.FileVariantAccessor; import org.simantics.databoard.accessor.impl.DirectoryWatch.DirectoryEvent; import org.simantics.databoard.accessor.impl.DirectoryWatch.DirectoryListener; import org.simantics.databoard.accessor.interestset.InterestSet; import org.simantics.databoard.accessor.interestset.MapInterestSet; import org.simantics.databoard.accessor.reference.ChildReference; import org.simantics.databoard.accessor.reference.KeyReference; import org.simantics.databoard.accessor.reference.LabelReference; import org.simantics.databoard.adapter.AdaptException; import org.simantics.databoard.adapter.Adapter; import org.simantics.databoard.adapter.AdapterConstructionException; import org.simantics.databoard.binding.ArrayBinding; import org.simantics.databoard.binding.Binding; import org.simantics.databoard.binding.MapBinding; import org.simantics.databoard.binding.VariantBinding; import org.simantics.databoard.binding.error.BindingConstructionException; import org.simantics.databoard.binding.error.BindingException; import org.simantics.databoard.binding.error.RuntimeBindingException; import org.simantics.databoard.binding.mutable.MutableVariant; import org.simantics.databoard.type.Datatype; import org.simantics.databoard.type.MapType; /** * DirectoryMap is a file backed map implementation where keys are filenames * and values are corresponding files. *

* This class is an implmentation to Map(Variant, Variant) -Accessor. * * Filenames have the following encoding: * S.dbb String types, if string doesn't have the following * control characters " : < > | ? * \ / [0..31] * I.dbb Integer types * L.dbb Long types * H.dbb All other cases the value as binary *

* File accessor is created if an entry opened as a sub-accessor. * The file accessor is closed when all sub-accessors are released. * The implementation is based on proxy instances and a reference queue. * Once the queue is empty, file accessor is closed. *

* DirectoryMap must be closed with #close(); * * @author Toni Kalajainen */ public class DirectoryMap implements MapAccessor, CloseableAccessor { /** Key binding */ final static Binding KEY_BINDING = Bindings.STR_VARIANT; /** Cache of sub-accessors */ FileLibrary files; /** Monitors directory for file changes */ DirectoryWatch dir; /** Folder */ File path; /** Listeners */ ListenerEntry listeners = null; /** Parent, optional */ Accessor parent; /** Accessor params */ AccessorParams params; DirectoryListener dirListener = new DirectoryListener() { @Override public void onWatchEvent(DirectoryEvent e) { } }; public DirectoryMap(File directory) { this(directory, null, AccessorParams.DEFAULT); } public DirectoryMap(File directory, Accessor parent) { this(directory, parent, AccessorParams.DEFAULT); } public DirectoryMap(File directory, Accessor parent, AccessorParams params) { this.parent = parent; this.path = directory; this.params = params; // Filters .dbb files FileFilter filter = new FileFilter() { public boolean accept(File pathname) { String filename = pathname.getName(); if (filename.length()==0) return false; char c = filename.charAt(0); if (c!='S' && c!='I' && c!='L' && c!='B') return false; if (filename.endsWith(".dbb")) return true; return filename.toLowerCase().endsWith(".dbb"); }}; dir = new DirectoryWatch(path, filter); dir.addListener( dirListener ); files = new FileLibrary(); } public void close() { dir.removeListener( dirListener ); dir.close(); files.close(); } static MapType type = new MapType(Datatypes.VARIANT, Datatypes.VARIANT); public MapType type() { return type; } private String fileToKey(File f) { String filename = f.getName(); String keyStr = filename.substring(0, filename.length()-4); return keyStr; } private File keyToFile(String keyStr) { return new File(path, keyStr + ".dbb" ); } @Override public void clear() throws AccessorException { //List deleteList = dir.files(); List failList = new ArrayList(); boolean hasListeners = listeners!=null; List keys = hasListeners ? new ArrayList() : null; // Close all file handles files.close(); // Delete files for (File f : dir.files()) { if (!files.deleteFile(f)) { failList.add(f); } if (hasListeners) { String keyStr = fileToKey(f); keys.add(keyStr); } } // Re-read directory dir.refresh(); // Notify Listeners ListenerEntry le = listeners; while (le!=null) { MapInterestSet is = le.getInterestSet(); for (Object key : keys) { MutableVariant var = new MutableVariant(KEY_BINDING, key); if (is.inNotificationsOf(var)) { MapEntryRemoved e = new MapEntryRemoved(var); emitEvent(le, e); } } le = le.next; } // Some files failed to delete if (!failList.isEmpty()) { StringBuilder sb = new StringBuilder(); sb.append("Failed to delete"); for (File f : failList) { sb.append(' '); sb.append(f.toString()); } // HAX throw new AccessorException(sb.toString()); } } @Override public String toString() { return dir.toString(); } public Object getValue(Binding binding) throws AccessorException { MapBinding mb = (MapBinding) binding; if (mb.getKeyBinding() instanceof VariantBinding==false || mb.getValueBinding() instanceof VariantBinding==false) throw new AccessorException("Map(Variant, Variant) Expected"); // Get all files as a single map try { Object result = binding.createDefault(); for (File f : dir.files()) { // Create Key String keyStr = fileToKey(f); Object key = params.adapterScheme.adapt(keyStr, KEY_BINDING, mb.getKeyBinding()); // Read value VariantAccessor va = getValueAccessor(KEY_BINDING, keyStr); Object value = va.getValue(mb.getValueBinding()); mb.put(result, key, value); } return result; } catch (BindingException e) { throw new AccessorException(e); } catch (AccessorConstructionException e) { throw new AccessorException(e); } catch (AdaptException e) { throw new AccessorException(e); } } @Override public void getValue(Binding dstBinding, Object dst) throws AccessorException { MapBinding db = (MapBinding) dstBinding; Binding dkb = db.getKeyBinding(); Binding dvb = db.getValueBinding(); if (dkb instanceof VariantBinding==false || dvb instanceof VariantBinding==false) throw new AccessorException("Map(Variant, Variant) Expected"); // Get all files as a single map try { TreeSet dstKeys = new TreeSet(dkb); db.getKeys(dst, dstKeys); for (File f : dir.files()) { // Create Key String keyStr = fileToKey(f); Object key = params.adapterScheme.adapt(keyStr, KEY_BINDING, dkb); Object v = db.containsKey(dst, key) ? db.get(dst, key) : dvb.createDefault(); VariantAccessor va = getValueAccessor(KEY_BINDING, keyStr); va.getValue(dvb, v); db.put(dst, key, v); dstKeys.remove(key); } for (Object key : dstKeys) db.remove(dst, key); } catch (BindingException e) { throw new AccessorException(e); } catch (AccessorConstructionException e) { throw new AccessorException(e); } catch (AdaptException e) { throw new AccessorException(e); } } @Override public boolean getValue(ChildReference path, Binding binding, Object obj) throws AccessorException { try { Accessor a = getComponent(path); a.getValue(binding, obj); return true; } catch (ReferenceException re) { return false; } catch (AccessorConstructionException e) { throw new AccessorException(e); } } public Object getValue(ChildReference path, Binding binding) throws AccessorException { try { Accessor a = getComponent(path); return a.getValue(binding); } catch (ReferenceException re) { return null; } catch (AccessorConstructionException e) { throw new AccessorException(e); } } @Override public boolean containsKey(Binding keyBinding, Object key) throws AccessorException { try { String key_ = (String) params.adapterScheme.adapt(key, keyBinding, KEY_BINDING); File file = keyToFile(key_); return dir.files().contains(file); } catch (AdaptException e) { throw new AccessorException(e); } } @Override public boolean containsValue(Binding valueBinding, Object value) throws AccessorException { try { for (File f : dir.files()) { String key = fileToKey(f); VariantAccessor va = getValueAccessor(KEY_BINDING, key); Object v = va.getValue(valueBinding); boolean match = valueBinding.equals(v, value); if ( match ) return true; } return false; } catch (AccessorConstructionException e) { throw new AccessorException(e); } } @Override public Object get(Binding keyBinding, Object key, Binding valueBinding) throws AccessorException { try { VariantAccessor va = getValueAccessor(keyBinding, key); return va.getValue(valueBinding); } catch (AccessorConstructionException e) { throw new AccessorException(e); } } /** * Get the value as a variant * * @param keyBinding * @param key * @return value * @throws AccessorException */ public MutableVariant getAsVariant(Binding keyBinding, Object key) throws AccessorException { try { VariantAccessor va = getValueAccessor(keyBinding, key); Datatype type = va.getContentType(); Binding binding = params.bindingScheme.getBinding(type); Object value = va.getContentValue(binding); MutableVariant result = new MutableVariant(binding, value); return result; } catch (AccessorConstructionException e) { throw new AccessorException(e); } catch (BindingConstructionException e) { throw new AccessorException(e); } } @Override public void getAll(Binding keyBinding, Binding valueBinding, Map to) throws AccessorException { try { for (File f : dir.files()) { // Create key String keyStr = fileToKey(f); Object key = params.adapterScheme.adapt(keyStr, KEY_BINDING, keyBinding); // Read value VariantAccessor va = getValueAccessor(KEY_BINDING, keyStr); Object value = va.getValue(valueBinding); to.put(key, value); } } catch (AdaptException e) { throw new AccessorException(e); } catch (AccessorConstructionException e) { throw new AccessorException(e); } } @Override public void getAll(Binding keyBinding, Binding valueBinding, Object[] keys, Object[] values) throws AccessorException { try { Set fileKeys = createKeys(); int i=0; for (String keyStr : fileKeys) { // Read value VariantAccessor va = getValueAccessor(KEY_BINDING, keyStr); Object value = va.getValue(valueBinding); Object key2 = params.adapterScheme.adapt(keyStr, KEY_BINDING, keyBinding); keys[i] = key2; values[i] = value; i++; } } catch (AdaptException e) { throw new AccessorException(e); } catch (AccessorConstructionException e) { throw new AccessorException(e); } } @Override public int count(Binding keyBinding, Object from, boolean fromInclusive, Object end, boolean endInclusive) throws AccessorException { throw new AccessorException("Not implemented"); } @Override public int getEntries(Binding keyBinding, Object from, boolean fromInclusive, Object end, boolean endInclusive, ArrayBinding keyArrayBinding, Object dstKeys, ArrayBinding valueArrayBinding, Object dstValues, int limit) throws AccessorException { throw new AccessorException("Not implemented"); } TreeSet createKeys() throws RuntimeBindingException { List files = dir.files(); TreeSet keys = new TreeSet(KEY_BINDING); for (File f : files) { String filename = f.getName(); String str = filename.substring(0, filename.length()-4); keys.add(str); } return keys; } @Override public Object getCeilingKey(Binding keyBinding, Object key) throws AccessorException { try { TreeSet keys = createKeys(); String k = (String) params.adapterScheme.adapt(key, keyBinding, KEY_BINDING); if (keys.contains(k)) return key; Object res = keys.ceiling(k); if (res==null) return null; return params.adapterScheme.adapt(res, KEY_BINDING, keyBinding); } catch (RuntimeBindingException e) { throw new AccessorException(e); } catch (AdaptException e) { throw new AccessorException(e); } } @Override public Object getFirstKey(Binding keyBinding) throws AccessorException { List files = dir.files(); String firstKey = null; for (File f : files) { String filename = f.getName(); String str = filename.substring(0, filename.length()-4); if (firstKey == null) { firstKey = str; } else { if (KEY_BINDING.compare(str, firstKey)<0) firstKey = str; } } if (firstKey==null) return null; try { return params.adapterScheme.adapt(firstKey, KEY_BINDING, keyBinding); } catch (AdaptException e) { throw new AccessorException(e); } } @Override public Object getFloorKey(Binding keyBinding, Object key) throws AccessorException { try { TreeSet keys = createKeys(); String k = (String) params.adapterScheme.adapt(key, keyBinding, KEY_BINDING); Object res = keys.floor(k); if (res==null) return null; return params.adapterScheme.adapt(res, KEY_BINDING, keyBinding); } catch (RuntimeBindingException e) { throw new AccessorException(e); } catch (AdaptException e) { throw new AccessorException(e); } } @Override public Object getHigherKey(Binding keyBinding, Object key) throws AccessorException { try { TreeSet keys = createKeys(); String k = (String) params.adapterScheme.adapt(key, keyBinding, KEY_BINDING); Object res = keys.higher(k); if (res==null) return null; return params.adapterScheme.adapt(res, KEY_BINDING, keyBinding); } catch (RuntimeBindingException e) { throw new AccessorException(e); } catch (AdaptException e) { throw new AccessorException(e); } } @Override public Object[] getKeys(Binding keyBinding) throws AccessorException { TreeSet keys = createKeys(); Object[] result = new Object[keys.size()]; if (keys.isEmpty()) return result; try { Adapter a = params.adapterScheme.getAdapter(KEY_BINDING, keyBinding, true, false); int index = 0; for (String key : keys) { result[index++] = a.adapt( key ); } } catch (AdaptException e) { throw new AccessorException(e); } catch (AdapterConstructionException e) { throw new AccessorException(e); } return result; } @Override public Object getLastKey(Binding keyBinding) throws AccessorException { List files = dir.files(); String lastKey = null; for (File f : files) { String filename = f.getName(); String str = filename.substring(0, filename.length()-4); if (lastKey == null) { lastKey = str; } else { if (KEY_BINDING.compare(str, lastKey)>0) lastKey = str; } } if (lastKey==null) return null; try { return params.adapterScheme.adapt(lastKey, KEY_BINDING, keyBinding); } catch (AdaptException e) { throw new AccessorException(e); } } @Override public Object getLowerKey(Binding keyBinding, Object key) throws AccessorException { try { TreeSet keys = createKeys(); String k = (String) params.adapterScheme.adapt(key, keyBinding, KEY_BINDING); Object res = keys.lower(k); if (res==null) return null; return params.adapterScheme.adapt(res, KEY_BINDING, keyBinding); } catch (RuntimeBindingException e) { throw new AccessorException(e); } catch (AdaptException e) { throw new AccessorException(e); } } public FileVariantAccessor getExistingAccessor(Binding keyBinding, Object key) throws AccessorConstructionException { try { String key_ = (String) params.adapterScheme.adapt(key, keyBinding, KEY_BINDING); File file = new File(path, key_ + ".dbb" ); return files.getExistingFile(file); } catch (AdaptException e) { throw new AccessorConstructionException(e); } } @SuppressWarnings("unchecked") @Override public FileVariantAccessor getValueAccessor(Binding keyBinding, Object key) throws AccessorConstructionException { try { String keyStr = (String) params.adapterScheme.adapt(key, keyBinding, KEY_BINDING); File file = keyToFile(keyStr); FileVariantAccessor sa = files.getExistingFile(file); if (sa!=null) return sa; // Create new accessor sa = files.getFile(file); // Add component interest sets ListenerEntry le = listeners; if (le!=null) { MutableVariant kv = new MutableVariant(keyBinding, key); while (le!=null) { MapInterestSet is = le.getInterestSet(); // Generic element interest InterestSet gis = is.getComponentInterest(); if (gis != null) { try { ChildReference childPath = ChildReference.concatenate(le.path, new KeyReference(kv) ); sa.addListener(le.listener, gis, childPath, le.executor); } catch (AccessorException e) { throw new AccessorConstructionException(e); } } // Specific element interest InterestSet cis = is.getComponentInterest(kv); if (cis != null) { try { ChildReference childPath = ChildReference.concatenate(le.path, new KeyReference(kv) ); sa.addListener(le.listener, cis, childPath, le.executor); } catch (AccessorException e) { throw new AccessorConstructionException(e); } } // Next listener le = le.next; } } return sa; } catch (AdaptException e) { throw new AccessorConstructionException(e); } } @Override public Object[] getValues(Binding valueBinding) throws AccessorException { try { Set keys = createKeys(); int count = keys.size(); Object[] result = new Object[ count ]; Iterator iter = keys.iterator(); for (int i=0; i from) throws AccessorException { //boolean created = false; for (Entry e : from.entrySet()) { Object key = e.getKey(); Object value = e.getValue(); /*created |=*/ putLocal(keyBinding, key, valueBinding, value); } } @Override public void putAll(Binding keyBinding, Binding valueBinding, Object[] keys, Object[] values) throws AccessorException { //boolean created = false; if (keys.length!=values.length) throw new AccessorException("Array lengths mismatch"); for (int i=0; i writeFiles = new HashSet(); List oldFiles = dir.files(); //HashSet modifiedFiles = new HashSet(); HashSet addedFiles = new HashSet(writeFiles); addedFiles.removeAll(oldFiles); // Write for (int i=0; i removedFiles = new HashSet(oldFiles); removedFiles.removeAll(writeFiles); // Remove old files files.expunge(); if (!removedFiles.isEmpty()) { List failList = new ArrayList(); for (File f : removedFiles) { String filename = f.getName(); String keyStr = filename.substring(0, filename.length()-4); boolean deleted = files.deleteFile(f); if ( !deleted ) { failList.add(f); } else { // Notify Listeners if (listeners!=null) { MutableVariant var = new MutableVariant(KEY_BINDING, keyStr); ListenerEntry le = listeners; while (le!=null) { MapInterestSet is = le.getInterestSet(); if (is.inNotificationsOf(var)) { MapEntryRemoved e = new MapEntryRemoved(var); emitEvent(le, e); } le = le.next; } } } if (!failList.isEmpty()) { StringBuilder sb = new StringBuilder(); sb.append("Failed to delete"); for (File ff : failList) { sb.append(' '); sb.append(ff.toString()); } throw new AccessorException(sb.toString()); } } } dir.refresh(); } catch (BindingException e) { throw new AccessorException(e); } catch (AdaptException e) { throw new AccessorException(e); } catch (AdapterConstructionException e) { throw new AccessorException(e); } catch (AccessorConstructionException e) { throw new AccessorException(e); } } @Override public int size() throws AccessorException { dir.refresh(); return dir.files().size(); } @Override public void addListener(Listener listener, InterestSet interestSet, ChildReference path, Executor executor) throws AccessorException { listeners = ListenerEntry.link(listeners, listener, interestSet, path, executor); MapInterestSet is = (MapInterestSet) interestSet; try { for (File f : dir.files()) { String filename = f.getName(); String keyStr = filename.substring(0, filename.length()-4); Accessor sa = getExistingAccessor(KEY_BINDING, keyStr); if (sa==null) continue; MutableVariant key = new MutableVariant(KEY_BINDING, keyStr); InterestSet cis = is.getComponentInterest(); if (cis!=null) { ChildReference childPath = ChildReference.concatenate( path, new KeyReference(key) ); sa.addListener(listener, cis, childPath, executor); } cis = is.getComponentInterest( key ); if (cis!=null) { ChildReference childPath = ChildReference.concatenate( path, new KeyReference(key) ); sa.addListener(listener, cis, childPath, executor); } } } catch (AccessorConstructionException e) { throw new AccessorException(e); } } @Override public void apply(List cs, LinkedList rollback) throws AccessorException { try { boolean makeRollback = rollback != null; ArrayList single = new ArrayList(); for (Event e : cs) { if (e.reference==null) { Event rbe = applyLocal(e, makeRollback); if (makeRollback) { rbe.reference = e.reference; rollback.addFirst( rbe ); } } else { Accessor sa = getComponent(e.reference); // Apply changes single.clear(); Event noRefEvent = e.clone(null); single.add(noRefEvent); sa.apply(single, rollback); } } } catch (AccessorConstructionException ae) { throw new AccessorException(ae); } } Event applyLocal(Event e, boolean makeRollback) throws AccessorException { Event rollback = null; try { if (e instanceof ValueAssigned) { ValueAssigned va = (ValueAssigned) e; if (makeRollback) { Binding binding = params.bindingScheme.getBinding(type()); rollback = new ValueAssigned(binding, getValue(binding)); } setValue(va.newValue.getBinding(), va.newValue.getValue()); return rollback; } else if (e instanceof MapEntryAdded) { MapEntryAdded ea = (MapEntryAdded) e; if (ea.key==null) throw new AccessorException("Cannot apply entry added event because key is missing"); if (ea.value==null) throw new AccessorException("Cannot apply entry added event because value is missing"); boolean hadValue = containsKey(ea.key.getBinding(), ea.key.getValue()); if (hadValue) throw new AccessorException("Could not add entry to key that already existed"); if (makeRollback) { rollback = new MapEntryRemoved( ea.key ); } put(ea.key.getBinding(), ea.key.getValue(), ea.value.getBinding(), ea.value.getValue()); } else if (e instanceof MapEntryRemoved) { MapEntryRemoved er = (MapEntryRemoved) e; if (makeRollback) { boolean hadValue = containsKey(er.key.getBinding(), er.key.getValue()); if (hadValue) { MutableVariant oldKey = er.key; MutableVariant oldValue = getAsVariant(er.key.getBinding(), er.key.getValue()); rollback = new MapEntryAdded(oldKey, oldValue); } else { rollback = new MapEntryRemoved( er.key.clone() ); } } remove( er.key.getBinding(), er.key.getValue() ); } else throw new AccessorException("Cannot apply "+e.getClass().getName()+" to Map Type"); return rollback; } catch (BindingConstructionException e2) { throw new AccessorException( e2 ); } } @SuppressWarnings("unchecked") @Override public T getComponent(ChildReference reference) throws AccessorConstructionException { if (reference==null) return (T) this; if (reference instanceof LabelReference) { try { LabelReference lr = (LabelReference) reference; MutableVariant variant = (MutableVariant) params.adapterScheme.adapt(lr.label, Bindings.STRING, Bindings.MUTABLE_VARIANT); Object value = variant.getValue(KEY_BINDING); Accessor result = (T) getValueAccessor(KEY_BINDING, value); if (reference.getChildReference() != null) result = result.getComponent(reference.getChildReference()); return (T) result; } catch (AdaptException e) { throw new ReferenceException(e); } } else if (reference instanceof KeyReference) { try { KeyReference ref = (KeyReference) reference; String keyStr = (String) params.adapterScheme.adapt(ref.key.getValue(), ref.key.getBinding(), KEY_BINDING); File f = keyToFile(keyStr); if (!dir.files().contains(f)) throw new AccessorConstructionException("Invalid reference "+ref.key); Accessor result = getValueAccessor(KEY_BINDING, keyStr); if (reference.getChildReference() != null) result = result.getComponent(reference.getChildReference()); return (T) result; } catch (AdaptException e) { throw new ReferenceException(e); } } throw new ReferenceException(reference.getClass().getName()+" is not a reference of a map"); } @Override public void removeListener(Listener listener) throws AccessorException { detachListener(listener); } protected ListenerEntry detachListener(Listener listener) throws AccessorException { ListenerEntry e = listeners; ListenerEntry p = null; while (e!=null) { // Found match if (e.listener == listener) { // The match was the first entry of the linked list if (p==null) { listeners = e.next; return e; } // Some other entry, unlink e p.next = e.next; return e; } p = e; e = e.next; } return null; } protected void emitEvent(ListenerEntry le, Event e) { e.reference = ChildReference.concatenate(le.path, e.reference); le.emitEvent(e); } protected void emitEvents(ListenerEntry le, Collection events) { for (Event e : events) e.reference = ChildReference.concatenate(le.path, e.reference); le.emitEvents(events); } }