Browse Source

- pierwszy TestNG
- ColorUtlis: zmiana implementacji "asHexString"
- CollectionUtils: - dodano "equalContent" i kilka adaptujących iteratory/kolekcje
- dodano klasę collection.map.Cache

- collection.MapAdapter: thread-safe kosztem wydajności (brak współdzielonego Entry)
- collection.MapAdapter: poprawiony iterator values(), współpracuje z mapami, które się "złośliwie modyfikują" (np podczas odczytu)
- collection.MapAdapter: poprawiona metoda toString dla Entry

- LocalReader: tworzenie anonimowego readera za pomocą fromStream

- MathUtils: podebrana metoda "signum(long)"

- usunięto: CollectionUtils.compareSize
- usunięto: ComponentInspector (przeniesiony do assira.ui)

- StringLoader: dodano metodę "fromReader"
- StringLoader: dodano metodę "loadProperties"
- StringLoader: zmiana argumentów charset ze String na Charset
- StringLoader: fromStream - poprawne zamykanie strumienia

- StringUtils: dodano parę nowych funkcji
- StringUtils: nCopies - zwraca obiekt CharSequence, który poprawnie się konwertuje na String
- Strings: isEmpty oraz isAssigned działa z CharSequence, a nie tylko String

Ranides Atterwim 13 years ago
parent
commit
9fede16bfe

+ 8 - 1
pom.xml

@@ -4,7 +4,7 @@
 
     <groupId>net.ranides</groupId>
     <artifactId>assira</artifactId>
-    <version>0.49</version>
+    <version>0.50</version>
     <packaging>jar</packaging>
 
     <name>assira</name>
@@ -79,5 +79,12 @@
             <artifactId>annotations</artifactId>
             <version>1.0.0</version>
         </dependency>
+        <dependency>
+            <groupId>org.testng</groupId>
+            <artifactId>testng</artifactId>
+            <version>6.5.2</version>
+            <scope>test</scope>
+            <type>jar</type>
+        </dependency>
     </dependencies>
 </project>

+ 1 - 1
src/main/java/net/ranides/assira/awt/ColorUtils.java

@@ -73,7 +73,7 @@ public final class ColorUtils {
      * @return
      */
     public static String asHexString(Color color) {
-        return Strings.sprintf("#%02x%02x%02x", color.getRed(), color.getGreen(), color.getBlue());
+        return Strings.sprintf("#%06x", color.getRGB() & 0xFFFFFF );
     }
     
     private static int clip(int value) {

+ 72 - 18
src/main/java/net/ranides/assira/collection/CollectionUtils.java

@@ -8,8 +8,14 @@
 package net.ranides.assira.collection;
 
 import java.util.AbstractCollection;
+import java.util.AbstractSet;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Iterator;
+import java.util.Set;
+import net.ranides.assira.generic.Function;
 
 /**
  * Generyczne operacje na kolekcjach. Jeśli to możliwe, korzystać z innych klas 
@@ -199,24 +205,6 @@ public final class CollectionUtils {
         return 0 == size(values);
     }
 
-    /**
-     * Porównuje rozmiary kolekcji.
-     * <table style="margin: 10px 0 0 0; background: #c0F0c0; border: solid 1px #006000;" cellpadding="0"><tr><td>{@code null} safe method</td></tr></table>
-     * @param <T1>
-     * @param <T2>
-     * @param a
-     * @param b
-     * @return
-     * <table>
-     *  <tr><td>ujemne</td><td> - jeśli a &lt; b</td></tr>
-     *  <tr><td>dodatnie</td><td> - jeśli a &gt; b </td></tr>
-     *  <tr><td>dodatnie</td><td> - jeśli a == b </td></tr>
-     * </table>
-     */
-    public static <T1,T2> int compareSize(Collection<T1> a, Collection<T2> b) {
-        return size(a) - size(b);
-    }
-
     /**
      * Sprawdza, czy kolekcje mają ten sam rozmiar.
      * Kolekcje, które są równe {@code null} traktowane są jako puste (rozmiar 0).
@@ -231,6 +219,33 @@ public final class CollectionUtils {
     public static <T1,T2> boolean equalSize(Collection<T1> a, Collection<T2> b) {
         return size(a) == size(b);
     }
+    
+    /**
+     * Sprawdza, czy kolekcje zawierają te same elementy ignorując kolejność.
+     * Uwaga: bardzo kosztowna metoda, ponieważ kopiuje a następnie sortuje obie
+     * kolekcje.
+     * @param <T>
+     * @param a
+     * @param b
+     * @return 
+     */
+    public static <T> boolean equalContent(Collection<T> a, Collection<T> b) {
+        if( a.size() != b.size() ){
+            return false;
+        }
+        // najpierw spróbujmy tak sprawdzić - takie porównanie powinno być względnie tanie
+        // (złożoność O(N)) w porówaniu do następnej metody
+        if( a.equals(b) || b.equals(a) ) {
+            
+        }
+        // dużo kopiowania, na dodatek mamy sortowanie, czyli złożoność 
+        // na pamięć O(N) oraz operacje O(N*log(N)) + O(N)
+        Object[] asort = a.toArray();
+        Object[] bsort = b.toArray();
+        Arrays.sort(asort);
+        Arrays.sort(bsort);
+        return Arrays.equals(asort, bsort);
+    }
 
     /**
      * Przycina kolekcję jeśli kolekcja ma rozmiar większy od podanej granicy.
@@ -310,4 +325,43 @@ public final class CollectionUtils {
         };
     }
     
+    public static <T> Collection<T> asCollection(final int size, final Iterator<T> iterator) {
+        return new AbstractCollection<T>() {
+            @Override
+            public Iterator<T> iterator() { return iterator(); }
+            @Override
+            public int size() { return size; }
+        };
+    }
+    
+    public static <T,S> Iterator<T> adapt(final Iterator<S> iterator, final Function<T,S> function) {
+        return new Iterator<T>() {
+            @Override
+            public boolean hasNext() {
+                return iterator.hasNext();
+            }
+            @Override
+            public T next() {
+                return function.apply(iterator.next());
+            }
+            @Override
+            public void remove() {
+                iterator.remove();
+            }
+        };
+    }
+    
+    public static <T,S> Collection<T> adapt(final Collection<S> collection, final Function<T,S> function) {
+        return new AbstractCollection<T>() {
+            @Override
+            public Iterator<T> iterator() {
+                return adapt(collection.iterator(), function);
+            }
+            @Override
+            public int size() {
+                return collection.size();
+            }
+        };
+    }
+    
 }

+ 199 - 0
src/main/java/net/ranides/assira/collection/map/Cache.java

@@ -0,0 +1,199 @@
+/*
+ * @author Ranides Atterwim <ranides@gmail.com>
+ * @copyright Ranides Atterwim
+ * @license WTFPL
+ * @url http://ranides.net/projects/assira
+ */
+package net.ranides.assira.collection.map;
+
+import java.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.SoftReference;
+import java.util.AbstractMap;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ *
+ * @author ranides
+ */
+@SuppressWarnings("element-type-mismatch")
+public class Cache<K,V> extends AbstractMap<K,V> {
+
+    private final CacheMap<K,V> map;
+
+    private final CacheList<V> list;
+    
+    // Wszystkie widoki są niebezpieczne w tym sensie, że nie można wywoływać
+    // *żadnej* metody na obiekcie cache w czasie iterowania po widoku. cache 
+    // uwzględnia czas dostępu do obiektów, nawet odczyt zmienia kolejność w LinkedHashMap.
+    // Zawsze zostanie rzucone ConcurentModificationException, jeśli podczas iteracji
+    // zostanie jakakolwiek metoda. 
+    //
+    // może zrobić ciekawy interfejs (oraz implementację): LockingIterator. Taki
+    // iterator blokowałby kolekcję, dopóki nie doiterowałby do końca zbioru.
+    // po osiągnięciu końca zwalniałby lock
+    private final Map<K,V> view;
+    
+    private final ReferenceQueue<V> queue = new ReferenceQueue<V>();
+
+    public Cache() {
+        // Trzymamy maksymalnie 1024 ostatnio używanych[*] elementów.
+        // Przez "użycie" rozumiemy zapis lub odczyt. Nie względniając GC, 
+        // który może nam je wyczyścić. Przed usnięciem przez GC chronimy 128 
+        // ostatnio czytanych elementów. W szczególności taka ochrona pozwala
+        // polegać na metodzie "containsKey", która chroni wartość przed 
+        // natychmiastowym usunięciem - jeśli stwierdzono, że klucz istnieje.
+        this(1024, 32);
+        
+    }
+    
+    public Cache(int cachesize) {
+        this(cachesize, cachesize);
+    }
+
+    public Cache(int cachesize, int holdsize) {
+        this.list   = new CacheList<V>(Math.max(1,holdsize));
+        this.map    = new CacheMap<K,V>(cachesize);
+        this.view   = new CacheView<K, V>(map);
+    }
+
+    @Override
+    public boolean containsKey(Object key) {
+        return null != get(key);
+    }
+
+    @Override
+    public V get(Object key) {
+        Reference<V> ref = map.get(key);
+        if(ref == null) {
+            return null;
+        }
+        // From the SoftReference we get the value, which can be
+        // null if it was removed in the processQueue() method defined below
+        V result = ref.get();
+        if (result == null) {
+            map.remove(key);
+        } else {
+            // hold value for a while...
+            list.hold(result);
+        }
+        return result;
+    }
+
+    @Override
+    public V put(K key, V value) {
+        release();
+        V prev = asValue(map.get(key));
+        map.put(key, new ReferenceEntry<K,V>(key, value, queue));
+        return prev;
+    }
+
+    @Override
+    public V remove(Object key) {
+        release();
+        return asValue(map.remove(key));
+    }
+
+    @Override
+    public void clear() {
+        list.clear();
+        release();
+        map.clear();
+    }
+
+    @Override
+    public int size() {
+        release();
+        return map.size();
+    }
+    
+    @Override
+    public Set<Entry<K,V>> entrySet() {
+        release();
+        return view.entrySet();
+    }
+
+    @Override
+    public Set<K> keySet() {
+        release();
+        return view.keySet();
+    }
+
+    @Override
+    public Collection<V> values() {
+        release();
+        return view.values();
+    }
+    
+    private void release() {
+        Reference<? extends V> ref;
+        while ( null != (ref = queue.poll()) ) { map.remove( asKey(ref) ); }
+    }
+    
+    private static <TV> TV asValue(Reference<? extends TV> ref) {
+        return (ref != null) ? ref.get() : null;
+    }
+    
+    @SuppressWarnings("unchecked")
+    private static <TK> TK asKey(Reference<?> ref) {
+        return (ref != null) ? ((ReferenceEntry<TK,?>)ref).key : null;
+    }
+    
+    private static class ReferenceEntry<KT,VT> extends SoftReference<VT> {
+        
+        final KT key;
+
+        ReferenceEntry(KT key, VT value, ReferenceQueue<VT> queue) {
+            super(value, queue);
+            this.key = key;
+        }
+    }
+    
+    private static class CacheMap<TK,TV> extends LinkedHashMap<TK, Reference<TV>> {
+        
+        final int maxsize;
+
+        CacheMap(int maxsize) {
+            super( Math.round(maxsize / 0.75f)+2, 0.75f, true);
+            this.maxsize = maxsize;
+        }
+
+        @Override
+        protected boolean removeEldestEntry(Entry<TK, Reference<TV>> eldest) {
+            return size() > maxsize;
+        }
+        
+    };
+    
+    private static class CacheList<TV> extends LinkedList<TV> {
+        
+        final int maxsize;
+
+        CacheList(int maxsize) {
+            this.maxsize = maxsize;
+        }
+        
+        void hold(TV value) {
+            addFirst(value);
+            if (size() > maxsize) { removeLast(); }
+        }
+        
+    }
+    
+    private static class CacheView<TK,TV> extends MapAdapter<TK, Reference<TV>, TV> {
+
+        CacheView(Map<TK, Reference<TV>> content) {
+            super(content);
+        }
+
+        @Override
+        public TV apply(Entry<TK, Reference<TV>> source) {
+            return asValue(source.getValue());
+        }
+    }
+
+}

+ 16 - 8
src/main/java/net/ranides/assira/collection/map/MapAdapter.java

@@ -53,8 +53,7 @@ import net.ranides.assira.generic.Function;
 public abstract class MapAdapter<K, VS, VT> implements Map<K, VT>, Function<VT, Entry<K,VS>> {
 
     final Map<K, VS> content;
-    final EntrySource<K,VS> appEntry = new EntrySource<K, VS>();
-
+    
     /**
      * Tworzy nowy adapter odwzorowujący podaną mapę.
      * @param content
@@ -86,7 +85,7 @@ public abstract class MapAdapter<K, VS, VT> implements Map<K, VT>, Function<VT,
 
     @Override
     public VT get(Object key) {
-        return apply(appEntry.set( key, this.content.get(key) ));
+        return apply(new EntryConst<K,VS>(key, this.content.get(key) ));
     }
 
     @Override
@@ -96,7 +95,7 @@ public abstract class MapAdapter<K, VS, VT> implements Map<K, VT>, Function<VT,
 
     @Override
     public VT remove(Object key) {
-        return apply(appEntry.set(key, this.content.remove(key) ));
+        return apply(new EntryConst<K,VS>(key, this.content.remove(key) ));
     }
 
     @Override
@@ -140,10 +139,11 @@ public abstract class MapAdapter<K, VS, VT> implements Map<K, VT>, Function<VT,
 
     private class ValueIterator implements Iterator<VT> {
 
-        private final Iterator<K> iterator;
+        private final EntrySource<K,VS> shentry = new EntrySource<K, VS>();
+        private final Iterator<Entry<K,VS>> iterator;
 
         public ValueIterator() {
-            iterator = content.keySet().iterator();
+            iterator = content.entrySet().iterator();
         }
 
         @Override
@@ -153,8 +153,8 @@ public abstract class MapAdapter<K, VS, VT> implements Map<K, VT>, Function<VT,
 
         @Override
         public VT next() {
-            K key = iterator.next();
-            return apply(appEntry.set(key, content.get(key) ));
+            Entry<K,VS> entry = iterator.next();
+            return apply(shentry.set(entry.getKey(), entry.getValue() ));
         }
 
         @Override
@@ -255,6 +255,10 @@ public abstract class MapAdapter<K, VS, VT> implements Map<K, VT>, Function<VT,
             this.key = (KK)key;
             this.val = val;
         }
+        @Override
+        public String toString() {
+            return key + "=" + val;
+        }
     }
 
     final static class EntrySource<KK,VV> implements Entry<KK,VV> {
@@ -278,6 +282,10 @@ public abstract class MapAdapter<K, VS, VT> implements Map<K, VT>, Function<VT,
             this.val = val;
             return this;
         }
+        @Override
+        public String toString() {
+            return key + "=" + val;
+        }
     }
 
 

+ 1 - 1
src/main/java/net/ranides/assira/collection/map/MapBiAdapter.java

@@ -119,7 +119,7 @@ public abstract class MapBiAdapter<K, VS, VT> extends MapAdapter<K, VS, VT> impl
     @Override
     public VT put(K key, VT value) {
         VS result = this.content.put(key, inverse(invEntry.set(key, value)) );
-        return null == result ? null : apply(appEntry.set(key, result));
+        return null == result ? null : apply(new EntryConst<K, VS>(key, result));
     }
 
 }

+ 8 - 0
src/main/java/net/ranides/assira/io/LocalReader.java

@@ -48,6 +48,14 @@ public class LocalReader extends Reader {
         return new LocalReader(path, new StringReader(content));
     }
     
+    public static LocalReader fromStream(InputStream istream) {
+        return fromStream(new File("anonymous"), istream, TextEncoding.NIO_UTF8);
+    }
+    
+    public static LocalReader fromStream(InputStream istream, Charset charset) {
+        return new LocalReader(new File("anonymous"), new InputStreamReader(istream, charset));
+    }
+    
     public static LocalReader fromStream(File path, InputStream istream) {
         return fromStream(path, istream, TextEncoding.NIO_UTF8);
     }

+ 42 - 0
src/main/java/net/ranides/assira/math/MathUtils.java

@@ -48,5 +48,47 @@ public final class MathUtils {
         assert min <= max;
         return Math.round(value < min ? min : value > max ? max : value);
     }
+    
+    /**
+     * alternate to signum for use in compare. Not a true signum, since it
+     * returns ints other than +/-1. Where there is any possibility of overflow,
+     * you should compare two longs with < rather than subtraction. 
+     * <p>
+     * In Pentium assembler you could implement this algorthm with following code:
+     * <pre>
+     *  # diff = edx:eax 
+     *  # result = eax
+     *  mov ebx,eax
+     *  shl eax,1
+     *  or  eax,ebx
+     *  slr eax,1
+     *  or  eax,edx
+     * </pre>
+     * which would take 5 cycles, 2 more that lohi.  However, JET did even better,
+     * with code essentially this using a clever trick to implement piotr.
+     * <pre>
+     *   lea    ecx,0(eax,eax)  ; shifts lo left by doubling, keeps copy of
+     *   lo
+     *   or     eax,ecx
+     *   shr    eax,1
+     *   or     eax,edx
+     * </pre>
+     * This is 4 cycles, still one more than lohi. Why was Piotr so much
+     * faster on JET? Peter has no pipeline-confounding jumps. Further, the lo 
+     * then high operands actually come from the ram-based stack. Piotr nicely 
+     * separates the accesses giving plenty of for pre-emptive fetch of hi. 
+     * lohi insists on having them both upfront, so it has to wait for memory 
+     * access. Piotr does not have to wait. Modern CPUS hurry up and wait for 
+     * RAM most of the time.
+     * </p>
+     * @param diff number to be collapsed to an int preserving sign and
+     * zeroness. usually represents the difference of two long.
+     *
+     * @return sign of {@code diff}, some {@code int < 0}, 0 or some {@code int > 0}.
+     * @author Peter Kobzda
+     */
+    public static int signum(long diff) {
+        return (int) (diff >>> 32) | ((int) diff | (int) diff << 1) >>> 1;
+    }
 }
 

+ 0 - 189
src/main/java/net/ranides/assira/reflection/ComponentInspector.java

@@ -1,189 +0,0 @@
-/*
- *  @author Ranides Atterwim <ranides@gmail.com>
- *  @copyright Ranides Atterwim
- *  @license WTFPL
- *  @url http://ranides.net/projects/assira
- */
-
-package net.ranides.assira.reflection;
-
-import java.awt.Component;
-import java.awt.Container;
-import javax.accessibility.Accessible;
-import javax.swing.*;
-import javax.swing.plaf.ComponentUI;
-import net.ranides.assira.text.StringCompiler;
-import net.ranides.assira.text.Strings;
-
-/**
- *
- * @author ranides
- */
-
-@SuppressWarnings("PMD.ShortVar")
-public final class ComponentInspector {
-
-    private ComponentInspector() { }
-
-    static String dumpControl(JButton control) {
-        return Strings.sprintf("%s%s ", dumpControl( (Component)control ), control.getText() );
-    }
-
-    static String dumpControl(JLabel control) {
-        return Strings.sprintf("%s%s ", dumpControl( (Component)control ), control.getText() );
-    }
-
-    static String dumpControl(JPanel control) {
-        return Strings.sprintf("%s[%d:%d:%d:%d] ",
-                dumpControl( (Component)control ),
-                control.getLocation().x,
-                control.getLocation().y,
-                control.getWidth(),
-                control.getHeight()
-                );
-    }
-
-    static String dumpControl(Component control) {
-        StringBuilder result = new StringBuilder();
-        result.append( control.getClass().getName() ).append(" - ");
-        if(null != control.getName()) {
-            result.append(StringCompiler.quote( control.getName() ) );
-        }
-        return result.toString();
-    }
-
-    /* ********************************************************************** */
-
-    /**
-     * Wyrzuca na konsolę listę wszystkich komponentów zawartych w kontenerze.
-     * Wynik jest prezentowany w formie zagnieżdżonej listy numerowanej.
-     * @param container kontener
-     * @param indent początkowe wcięcie (lewy margines) listy
-     */
-    public static String iterate(Container container, String indent) {
-        StringBuilder output = new StringBuilder();
-        int max = container.getComponentCount();
-
-        for(int i = 0; i<max; i++) {
-            Component child = container.getComponent(i);
-            output.append(indent).append(i).append(". ");
-            if(child instanceof JButton) {
-                output.append( dumpControl( (JButton)child ) );
-            }
-            else if(child instanceof JPanel) {
-                output.append( dumpControl( (JPanel)child ) );
-            }
-            else if(child instanceof JLabel) {
-                output.append( dumpControl( (JLabel)child ) );
-            }
-            else {
-                output.append( dumpControl( child ) );
-            }
-
-            output.append("\n");
-
-            if(child instanceof Container) {
-                iterate((Container)child, "   "+indent+i+".");
-            }
-        }
-        return output.toString();
-    }
-
-    @edu.umd.cs.findbugs.annotations.SuppressWarnings({
-        "DE_MIGHT_IGNORE", 
-        "DLS_DEAD_LOCAL_STORE", 
-        "REC_CATCH_EXCEPTION"
-    })
-    public static String iterate(ComponentUI ui, JComponent container, String indent) {
-        StringBuilder output = new StringBuilder();
-        int max = ui.getAccessibleChildrenCount(container);
-
-        for(int i = 0; i<max; i++) {
-            Accessible child = ui.getAccessibleChild(container, i);
-            output.append(indent).append(i).append(". ");
-
-            if(child instanceof JButton) {
-                output.append( dumpControl( (JButton)child ) );
-            }
-            else if(child instanceof JPanel) {
-                output.append( dumpControl( (JPanel)child ) );
-            }
-            else if(child instanceof JLabel) {
-                output.append( dumpControl( (JLabel)child ) );
-            }
-            else {
-                try { output.append( dumpControl( (Component)child ) ); }
-                catch(Exception _) { /* ignore and continue enumeration */ } // NOPMD
-            }
-
-            output.append("\n");
-
-            if(child instanceof Container) {
-                output.append( iterate((Container)child, "   "+indent+i+".") );
-            }
-        }
-        return output.toString();
-    }
-
-    /* ********************************************************************** */
-
-    /**
-     * Zwraca komponent leżący w kontenerze. Struktura jest traktowana jako drzewo,
-     * a indeks określa numer węzła na każdym kolejnym poziomie. Przykładowo
-     * tablica {5,7} wybiera 5. element w pojemniku, następnie niego wybiera 7.
-     * i zwraca jako wynik. Można podać ścieżkę o dowolnej długości.
-     * @param container kontener
-     * @param index numery węzłów, które mają zostać pobrane na kolejnych poziomach zagnieżdżenia
-     * @return element albo null, jeśli nie istnieje
-     */
-    @edu.umd.cs.findbugs.annotations.SuppressWarnings({
-        "DLS_DEAD_LOCAL_STORE"
-    })
-    public static Component find(Container container, int... index) {
-        try {
-            Component current = container;
-            for(int i=0; i<index.length; i++) { current = ((Container)current).getComponent( index[i] ); }
-            return current;
-        } catch(RuntimeException _) {
-            return null;
-        }
-    }
-
-    public static <T extends Component> T find(Class<T> clazz, Container container, int... index) {
-        Component result = find(container, index);
-        return clazz.isInstance(result) ? clazz.cast(result) : null;
-    }
-
-    @SuppressWarnings({"unchecked", "PMD"})
-    public static <T extends Component> T $find(Container container, int[] index) {
-        return (T)find(container, index);
-    }
-
-    /* ********************************************************************** */
-
-    @edu.umd.cs.findbugs.annotations.SuppressWarnings({"BC_UNCONFIRMED_CAST", "DLS_DEAD_LOCAL_STORE"})
-    public static JComponent find(ComponentUI ui, Accessible container, int... index) {
-        try {
-            JComponent icurrent = (JComponent)container;
-            ComponentUI iui = ui;
-            for(int i=0; i<index.length; i++) {
-                icurrent = (JComponent)iui.getAccessibleChild( icurrent, index[i]);
-                iui = UIManager.getUI(icurrent);
-            }
-            return icurrent;
-        } catch(Exception _) {
-            return null;
-        }
-    }
-
-    public static <T extends JComponent> T find(Class<T> clazz, ComponentUI ui, Accessible container, int... index) {
-        JComponent result = find(ui, container, index);
-        return clazz.isInstance(result) ? clazz.cast(result) : null;
-    }
-
-    @SuppressWarnings({"unchecked","PMD"})
-    public static <T extends JComponent> T $find(ComponentUI ui, Accessible container, int[] index) {
-        return (T)find(ui, container, index);
-    }
-
-}

+ 59 - 10
src/main/java/net/ranides/assira/text/StringLoader.java

@@ -7,10 +7,16 @@
 
 package net.ranides.assira.text;
 
+import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
+import java.io.IOException;
 import java.io.InputStream;
+import java.io.Reader;
+import java.io.StreamTokenizer;
+import java.nio.charset.Charset;
+import java.util.LinkedHashMap;
 import java.util.Scanner;
 
 /**
@@ -21,20 +27,30 @@ public final class StringLoader {
 
     private StringLoader() { }
 
+    public static String fromReader(Reader reader) {
+        Scanner scanner = new Scanner(reader).useDelimiter("\\Z");
+        try {
+            return scanner.next();
+        } finally {
+            scanner.close();
+        }
+    }
 
-    public static String fromStream(InputStream istream, String charset) {
-        Scanner scanner = new Scanner(istream, charset).useDelimiter("\\Z");
-        String contents = scanner.next();
-        scanner.close();
-        return contents;
+    public static String fromStream(InputStream istream, Charset charset) {
+        Scanner scanner = new Scanner(istream, charset.name()).useDelimiter("\\Z");
+        try {
+            return scanner.next();
+        } finally {
+            scanner.close();
+        }
     }
 
     public static String fromStream(InputStream istream) {
-        return fromStream(istream, TextEncoding.NIO_UTF8.name() );
+        return fromStream(istream, TextEncoding.NIO_UTF8);
     }
 
 
-    public static String fromFile(File file, String charset) {
+    public static String fromFile(File file, Charset charset) {
         try {
             return fromStream( new FileInputStream(file), charset );
         } catch (FileNotFoundException _) {
@@ -43,15 +59,48 @@ public final class StringLoader {
     }
 
     public static String fromFile(File file) {
-        return fromFile(file, TextEncoding.NIO_UTF8.name() );
+        return fromFile(file, TextEncoding.NIO_UTF8);
     }
 
-    public static String fromFile(String path, String charset) {
+    public static String fromFile(String path, Charset charset) {
         return fromFile(new File(path), charset);
     }
 
     public static String fromFile(String path) {
-        return fromFile(path, TextEncoding.NIO_UTF8.name() );
+        return fromFile(path, TextEncoding.NIO_UTF8 );
     }
 
+    /**
+     * Ładuje plik ".properties", zachowując kolejność wpisów.
+     */
+    public static LinkedHashMap<String,String> loadProperties(Reader reader) throws IOException {
+        try {
+            LinkedHashMap<String,String> target = new LinkedHashMap<String,String>();
+            StreamTokenizer tokenizer =new StreamTokenizer(new BufferedReader(reader));
+
+            // treat space, alpha, numbers and most punctuation as ordinary char
+            tokenizer.wordChars(' ', '_');
+            tokenizer.commentChar('#');
+            tokenizer.whitespaceChars('=', '=');// ignore equal, just separates fields
+            tokenizer.eolIsSignificant(true);
+            
+            while (true) {
+                tokenizer.nextToken();
+                if (tokenizer.ttype == StreamTokenizer.TT_EOF) { 
+                    return target;
+                }
+                if (tokenizer.ttype == StreamTokenizer.TT_EOL) { 
+                    continue; 
+                }
+                String key = tokenizer.sval.trim();
+                tokenizer.nextToken();
+                String value = tokenizer.sval.trim();
+                target.put(key, value);
+            }
+
+        } finally {        
+            reader.close();
+        }
+    }
+    
 }

+ 90 - 6
src/main/java/net/ranides/assira/text/StringUtils.java

@@ -19,6 +19,8 @@ public final class StringUtils {
     
     private static final Pattern  REGEX_SPECIAL = Pattern.compile("([\\\\*+\\[\\](){}\\$.?\\^|])");
     
+    private static final CharSequence[] EMPTY = new CharSequence[0];
+    
     private StringUtils() { /* utility class */ }
     
     public static String remove(String text, String fragment) {
@@ -41,6 +43,49 @@ public final class StringUtils {
         }
         return new String(buffer, 0, position);
     }
+    
+    public static String removePrefix(String text, String prefix) {
+        if( Strings.isEmpty(text)) {
+            return "";
+        }
+        if(text.startsWith(prefix)) {
+            return text.substring(prefix.length());
+        }
+        return text;
+    }
+    
+    public static String removeSuffix(String text, String suffix) {
+        if( Strings.isEmpty(text)) {
+            return "";
+        }
+        if(text.endsWith(suffix)) {
+            return text.substring(0, text.length() - suffix.length());
+        }
+        return text;
+    }
+    
+    public static String condense(String text, char value) {
+        final int size = text.length();
+        final char buffer[] = new char[size];
+        char c;
+        boolean pc = false;
+        int position = 0;
+        for(int i=0; i<size; i++) {
+            c = text.charAt(i);
+            if(c == value) {
+                if(!pc) {
+                    buffer[position++] = c; 
+                    pc = true;
+                } else {
+                    // skip character
+                }
+            } else {
+                buffer[position++] = c; 
+                pc = false;
+            }
+        }
+        return new String(buffer, 0, position);
+    }
 
     public static String ltrim(String source) {
         int n = source.length(), offset = 0;
@@ -100,8 +145,7 @@ public final class StringUtils {
 
     public static <T extends CharSequence> String join(Iterable<T> values, String separator) {
         if (values instanceof Collection) {
-            Collection<?> collection = (Collection<?>) values;
-            return join(collection.toArray(new CharSequence[collection.size()]), separator);
+            return join( ((Collection<?>)values).toArray(new CharSequence[0]), separator );
         }
         StringBuilder builder = new StringBuilder();
         boolean first = true;
@@ -115,8 +159,7 @@ public final class StringUtils {
 
     public static <T extends CharSequence> String join(Iterable<T> values) {
         if (values instanceof Collection) {
-            Collection<?> collection = (Collection<?>) values;
-            return join(collection.toArray(new CharSequence[collection.size()]));
+            return join( ((Collection<?>)values).toArray(new CharSequence[0]) );
         }
         StringBuilder builder = new StringBuilder();
         for(Object value : values) {
@@ -229,10 +272,34 @@ public final class StringUtils {
     public static String clip(String text, int size) {
         return text.substring(0, Math.min(size, text.length()));
     }
+    
+    public static String lpadd(String text, char padd, int size) {
+        int n = text.length();
+        int d = size - n;
+        if(d <= 0) {
+            return text;
+        }
+        char[] buffer = new char[size];
+        text.getChars(0, n, buffer, 0);
+        for(int i=n; i<size; i++) { buffer[i] = padd; }
+        return new String(buffer);
+    }
+    
+    public static String rpadd(String text, char padd, int size) {
+        int n = text.length();
+        int d = size - n;
+        if(d <= 0) {
+            return text;
+        }
+        char[] buffer = new char[size];
+        text.getChars(0, n, buffer, d);
+        for(int i=0; i<d; i++) { buffer[i] = padd; }
+        return new String(buffer);
+    }
 
     public static String lcommon(String value1, String value2) {
-        if(null==value1 || value1.isEmpty() ) { return ""; }
-        if(null==value2 || value2.isEmpty() ) { return ""; }
+        if( Strings.isEmpty(value1) ) { return ""; }
+        if( Strings.isEmpty(value2) ) { return ""; }
         int n = Math.min(value1.length(), value2.length());
         for(int i=0; i<n; i++) {
             if( value1.charAt(i)!=value2.charAt(i) ) { return value1.substring(0,i); }
@@ -342,10 +409,27 @@ public final class StringUtils {
         public CharSequence subSequence(int start, int end) {
             return new NCopies(text, start, end);
         }
+
+        @Override
+        public String toString() {
+            int n = length();
+            char[] data = new char[n];
+            for(int i=0; i<n; i++) { data[i] = charAt(i); }
+            return new String(data);
+        }
+        
     }
     
     public static String escapeRegex(String text) {
         return REGEX_SPECIAL.matcher(text).replaceAll("\\\\$1");
     }
+    
+    public static boolean contains(String text, char value) {
+        return text.indexOf(value) != -1;
+    }
+    
+    public static boolean contains(String text, CharSequence value) {
+        return text.contains(value);
+    }
    
 }

+ 4 - 4
src/main/java/net/ranides/assira/text/Strings.java

@@ -73,12 +73,12 @@ public final class Strings {
         return null;
     }
 
-    public static boolean isEmpty(String value) {
-        return (null==value) || value.isEmpty();
+    public static boolean isEmpty(CharSequence value) {
+        return (null==value) || value.length() == 0;
     }
 
-    public static boolean isAssigned(String value) {
-        return (null!=value) && !value.isEmpty();
+    public static boolean isAssigned(CharSequence value) {
+        return (null!=value) && value.length() != 0;
     }
     
     public static boolean isEqual(Object valueA, Object valueB) {

+ 171 - 0
src/test/java/net/ranides/assira/collection/map/CacheNGTest.java

@@ -0,0 +1,171 @@
+/*
+ * @author Ranides Atterwim <ranides@gmail.com>
+ * @copyright Ranides Atterwim
+ * @license WTFPL
+ * @url http://ranides.net/projects/assira
+ */
+package net.ranides.assira.collection.map;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import net.ranides.assira.collection.CollectionUtils;
+import static org.testng.Assert.*;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ *
+ * @author ranides
+ */
+public class CacheNGTest {
+    
+    public CacheNGTest() {
+    }
+    
+    @DataProvider(name = "basic")
+    public Object[][] createMap() {
+        // tworzymy cache, który chroni wszystkie swoje elementy przed GC
+        Map<Integer, String> cache = new Cache<Integer, String>(32);
+        for(int i=0; i<48; i++) {
+            cache.put(i, "BNGV-"+String.valueOf(i));
+        }
+        return new Object[][]{{ cache }};
+    }
+
+    @Test(dataProvider = "basic")
+    public void testContainsKey(Map<Integer, String> cache) {
+        assertFalse( cache.containsKey(5) );
+        assertFalse( cache.containsKey(12) );
+        assertFalse( cache.containsKey(15) );
+        
+        assertTrue( cache.containsKey(16) );
+        assertTrue( cache.containsKey(30) );
+        assertTrue( cache.containsKey(47) );
+        
+    }
+
+    @Test(dataProvider = "basic")
+    public void testGet(Map<Integer, String> cache) {
+        assertNull( cache.get(5) );
+        assertNull( cache.get(12) );
+        assertNull( cache.get(15) );
+        
+        assertEquals(cache.get(16), "BNGV-16");
+        assertEquals(cache.get(30), "BNGV-30");
+        assertEquals(cache.get(47), "BNGV-47");
+    }
+
+    @Test(dataProvider = "basic")
+    public void testRemove(Map<Integer, String> cache) {
+        assertTrue( cache.containsKey(32) );
+        cache.remove(32);
+        assertFalse( cache.containsKey(32) );
+        cache.remove(3);
+        cache.remove(300);
+        assertFalse( cache.containsKey(3) );
+        assertFalse( cache.containsKey(300) );
+    }
+
+    @Test(dataProvider = "basic")
+    public void testClear(Map<Integer, String> cache) {
+        cache.clear();
+        assertEquals(cache.size(), 0);
+        assertFalse( cache.containsKey(16) );
+        assertFalse( cache.containsKey(30) );
+        assertFalse( cache.containsKey(47) );
+    }
+
+    @Test(dataProvider = "basic")
+    public void testSize(Map<Integer, String> cache) {
+        assertEquals(cache.size(), 32);
+    }
+
+    @Test(dataProvider = "basic")
+    public void testEntrySet(Map<Integer, String> cache) {
+        Set<Entry<Integer,String>> entries = cache.entrySet();
+        assertEquals(entries.size(), cache.size());
+        
+        Map<Integer, String> copy1 = new HashMap<Integer, String>();
+        for(Entry<Integer,String> entry : entries) {
+            copy1.put(entry.getKey(), entry.getValue());
+        }
+        
+        assertEquals(copy1, cache);
+        
+        Map<Integer, String> copy2 = new HashMap<Integer, String>();
+        Iterator<Entry<Integer,String>> iterator = entries.iterator();
+        int i = 0;
+        while(iterator.hasNext()) {
+            Entry<Integer,String> entry = iterator.next();
+            copy2.put(entry.getKey(), entry.getValue());
+            if( i++%2 == 0 ) {
+                iterator.remove();
+            }
+        }
+        
+        assertEquals(cache.size(), copy1.size() / 2);
+        assertEquals(copy2, copy1);
+    }
+
+    @Test(dataProvider = "basic")
+    public void testKeySet(Map<Integer, String> cache) {
+        Set<Integer> keys = cache.keySet();
+        assertEquals(keys.size(), cache.size());
+        
+        Set<Integer> keys1 = new HashSet<Integer>(keys);
+        Map<Integer, String> copy1 = new HashMap<Integer, String>();
+        for(Integer key : keys1) {
+            copy1.put(key, cache.get(key));
+        }
+        
+        assertEquals(copy1, cache);
+
+        Iterator<Integer> iterator = keys.iterator();
+        int i = 0;
+        while(iterator.hasNext()) {
+            iterator.next();
+            if( i++%2 == 0 ) {
+                iterator.remove();
+            }
+        }
+        
+        assertEquals(cache.size(), copy1.size() / 2);
+    }
+
+    @Test(dataProvider = "basic")
+    public void testValues(Map<Integer, String> cache) {
+        Collection<String> values = cache.values();
+        assertEquals(values.size(), cache.size());
+        
+        Map<Integer, String> copy1 = new LinkedHashMap<Integer, String>(cache);
+        List<String> values1 = new ArrayList<String>(values);
+        
+        assertEquals(copy1.size(), cache.size());
+
+        // kolejność nie musi być zachowana
+        assertTrue( CollectionUtils.equalContent(copy1.values(), values) );
+        // kolejność nie musi być zachowana
+        assertTrue( CollectionUtils.equalContent(values1, values) );
+
+        Iterator<String> iterator = values.iterator();
+        int i = 0;
+        while(iterator.hasNext()) {
+            iterator.next();
+            if( i++%2 == 0 ) {
+                iterator.remove();
+            }
+        }
+        
+        assertEquals(cache.size(), copy1.size() / 2);
+    }
+    
+}

+ 1 - 1
src/test/java/net/ranides/assira/reflection/ReflectUtilsTest.java

@@ -24,7 +24,7 @@ public class ReflectUtilsTest {
     
 
     @Test
-    public void testSomeMethod() {
+    public void testMatcher() {
         new ReflectUtils.MethodMatcher("counter", new Object[0]).find( TObject.class.getMethods() );
         
         new ReflectUtils.MethodMatcher("rand", new Object[]{11.7f}).find( TObject.class.getMethods() );