Jelajahi Sumber

fix: Events.failure
new: CheckedRunnable
change: IOHandle is interface now
refactor: uri: CHandleManager
new: ExceptionUtils: group, observe, consume

Ranides Atterwim 9 tahun lalu
induk
melakukan
6539da04fc

+ 5 - 0
assira/src/main/java/net/ranides/assira/events/Events.java

@@ -6,7 +6,9 @@
  */
  */
 package net.ranides.assira.events;
 package net.ranides.assira.events;
 
 
+import java.io.IOException;
 import java.io.Serializable;
 import java.io.Serializable;
+import net.ranides.assira.io.IOEvent;
 
 
 /**
 /**
  *
  *
@@ -35,6 +37,9 @@ public final class Events {
     }
     }
 
 
     public static Failure failure(Throwable cause) {
     public static Failure failure(Throwable cause) {
+        if(cause instanceof IOException) {
+            return IOEvent.failure((IOException)cause);
+        }
         return new Failure(cause);
         return new Failure(cause);
     }
     }
 
 

+ 28 - 0
assira/src/main/java/net/ranides/assira/functional/CheckedRunnable.java

@@ -0,0 +1,28 @@
+/*
+ * @author Ranides Atterwim <ranides@gmail.com>
+ * @copyright Ranides Atterwim
+ * @license WTFPL
+ * @url http://ranides.net/projects/assira
+ */
+
+package net.ranides.assira.functional;
+
+import net.ranides.assira.trace.ExceptionUtils;
+
+/**
+ *
+ * @author Ranides Atterwim <ranides@gmail.com>
+ */
+public interface CheckedRunnable<E extends Exception> {
+
+    void run() throws E;
+    
+    default void $run() {
+        try {
+            run();
+        } catch(Exception cause) {
+            throw ExceptionUtils.rethrow(cause);
+        }
+    }
+
+}

+ 9 - 64
assira/src/main/java/net/ranides/assira/io/IOHandle.java

@@ -6,7 +6,6 @@
  */
  */
 package net.ranides.assira.io;
 package net.ranides.assira.io;
 
 
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import java.io.Closeable;
 import java.io.Closeable;
 import java.io.IOException;
 import java.io.IOException;
 import net.ranides.assira.functional.CheckedConsumer;
 import net.ranides.assira.functional.CheckedConsumer;
@@ -16,77 +15,23 @@ import net.ranides.assira.functional.CheckedSupplier;
  *
  *
  * @author Ranides Atterwim <ranides@gmail.com>
  * @author Ranides Atterwim <ranides@gmail.com>
  */
  */
-@SuppressWarnings({"DoubleCheckedLocking"})
-@SuppressFBWarnings({"DC_DOUBLECHECK","IS2_INCONSISTENT_SYNC"})
-public final class IOHandle<T> implements Closeable, CheckedSupplier<T, IOException> {
-    
-    private final CheckedSupplier<T,IOException> open;
-    private final CheckedConsumer<T,IOException> close;
-		
-    private SharedWrapper<T> ref = null;
+public interface IOHandle<T> extends Closeable, CheckedSupplier<T, IOException> {
 
 
-    public IOHandle(CheckedSupplier<T,IOException> open) {
-        this.close = (CheckedConsumer)(CheckedConsumer<Closeable, IOException>)Closeable::close;
-        this.open = open;
+    static <T> IOHandle<T> shared(CheckedConsumer<T,IOException> close, CheckedSupplier<T,IOException> open) {
+        return new IOUtils.SharedHandle<>(close, open);
     }
     }
-    
-    public IOHandle(CheckedConsumer<T,IOException> close, CheckedSupplier<T,IOException> open) {
-        this.close = close;
-        this.open = open;
+
+    static <T> IOHandle<T> shared(CheckedSupplier<T,IOException> open) {
+        return new IOUtils.SharedHandle<>(open);
     }
     }
     
     
     @Override
     @Override
-    public void close() throws IOException {
-        SharedWrapper<T> temp = ref;
-        if (temp == null) {
-            synchronized(this) {
-                temp = ref;
-                if (temp == null) {
-                    return;
-                }
-            }
-        }
-        if(null != ref.value) {
-            close.$accept(ref.value);
-        }
-    }
+    void close() throws IOException;
 
 
     @Override
     @Override
-    public T get() throws IOException {
-        SharedWrapper<T> temp = ref;
-        if (temp == null) {
-            synchronized(this) {
-                temp = ref;
-                if (temp == null) {
-                    ref = temp = new SharedWrapper<>(open.get());
-                }
-            }
-        }
-        return temp.value;
-    }
-    
-    public boolean opened() {
-        SharedWrapper<T> temp = ref;
-        if (temp == null) {
-            synchronized(this) {
-                temp = ref;
-                if (temp == null) {
-                    return false;
-                }
-            }
-        }
-        return null != temp.value;
-    }
+    T get() throws IOException;
     
     
-    private static final class SharedWrapper<T> {
-		
-		public final T value;
-
-		public SharedWrapper(T value) {
-			this.value = value;
-		}
-		
-	}
+    boolean opened();
     
     
     
     
 }
 }

+ 66 - 23
assira/src/main/java/net/ranides/assira/io/IOUtils.java

@@ -6,13 +6,13 @@
  */
  */
 package net.ranides.assira.io;
 package net.ranides.assira.io;
 
 
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import java.io.Closeable;
 import java.io.Closeable;
 import java.io.IOException;
 import java.io.IOException;
-import java.util.List;
 import java.util.function.Consumer;
 import java.util.function.Consumer;
-import net.ranides.assira.collection.lists.RTList;
 import net.ranides.assira.events.EventListener;
 import net.ranides.assira.events.EventListener;
-import net.ranides.assira.events.Events;
+import net.ranides.assira.functional.CheckedConsumer;
+import net.ranides.assira.functional.CheckedSupplier;
 import net.ranides.assira.trace.ExceptionUtils;
 import net.ranides.assira.trace.ExceptionUtils;
 import net.ranides.assira.trace.LoggerUtils;
 import net.ranides.assira.trace.LoggerUtils;
 
 
@@ -82,37 +82,80 @@ public final class IOUtils {
         }
         }
     }
     }
     
     
-    // @todo (assira #1) close-chain API
-    public static CloseFunction close() {
-        return new CloseFunction();
-    } 
-    
-    public static class CloseFunction implements AutoCloseable {
+    @SuppressWarnings({"DoubleCheckedLocking"})
+    @SuppressFBWarnings({"DC_DOUBLECHECK","IS2_INCONSISTENT_SYNC"})
+    static final class SharedHandle<T> implements IOHandle<T> {
         
         
-        private final List<IOException> list = new RTList<>();
+        private final CheckedSupplier<T,IOException> open;
         
         
-        public CloseFunction add(Closeable object) {
-            IOUtils.close(object, (Consumer<Exception>)e -> list.add((IOException)e));
-            return this;
+        private final CheckedConsumer<T,IOException> close;
+
+        private SharedWrapper<T> ref = null;
+
+        SharedHandle(CheckedSupplier<T,IOException> open) {
+            this.close = (CheckedConsumer)(CheckedConsumer<Closeable, IOException>)Closeable::close;
+            this.open = open;
+        }
+
+        SharedHandle(CheckedConsumer<T,IOException> close, CheckedSupplier<T,IOException> open) {
+            this.close = close;
+            this.open = open;
         }
         }
         
         
         @Override
         @Override
         public void close() throws IOException {
         public void close() throws IOException {
-            if(list.isEmpty()) {
-                return;
+            SharedWrapper<T> temp = ref;
+            if (temp == null) {
+                synchronized(this) {
+                    temp = ref;
+                    if (temp == null) {
+                        return;
+                    }
+                }
             }
             }
-            if(1 == list.size()) {
-                throw list.get(0);
+            if(null != ref.value) {
+                close.$accept(ref.value);
+            }
+        }
+
+        @Override
+        public T get() throws IOException {
+            SharedWrapper<T> temp = ref;
+            if (temp == null) {
+                synchronized(this) {
+                    temp = ref;
+                    if (temp == null) {
+                        ref = temp = new SharedWrapper<>(open.get());
+                    }
+                }
             }
             }
-            if(1 == list.size()) {
-                IOException out = new IOException("Multiple exception thrown at #close");
-                for(Exception e : list) {
-                    out.addSuppressed(e);
+            return temp.value;
+        }
+
+        @Override
+        public boolean opened() {
+            SharedWrapper<T> temp = ref;
+            if (temp == null) {
+                synchronized(this) {
+                    temp = ref;
+                    if (temp == null) {
+                        return false;
+                    }
                 }
                 }
-                throw out;
             }
             }
+            return null != temp.value;
         }
         }
         
         
+        private static final class SharedWrapper<T> {
+
+            public final T value;
+
+            public SharedWrapper(T value) {
+                this.value = value;
+            }
+
+        }
+
     }
     }
-    
+
 }
 }

+ 11 - 0
assira/src/main/java/net/ranides/assira/io/uri/URIBuilder.java

@@ -11,6 +11,7 @@ import java.net.URL;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Optional;
+import net.ranides.assira.collection.maps.MultiMap;
 
 
 /**
 /**
  *
  *
@@ -19,6 +20,16 @@ import java.util.Optional;
 public class URIBuilder {
 public class URIBuilder {
     
     
     // @todo (assira #2) URIBuilder
     // @todo (assira #2) URIBuilder
+    private String scheme;
+    private String host;
+    private Integer port;
+    private List<String> path;
+    
+    private String fragment;
+    private MultiMap<String,String> params;
+    
+    private String login;
+    private String password;
     
     
     public static URIBuilder from(String value) {
     public static URIBuilder from(String value) {
         return new URIBuilder();
         return new URIBuilder();

+ 0 - 3
assira/src/main/java/net/ranides/assira/io/uri/URIUtils.java

@@ -10,14 +10,11 @@ package net.ranides.assira.io.uri;
 import java.io.File;
 import java.io.File;
 import java.io.UnsupportedEncodingException;
 import java.io.UnsupportedEncodingException;
 import java.net.URI;
 import java.net.URI;
-import java.net.URISyntaxException;
 import java.net.URLDecoder;
 import java.net.URLDecoder;
 import java.net.URLEncoder;
 import java.net.URLEncoder;
 import java.nio.charset.Charset;
 import java.nio.charset.Charset;
 import java.util.Arrays;
 import java.util.Arrays;
 import java.util.Optional;
 import java.util.Optional;
-import java.util.logging.Level;
-import java.util.logging.Logger;
 import net.ranides.assira.collection.maps.MapCollectors;
 import net.ranides.assira.collection.maps.MapCollectors;
 import net.ranides.assira.collection.maps.MultiMap;
 import net.ranides.assira.collection.maps.MultiMap;
 import net.ranides.assira.text.Charsets;
 import net.ranides.assira.text.Charsets;

+ 2 - 1
assira/src/main/java/net/ranides/assira/io/uri/impl/CClassPathHandler.java

@@ -9,6 +9,7 @@ package net.ranides.assira.io.uri.impl;
 import java.io.IOException;
 import java.io.IOException;
 import java.net.URI;
 import java.net.URI;
 import java.nio.file.Path;
 import java.nio.file.Path;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collection;
 import java.util.Optional;
 import java.util.Optional;
 import net.ranides.assira.io.uri.URIHandle;
 import net.ranides.assira.io.uri.URIHandle;
@@ -36,7 +37,7 @@ public class CClassPathHandler implements URIHandler {
 
 
     @Override
     @Override
     public Collection<String> schemes() {
     public Collection<String> schemes() {
-        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+        return Arrays.asList("classpath");
     }
     }
     
     
     private class CClassPathHandle extends CHandle {
     private class CClassPathHandle extends CHandle {

+ 29 - 91
assira/src/main/java/net/ranides/assira/io/uri/impl/CFTPHandler.java

@@ -17,7 +17,6 @@ import java.nio.file.Paths;
 import java.time.Instant;
 import java.time.Instant;
 import java.util.*;
 import java.util.*;
 import net.ranides.assira.collection.maps.GenericMap;
 import net.ranides.assira.collection.maps.GenericMap;
-import net.ranides.assira.collection.maps.HashMap;
 import net.ranides.assira.collection.query.CQuery;
 import net.ranides.assira.collection.query.CQuery;
 import net.ranides.assira.collection.query.CQueryBuilder;
 import net.ranides.assira.collection.query.CQueryBuilder;
 import net.ranides.assira.credentials.UserProperty;
 import net.ranides.assira.credentials.UserProperty;
@@ -40,7 +39,8 @@ public class CFTPHandler implements URIHandler {
     
     
     private final URIResolver resolver;
     private final URIResolver resolver;
     
     
-    private final Map<FTPHostName, FTPHost> connections = new HashMap<>();
+    private final FTPManager man = new FTPManager();
+        
     
     
     public CFTPHandler() {
     public CFTPHandler() {
         this.resolver = URIResolver.DEFAULT;
         this.resolver = URIResolver.DEFAULT;
@@ -78,12 +78,12 @@ public class CFTPHandler implements URIHandler {
             this.charset = Charset.forName(charset.orElse("UTF-8"));
             this.charset = Charset.forName(charset.orElse("UTF-8"));
             this.text = charset.isPresent();
             this.text = charset.isPresent();
             this.hostname = new FTPHostName(uri, resolver.users());
             this.hostname = new FTPHostName(uri, resolver.users());
-            iopenHandle();
+            man.open(hostname);
         }
         }
 
 
         @Override
         @Override
         public void close() throws IOException {
         public void close() throws IOException {
-            icloseHandle();
+            man.close(hostname);
             super.close();
             super.close();
         }
         }
 
 
@@ -100,7 +100,7 @@ public class CFTPHandler implements URIHandler {
         @Override
         @Override
         public void move(URIHandle target) throws IOException {
         public void move(URIHandle target) throws IOException {
             if(isSameHost(target)) {
             if(isSameHost(target)) {
-                iclient().rename(path, target.uri().getPath());
+                man.shared(hostname).rename(path, target.uri().getPath());
             } else {
             } else {
                 super.move(target);
                 super.move(target);
             }
             }
@@ -108,35 +108,35 @@ public class CFTPHandler implements URIHandler {
 
 
         @Override
         @Override
         public void create() throws IOException {
         public void create() throws IOException {
-            iclient().storeFile(path, IOStreams.EMPTY);
+            man.shared(hostname).storeFile(path, IOStreams.EMPTY);
         }
         }
 
 
         @Override
         @Override
         public void delete() throws IOException {
         public void delete() throws IOException {
-            iclient().deleteFile(path);
+            man.shared(hostname).deleteFile(path);
         }
         }
 
 
         @Override
         @Override
         public OutputStream ostream() throws IOException {
         public OutputStream ostream() throws IOException {
-            FTPClient fc = iopenStream();
+            FTPClient fc = man.fork(hostname);
             return new OutputStreamWrapper(fc.storeFileStream(path)){
             return new OutputStreamWrapper(fc.storeFileStream(path)){
                 @Override
                 @Override
                 public void close() throws IOException {
                 public void close() throws IOException {
                     super.close();
                     super.close();
-                    icloseStream(fc);
+                    man.close(hostname, fc);
                 }
                 }
             };
             };
         }
         }
 
 
         @Override
         @Override
         public InputStream istream(long begin, long end) throws IOException {
         public InputStream istream(long begin, long end) throws IOException {
-            FTPClient fc = iopenStream();
+            FTPClient fc = man.fork(hostname);
             fc.setRestartOffset(begin);
             fc.setRestartOffset(begin);
             return new InputStreamWrapper(IOStreams.limit(fc.retrieveFileStream(path), 0, end)){
             return new InputStreamWrapper(IOStreams.limit(fc.retrieveFileStream(path), 0, end)){
                 @Override
                 @Override
                 public void close() throws IOException {
                 public void close() throws IOException {
                     super.close();
                     super.close();
-                    icloseStream(fc);
+                    man.close(hostname, fc);
                 }
                 }
             };
             };
         }
         }
@@ -148,7 +148,7 @@ public class CFTPHandler implements URIHandler {
 
 
         @Override
         @Override
         public CQuery<URIHandle> scan() throws IOException {
         public CQuery<URIHandle> scan() throws IOException {
-            FTPFile[] files = iclient().listFiles(path, FTPFileFilters.NON_NULL);
+            FTPFile[] files = man.shared(hostname).listFiles(path, FTPFileFilters.NON_NULL);
             return CQueryBuilder.fromArray(files).map(f -> new CFTPHandle(ireplace(uri, f.getName())));
             return CQueryBuilder.fromArray(files).map(f -> new CFTPHandle(ireplace(uri, f.getName())));
         }
         }
         
         
@@ -223,7 +223,7 @@ public class CFTPHandler implements URIHandler {
         }
         }
                 
                 
         private FTPFile stat(String path) throws IOException {
         private FTPFile stat(String path) throws IOException {
-            FTPFile ret = iclient().mlistFile(path);
+            FTPFile ret = man.shared(hostname).mlistFile(path);
             if(null == ret || !ret.isValid()) {
             if(null == ret || !ret.isValid()) {
                 return null;
                 return null;
             }
             }
@@ -243,77 +243,6 @@ public class CFTPHandler implements URIHandler {
         private boolean isSameHost(URIHandle target) {
         private boolean isSameHost(URIHandle target) {
             return (target instanceof CFTPHandle) && ((CFTPHandle)target).hostname.equals(hostname);
             return (target instanceof CFTPHandle) && ((CFTPHandle)target).hostname.equals(hostname);
         }
         }
-        
-        private FTPClient iclient() throws IOException {
-            synchronized(connections) {
-                FTPHost host = connections.get(hostname);
-                if(host == null) {
-                    connections.put(hostname, host = new FTPHost());
-                }
-                if(null == host.free) {
-                    host.free = hostname.connect();
-                }
-                return host.free;
-            }
-        }
-
-        private FTPClient iopenStream() throws IOException {
-            synchronized(connections) {
-                FTPHost host = connections.get(hostname);
-                if(host == null) {
-                    connections.put(hostname, host = new FTPHost());
-                }
-
-                FTPClient ret;
-                if(null != host.free) {
-                    host.data.add(ret = host.free);
-                    host.free = null;
-                } else {
-                    host.data.add(ret = hostname.connect());
-                }
-                return ret;
-            }
-        }
-
-        private void icloseStream(FTPClient fc) throws IOException {
-            synchronized(connections) {
-                FTPHost host = connections.get(hostname);
-                if(host == null) {
-                    return;
-                }
-                if(!host.data.remove(fc)) {
-                    return;
-                }
-                if(null == host.free && 0 != host.counter) {
-                    (host.free = fc).completePendingCommand();
-                } else {
-                    fc.disconnect();
-                }
-            }
-        }
-
-        private void iopenHandle() {
-            synchronized(connections) {
-                FTPHost host = connections.get(hostname);
-                if(host == null) {
-                    connections.put(hostname, host = new FTPHost());
-                }
-                host.counter++;
-            }
-        }
-
-        public void icloseHandle() throws IOException {
-            synchronized(connections) {
-                FTPHost host = connections.get(hostname);
-                if(host == null) {
-                    return;
-                }
-                if(0 == --host.counter && null != host.free) {
-                    host.free.disconnect();
-                    host.free = null;
-                }
-            }
-        }
 
 
     }
     }
 
 
@@ -407,17 +336,26 @@ public class CFTPHandler implements URIHandler {
 
 
             return ftp;
             return ftp;
         }
         }
-
         
         
     }
     }
     
     
-    private static final class FTPHost {
-        
-        public FTPClient free;
-        
-        public Set<FTPClient> data = new HashSet<>();
+    private static final class FTPManager extends CHandleManager<FTPHostName, FTPClient> {
         
         
-        private int counter;
+        @Override
+        protected void disconnect(FTPClient host) throws IOException {
+            host.disconnect();
+        }
+
+        @Override
+        protected void reset(FTPClient host) throws IOException {
+            host.completePendingCommand();
+        }
 
 
+        @Override
+        protected FTPClient connect(FTPHostName hostname) throws IOException {
+            return hostname.connect();
+        }
+        
     }
     }
+    
 }
 }

+ 6 - 2
assira/src/main/java/net/ranides/assira/io/uri/impl/CFileHandler.java

@@ -82,8 +82,8 @@ public class CFileHandler implements URIHandler {
             this.path = path;
             this.path = path;
             this.charset = Charset.forName(charset.orElse("UTF-8"));
             this.charset = Charset.forName(charset.orElse("UTF-8"));
             this.text = charset.isPresent();
             this.text = charset.isPresent();
-            this.istream = new IOHandle<>(() -> Files.newInputStream(path));
-            this.ostream = new IOHandle<>(() -> Files.newOutputStream(path));
+            this.istream = IOHandle.shared(() -> Files.newInputStream(path));
+            this.ostream = IOHandle.shared(() -> Files.newOutputStream(path));
         }
         }
 
 
         @Override
         @Override
@@ -136,7 +136,11 @@ public class CFileHandler implements URIHandler {
             BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class);
             BasicFileAttributes attr = Files.readAttributes(path, BasicFileAttributes.class);
             Set<URIFlags> flags = URIFlags.collect();
             Set<URIFlags> flags = URIFlags.collect();
 
 
+            // @todo (assira #3) CFileHandler: test
+            // @todo (assira #3) CFileHandler: URIFlags.EXISTS
+            
             if(attr.isDirectory()) {
             if(attr.isDirectory()) {
+                flags.add(URIFlags.DIR);
                 flags.add(URIFlags.EXISTS);
                 flags.add(URIFlags.EXISTS);
             }
             }
             if(attr.isRegularFile()) {
             if(attr.isRegularFile()) {

+ 232 - 232
assira/src/main/java/net/ranides/assira/io/uri/impl/CHTTPHandler.java

@@ -1,232 +1,232 @@
-/*
- * @author Ranides Atterwim <ranides@gmail.com>
- * @copyright Ranides Atterwim
- * @license WTFPL
- * @url http://ranides.net/projects/assira
- */
-package net.ranides.assira.io.uri.impl;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.HttpURLConnection;
-import java.net.MalformedURLException;
-import java.net.URI;
-import java.nio.charset.Charset;
-import java.time.Instant;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Date;
-import java.util.Set;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import javax.xml.bind.DatatypeConverter;
-import net.ranides.assira.collection.maps.GenericMap;
-import net.ranides.assira.collection.query.CQuery;
-import net.ranides.assira.credentials.UserProperty;
-import net.ranides.assira.io.IOHandle;
-import net.ranides.assira.io.IOStreams;
-import net.ranides.assira.io.uri.URITime;
-import net.ranides.assira.io.uri.URIFlags;
-import net.ranides.assira.io.uri.URIHandle;
-import net.ranides.assira.io.uri.URIHandler;
-import net.ranides.assira.io.uri.URIResolver;
-import net.ranides.assira.text.Charsets;
-import net.ranides.assira.text.StringTraits;
-import net.ranides.assira.trace.LoggerUtils;
-import org.slf4j.Logger;
-
-/**
- *
- * @author Ranides Atterwim <ranides@gmail.com>
- */
-public class CHTTPHandler implements URIHandler {
-    
-    private static final Logger LOGGER = LoggerUtils.getLogger();
-    
-    private static final Pattern RE_CHARSET = Pattern.compile(";[ ]*charset=([^; ]+)");
-
-    private final URIResolver resolver;
-    
-    public CHTTPHandler() {
-        this.resolver = URIResolver.DEFAULT;
-    }
-
-    public CHTTPHandler(URIResolver resolver) {
-        this.resolver = resolver;
-    }
-    
-    @Override
-    public URIHandle resolve(URI uri) throws IOException {
-        return new CHttpHandle(uri);
-    }
-
-    @Override
-    public Collection<String> schemes() {
-        return Arrays.asList("http","https");
-    }
-    
-    private class CHttpHandle extends CHandle {
-        
-        private final URI uri;
-        private final IOHandle<HttpURLConnection> head;
-        private final IOHandle<HttpURLConnection> get;
-        private final IOHandle<HttpURLConnection> post;
-
-        public CHttpHandle(URI uri) throws MalformedURLException {
-            this.uri = uri;
-            head = new IOHandle<>(() -> connect("HEAD"));
-            get = new IOHandle<>(() -> connect("GET"));
-            post = new IOHandle<>(() -> connect("POST"));
-        }
-
-        @Override
-        public URIResolver resolver() {
-            return resolver;
-        }
-        
-        @Override
-        public URI uri() throws IOException {
-            return uri;
-        }
-        
-        @Override
-        public Charset charset() throws IOException {
-            return getContentCharset(head().getContentType());
-        }
-
-        @Override
-        public Set<URIFlags> flags() throws IOException {
-            Set<URIFlags> re = URIFlags.collect(URIFlags.FILE);
-            try {
-                HttpURLConnection c = head();
-                if(isSuccess(c)) {
-                    re.add(URIFlags.EXISTS);
-                    re.add(URIFlags.READABLE);
-                }
-
-                if(StringTraits.starts(c.getContentType(), "text/")) {
-                    re.add(URIFlags.TEXT);
-                } else {
-                    re.add(URIFlags.BINARY);
-                }
-                if(c.getContentLengthLong() >= 0) {
-                    re.add(URIFlags.SIZE);
-                }
-                if(StringTraits.contains(c.getHeaderField("Accept-Ranges"), "bytes")) {
-                    re.add(URIFlags.SEEK);
-                }
-            } catch(IOException cause) {
-                LOGGER.warn("URI.flags: HTTP error", cause);
-            }
-            return re;
-        }
-
-        @Override
-        public Instant time(URITime ut) throws IOException {
-            if(ut == URITime.MODIFIED) {
-                HttpURLConnection c = head();
-                if(null == c.getHeaderField("Last-Modified")) {
-                    return Instant.ofEpochMilli(c.getDate());
-                }
-                return new Date(c.getHeaderFieldDate("Last-Modified", 0)).toInstant();
-            }
-            return super.time(ut);
-        }
-
-        @Override
-        public long size() throws IOException {
-            HttpURLConnection c = head();
-            if(2 != (c.getResponseCode() / 100)) {
-                throw new IOException(c.getResponseMessage());
-            }
-            return c.getContentLengthLong();
-        }
-
-        @Override
-        public InputStream istream() throws IOException {
-            return get.get().getInputStream();
-        }
-
-        @Override
-        public InputStream istream(long begin, long end) throws IOException {
-            HttpURLConnection c = get.get();
-            if(end == Long.MAX_VALUE) {
-                c.setRequestProperty("Range", "bytes=" + begin + "-");
-            } else {
-                c.setRequestProperty("Range", "bytes=" + begin + "-" + end);
-            }
-            if (c.getResponseCode() != HttpURLConnection.HTTP_PARTIAL) {
-                return IOStreams.limit(c.getInputStream(), begin, end);
-            } else {
-                return c.getInputStream();
-            }
-        }
-
-        @Override
-        public OutputStream ostream() throws IOException {
-            HttpURLConnection c = post.get();
-            c.connect();
-            return c.getOutputStream();
-        }
-
-        @Override
-        public void create() throws IOException {
-            execute("PUT");
-        }
-
-        @Override
-        public void delete() throws IOException {
-            execute("DELETE");
-        }
-        
-        private HttpURLConnection head() throws IOException {
-            return get.opened() ? get.get() : head.get();
-        }
-        
-        private void execute(String method) throws IOException {
-            HttpURLConnection c = connect(method);
-            boolean success = isSuccess(c);
-            c.disconnect();
-            if(!success) {
-                throw new IOException(c.getResponseCode() + ": " + c.getResponseMessage());
-            }
-        }
-        
-        private HttpURLConnection connect(String method) throws IOException {
-            HttpURLConnection c = (HttpURLConnection)uri.toURL().openConnection();
-            c.setRequestMethod(method);
-            
-            CQuery<GenericMap<String>> ui = resolver.users().match(uri);
-            if(!ui.isEmpty()) {
-                setAuthorizationHeader(c, ui.first());
-            }
-            return c;
-        }
-
-    }
-    
-    private static boolean isSuccess(HttpURLConnection c) throws IOException {
-        return 2 == (c.getResponseCode() / 100);
-    }
-    
-    private static Charset getContentCharset(String type) throws IOException {
-        if(null == type) {
-            throw new IOException("Unknown MIME type");
-        }
-        if(!type.startsWith("text/")) {
-            throw new IOException("Unsupported MIME type: " + type);
-        }
-        Matcher hit = RE_CHARSET.matcher(type);
-        if(!hit.find()) {
-            return Charsets.UTF8;
-        }
-        return Charset.forName(hit.group(1));
-    }
-    
-    private static void setAuthorizationHeader(HttpURLConnection connection, GenericMap<String> user) {
-        String passphrase = user.get(UserProperty.LOGIN) + ":" + user.get(UserProperty.PASSWORD);
-        String header = "Basic " + DatatypeConverter.printBase64Binary(passphrase.getBytes());
-        connection.setRequestProperty("Authorization", header);
-    }
-}
+/*
+ * @author Ranides Atterwim <ranides@gmail.com>
+ * @copyright Ranides Atterwim
+ * @license WTFPL
+ * @url http://ranides.net/projects/assira
+ */
+package net.ranides.assira.io.uri.impl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.nio.charset.Charset;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.xml.bind.DatatypeConverter;
+import net.ranides.assira.collection.maps.GenericMap;
+import net.ranides.assira.collection.query.CQuery;
+import net.ranides.assira.credentials.UserProperty;
+import net.ranides.assira.io.IOHandle;
+import net.ranides.assira.io.IOStreams;
+import net.ranides.assira.io.uri.URITime;
+import net.ranides.assira.io.uri.URIFlags;
+import net.ranides.assira.io.uri.URIHandle;
+import net.ranides.assira.io.uri.URIHandler;
+import net.ranides.assira.io.uri.URIResolver;
+import net.ranides.assira.text.Charsets;
+import net.ranides.assira.text.StringTraits;
+import net.ranides.assira.trace.LoggerUtils;
+import org.slf4j.Logger;
+
+/**
+ *
+ * @author Ranides Atterwim <ranides@gmail.com>
+ */
+public class CHTTPHandler implements URIHandler {
+    
+    private static final Logger LOGGER = LoggerUtils.getLogger();
+    
+    private static final Pattern RE_CHARSET = Pattern.compile(";[ ]*charset=([^; ]+)");
+
+    private final URIResolver resolver;
+    
+    public CHTTPHandler() {
+        this.resolver = URIResolver.DEFAULT;
+    }
+
+    public CHTTPHandler(URIResolver resolver) {
+        this.resolver = resolver;
+    }
+    
+    @Override
+    public URIHandle resolve(URI uri) throws IOException {
+        return new CHttpHandle(uri);
+    }
+
+    @Override
+    public Collection<String> schemes() {
+        return Arrays.asList("http","https");
+    }
+    
+    private class CHttpHandle extends CHandle {
+        
+        private final URI uri;
+        private final IOHandle<HttpURLConnection> head;
+        private final IOHandle<HttpURLConnection> get;
+        private final IOHandle<HttpURLConnection> post;
+
+        public CHttpHandle(URI uri) throws MalformedURLException {
+            this.uri = uri;
+            head = IOHandle.shared(() -> connect("HEAD"));
+            get = IOHandle.shared(() -> connect("GET"));
+            post = IOHandle.shared(() -> connect("POST"));
+        }
+
+        @Override
+        public URIResolver resolver() {
+            return resolver;
+        }
+        
+        @Override
+        public URI uri() throws IOException {
+            return uri;
+        }
+        
+        @Override
+        public Charset charset() throws IOException {
+            return getContentCharset(head().getContentType());
+        }
+
+        @Override
+        public Set<URIFlags> flags() throws IOException {
+            Set<URIFlags> re = URIFlags.collect(URIFlags.FILE);
+            try {
+                HttpURLConnection c = head();
+                if(isSuccess(c)) {
+                    re.add(URIFlags.EXISTS);
+                    re.add(URIFlags.READABLE);
+                }
+
+                if(StringTraits.starts(c.getContentType(), "text/")) {
+                    re.add(URIFlags.TEXT);
+                } else {
+                    re.add(URIFlags.BINARY);
+                }
+                if(c.getContentLengthLong() >= 0) {
+                    re.add(URIFlags.SIZE);
+                }
+                if(StringTraits.contains(c.getHeaderField("Accept-Ranges"), "bytes")) {
+                    re.add(URIFlags.SEEK);
+                }
+            } catch(IOException cause) {
+                LOGGER.warn("URI.flags: HTTP error", cause);
+            }
+            return re;
+        }
+
+        @Override
+        public Instant time(URITime ut) throws IOException {
+            if(ut == URITime.MODIFIED) {
+                HttpURLConnection c = head();
+                if(null == c.getHeaderField("Last-Modified")) {
+                    return Instant.ofEpochMilli(c.getDate());
+                }
+                return new Date(c.getHeaderFieldDate("Last-Modified", 0)).toInstant();
+            }
+            return super.time(ut);
+        }
+
+        @Override
+        public long size() throws IOException {
+            HttpURLConnection c = head();
+            if(2 != (c.getResponseCode() / 100)) {
+                throw new IOException(c.getResponseMessage());
+            }
+            return c.getContentLengthLong();
+        }
+
+        @Override
+        public InputStream istream() throws IOException {
+            return get.get().getInputStream();
+        }
+
+        @Override
+        public InputStream istream(long begin, long end) throws IOException {
+            HttpURLConnection c = get.get();
+            if(end == Long.MAX_VALUE) {
+                c.setRequestProperty("Range", "bytes=" + begin + "-");
+            } else {
+                c.setRequestProperty("Range", "bytes=" + begin + "-" + end);
+            }
+            if (c.getResponseCode() != HttpURLConnection.HTTP_PARTIAL) {
+                return IOStreams.limit(c.getInputStream(), begin, end);
+            } else {
+                return c.getInputStream();
+            }
+        }
+
+        @Override
+        public OutputStream ostream() throws IOException {
+            HttpURLConnection c = post.get();
+            c.connect();
+            return c.getOutputStream();
+        }
+
+        @Override
+        public void create() throws IOException {
+            execute("PUT");
+        }
+
+        @Override
+        public void delete() throws IOException {
+            execute("DELETE");
+        }
+        
+        private HttpURLConnection head() throws IOException {
+            return get.opened() ? get.get() : head.get();
+        }
+        
+        private void execute(String method) throws IOException {
+            HttpURLConnection c = connect(method);
+            boolean success = isSuccess(c);
+            c.disconnect();
+            if(!success) {
+                throw new IOException(c.getResponseCode() + ": " + c.getResponseMessage());
+            }
+        }
+        
+        private HttpURLConnection connect(String method) throws IOException {
+            HttpURLConnection c = (HttpURLConnection)uri.toURL().openConnection();
+            c.setRequestMethod(method);
+            
+            CQuery<GenericMap<String>> ui = resolver.users().match(uri);
+            if(!ui.isEmpty()) {
+                setAuthorizationHeader(c, ui.first());
+            }
+            return c;
+        }
+
+    }
+    
+    private static boolean isSuccess(HttpURLConnection c) throws IOException {
+        return 2 == (c.getResponseCode() / 100);
+    }
+    
+    private static Charset getContentCharset(String type) throws IOException {
+        if(null == type) {
+            throw new IOException("Unknown MIME type");
+        }
+        if(!type.startsWith("text/")) {
+            throw new IOException("Unsupported MIME type: " + type);
+        }
+        Matcher hit = RE_CHARSET.matcher(type);
+        if(!hit.find()) {
+            return Charsets.UTF8;
+        }
+        return Charset.forName(hit.group(1));
+    }
+    
+    private static void setAuthorizationHeader(HttpURLConnection connection, GenericMap<String> user) {
+        String passphrase = user.get(UserProperty.LOGIN) + ":" + user.get(UserProperty.PASSWORD);
+        String header = "Basic " + DatatypeConverter.printBase64Binary(passphrase.getBytes());
+        connection.setRequestProperty("Authorization", header);
+    }
+}

+ 110 - 0
assira/src/main/java/net/ranides/assira/io/uri/impl/CHandleManager.java

@@ -0,0 +1,110 @@
+/*
+ * @author Ranides Atterwim <ranides@gmail.com>
+ * @copyright Ranides Atterwim
+ * @license WTFPL
+ * @url http://ranides.net/projects/assira
+ */
+package net.ranides.assira.io.uri.impl;
+
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import net.ranides.assira.collection.maps.HashMap;
+
+/**
+ *
+ * @author Ranides Atterwim <ranides@gmail.com>
+ */
+public abstract class CHandleManager<P,C> {
+    
+    private final Map<P, CResource<C>> resources = new HashMap<>();
+    
+    protected abstract C connect(P path) throws IOException;
+    
+    protected abstract void disconnect(C channel) throws IOException;
+    
+    protected abstract void reset(C channel) throws IOException;
+    
+    public void open(P path) {
+        synchronized(resources) {
+            CResource<C> res = resources.get(path);
+            if(res == null) {
+                resources.put(path, res = new CResource());
+            }
+            res.counter++;
+        }
+    }
+
+    public void close(P path) throws IOException {
+        synchronized(resources) {
+            CResource<C> res = resources.get(path);
+            if(res == null) {
+                return;
+            }
+            if(0 == --res.counter && null != res.shared) {
+                disconnect(res.shared);
+                res.shared = null;
+            }
+        }
+    }
+
+    public C shared(P path) throws IOException {
+        synchronized(this) {
+            CResource<C> res = resources.get(path);
+            if(res == null) {
+                resources.put(path, res = new CResource());
+            }
+            if(null == res.shared) {
+                res.shared = connect(path);
+            }
+            return res.shared;
+        }
+    }
+    
+    public C fork(P path) throws IOException {
+        synchronized(resources) {
+            CResource<C> res = resources.get(path);
+            if(res == null) {
+                resources.put(path, res = new CResource());
+            }
+
+            C channel;
+            if(null != res.shared) {
+                res.forked.add(channel = res.shared);
+                res.shared = null;
+            } else {
+                res.forked.add(channel = connect(path));
+            }
+            return channel;
+        }
+    }
+    
+    public void close(P path, C channel) throws IOException {
+        synchronized(resources) {
+            CResource<C> res = resources.get(path);
+            if(res == null) {
+                return;
+            }
+            if(!res.forked.remove(channel)) {
+                return;
+            }
+            if(null == res.shared && 0 != res.counter) {
+                reset(res.shared = channel);
+            } else {
+                disconnect(channel);
+            }
+        }
+    }
+    
+    private static final class CResource<V> {
+        
+        public V shared;
+        
+        public Set<V> forked = new HashSet<>();
+        
+        private int counter;
+
+    }
+    
+}

+ 139 - 14
assira/src/main/java/net/ranides/assira/io/uri/impl/CJarHandler.java

@@ -7,25 +7,37 @@
 package net.ranides.assira.io.uri.impl;
 package net.ranides.assira.io.uri.impl;
 
 
 import java.io.File;
 import java.io.File;
+import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.IOException;
+import java.io.InputStream;
 import java.net.URI;
 import java.net.URI;
+import java.net.URISyntaxException;
 import java.nio.charset.Charset;
 import java.nio.charset.Charset;
-import java.nio.file.Path;
+import java.time.Instant;
 import java.util.Arrays;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collection;
 import java.util.Optional;
 import java.util.Optional;
+import java.util.Set;
+import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
 import java.util.zip.ZipFile;
+import net.ranides.assira.collection.query.CQuery;
+import net.ranides.assira.collection.query.CQueryBuilder;
 import net.ranides.assira.io.IOHandle;
 import net.ranides.assira.io.IOHandle;
-import net.ranides.assira.io.IOUtils;
+import net.ranides.assira.io.InputStreamWrapper;
+import net.ranides.assira.io.uri.URIFlags;
 import net.ranides.assira.io.uri.URIHandle;
 import net.ranides.assira.io.uri.URIHandle;
 import net.ranides.assira.io.uri.URIHandler;
 import net.ranides.assira.io.uri.URIHandler;
 import net.ranides.assira.io.uri.URIResolver;
 import net.ranides.assira.io.uri.URIResolver;
+import net.ranides.assira.io.uri.URITime;
 import net.ranides.assira.io.uri.URIUtils;
 import net.ranides.assira.io.uri.URIUtils;
+import net.ranides.assira.trace.ExceptionUtils;
 
 
 
 
 // @todo (assira #1) uri: JAR Handler
 // @todo (assira #1) uri: JAR Handler
 public class CJarHandler implements URIHandler {
 public class CJarHandler implements URIHandler {
     
     
+    private final JARManager man = new JARManager();
+    
     private final URIResolver resolver;
     private final URIResolver resolver;
     
     
     public CJarHandler() {
     public CJarHandler() {
@@ -46,15 +58,26 @@ public class CJarHandler implements URIHandler {
         return Arrays.asList("jar");
         return Arrays.asList("jar");
     }
     }
     
     
+    private static URI concat(File jar, String file) {
+        try {
+            return new URI("jar", jar+"?file="+file, null);
+        } catch (URISyntaxException cause) {
+            throw ExceptionUtils.rethrow(cause);
+        }
+    }
+    
     private class CJarHandle extends CHandle {
     private class CJarHandle extends CHandle {
         
         
         public final URI uri;
         public final URI uri;
-//        public final Path jar;
+        public final File jar;
         public final String file;
         public final String file;
         public final Charset charset;
         public final Charset charset;
         public final boolean text;
         public final boolean text;
         
         
-        public final IOHandle<ZipFile> zip;
+        // @todo (assira #1) cache'owanie ZipFile identycznie jak FTPClient
+//        public final IOHandle<ZipFile> zip;
+        public final IOHandle<ZipEntry> entry;
+//        public final IOHandle<InputStream> istream;
 
 
         public CJarHandle(URI uri) {
         public CJarHandle(URI uri) {
             this(uri, URIUtils.getFile(uri), URIUtils.getParam(uri, "file").get(), URIUtils.getParam(uri, "charset"));
             this(uri, URIUtils.getFile(uri), URIUtils.getParam(uri, "file").get(), URIUtils.getParam(uri, "charset"));
@@ -62,23 +85,22 @@ public class CJarHandler implements URIHandler {
 
 
         public CJarHandle(URI uri, File jar, String file, Optional<String> charset) {
         public CJarHandle(URI uri, File jar, String file, Optional<String> charset) {
             this.uri = uri;
             this.uri = uri;
+            this.jar = jar;
             this.file = file;
             this.file = file;
             this.charset = Charset.forName(charset.orElse("UTF-8"));
             this.charset = Charset.forName(charset.orElse("UTF-8"));
             this.text = charset.isPresent();
             this.text = charset.isPresent();
-//            this.istream = new IOHandle<>(() -> Files.newInputStream(path));
-//            this.ostream = new IOHandle<>(() -> Files.newOutputStream(path));
-            this.zip = new IOHandle<>(() -> new ZipFile(jar));
+            this.entry = IOHandle.shared(() -> man.shared(jar).getEntry(file));
+            man.open(jar);
+
+        }
+        
+        public CJarHandle(File jar, String file) {
+            this(concat(jar, file), jar, file, Optional.empty());
         }
         }
         
         
         @Override
         @Override
         public void close() throws IOException {
         public void close() throws IOException {
-            try {
-                IOUtils.close()
-                    .add(zip)
-                    .close();
-            } finally {
-                super.close();
-            }
+            man.close(jar);
         }
         }
         
         
         @Override
         @Override
@@ -101,7 +123,110 @@ public class CJarHandler implements URIHandler {
             return charset;
             return charset;
         }
         }
         
         
+        @Override
+        public boolean exists() throws IOException {
+            man.shared(jar).getEntry(file);
+            return null != entry.get();
+        }
+        
+        @Override
+        public Set<URIFlags> flags() throws IOException {
+            ZipEntry attr = entry.get();
+            Set<URIFlags> flags = URIFlags.collect();
+            
+            if(null == attr) {
+                return flags;
+            }
+            if(attr.isDirectory()) {
+                flags.add(URIFlags.DIR);
+            } else {
+                flags.add(URIFlags.FILE);
+                flags.add(URIFlags.READABLE);
+            }
+            if(-1 != attr.getSize()) {
+                flags.add(URIFlags.SIZE);
+            }
+            if(text) {
+                flags.add(URIFlags.TEXT);
+            } else {
+                flags.add(URIFlags.BINARY);
+            }
+            return flags;
+        }
+        
+        @Override
+        public Instant time(URITime ut) throws IOException {
+            ZipEntry attr = info();
+            
+            switch(ut) {
+                case ACCESSED:
+                    return attr.getLastAccessTime().toInstant();
+                case CREATED:
+                    return attr.getCreationTime().toInstant();
+                case MODIFIED:
+                    return attr.getLastModifiedTime().toInstant();
+                case META_MODIFIED:
+                    throw new UnsupportedOperationException("Not supported attribute: META_MODIFIED");
+                default:
+                    throw new UnsupportedOperationException("Not supported attribute: " + ut);
+            }
+        }
+
+        @Override
+        public long size() throws IOException {
+            return info().getSize();
+        }
         
         
+        @Override
+        public URIHandle parent() throws IOException {
+            return new CJarHandle(jar, new File(file).getParent());
+        }
+        
+        @Override
+        public CQuery<URIHandle> scan() throws IOException {
+            return CQueryBuilder
+                .fromStream(() -> man.shared(jar).stream().filter(e -> e.getName().startsWith(file)))
+                .map(e -> new CJarHandle(jar, e.getName()));
+        }
+
+        @Override
+        public InputStream istream() throws IOException {
+            InputStream is = man.shared(jar).getInputStream(entry.get());
+            return new InputStreamWrapper(is){
+                @Override
+                public void close() throws IOException {
+                    super.close();
+                    man.close(jar);
+                }
+            };
+        }
+        
+        private ZipEntry info() throws FileNotFoundException, IOException {
+            ZipEntry attr = entry.get();
+            if(null == attr) {
+                throw new FileNotFoundException("File not found: " + file);
+            }
+            return attr;
+        }
+        
+    }
+    
+    private static final class JARManager extends CHandleManager<File, ZipFile> {
+        
+        @Override
+        protected void disconnect(ZipFile zip) throws IOException {
+            zip.close();
+        }
+
+        @Override
+        protected void reset(ZipFile zip) throws IOException {
+            // do nothing
+        }
+
+        @Override
+        protected ZipFile connect(File path) throws IOException {
+            return new ZipFile(path);
+        }
         
         
     }
     }
 }
 }

+ 173 - 78
assira/src/main/java/net/ranides/assira/trace/ExceptionUtils.java

@@ -1,78 +1,173 @@
-/*
- * @author Ranides Atterwim <ranides@gmail.com>
- * @copyright Ranides Atterwim
- * @license WTFPL
- * @url http://ranides.net/projects/assira
- */
-package net.ranides.assira.trace;
-
-import java.io.PrintWriter;
-import java.io.StringWriter;
-import java.util.List;
-import java.util.function.Consumer;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-import java.util.stream.StreamSupport;
-import net.ranides.assira.collection.iterators.ForwardSpliterator;
-
-/**
- *
- * @author ranides
- */
-public final class ExceptionUtils {
-
-    private ExceptionUtils() { }
-	
-	public static Stream<Throwable> getCauseStream(Throwable cause) {
-		return StreamSupport.stream(new CauseSpliterator(cause), false);
-	}
-	
-	public static List<Throwable> getCauseList(Throwable cause) {
-		return getCauseStream(cause).collect(Collectors.toList());
-    }
-
-    public static String getStackTrace(Throwable cause) {
-        StringWriter result = new StringWriter();
-        PrintWriter writer = new PrintWriter(result);
-        cause.printStackTrace(writer);
-        writer.flush();
-        return result.toString();
-    }
-
-    public static RuntimeException rethrow(Throwable cause) {
-        new ExceptionErasure<RuntimeException>().rethrow(cause);
-        
-        // not reachable, wyjątek bez wrappowania leci linię wyżej
-        throw new RuntimeException(cause);  // NOPMD
-    }
-    
-    private static class ExceptionErasure<T extends Throwable> { // NOPMD
-        
-        @SuppressWarnings("unchecked")
-        private void rethrow(Throwable exception) throws T {
-            throw (T) exception;
-        }
-
-    }
-	
-	private static final class CauseSpliterator extends ForwardSpliterator<Throwable> {
-		
-		private Throwable current;
-		
-		public CauseSpliterator(Throwable current) {
-			this.current = current;
-		}
-
-		@Override
-		public boolean tryAdvance(Consumer<? super Throwable> action) {
-            if(null == current) {
-                return false;
-            }
-            action.accept(current);
-            current = current.getCause();
-            return true;
-		}
-
-	}
-    
-}
+/*
+ * @author Ranides Atterwim <ranides@gmail.com>
+ * @copyright Ranides Atterwim
+ * @license WTFPL
+ * @url http://ranides.net/projects/assira
+ */
+package net.ranides.assira.trace;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+import net.ranides.assira.collection.iterators.ForwardSpliterator;
+import net.ranides.assira.events.EventListener;
+import net.ranides.assira.events.Events;
+import net.ranides.assira.functional.CheckedRunnable;
+
+/**
+ *
+ * @author ranides
+ */
+public final class ExceptionUtils {
+
+    private ExceptionUtils() { }
+    
+    public static String toString(Throwable cause) {
+        StringWriter out = new StringWriter();
+        cause.printStackTrace(new PrintWriter(out));
+        return out.toString();
+    }
+	
+	public static Stream<Throwable> getCauseStream(Throwable cause) {
+		return StreamSupport.stream(new CauseSpliterator(cause), false);
+	}
+	
+	public static List<Throwable> getCauseList(Throwable cause) {
+		return getCauseStream(cause).collect(Collectors.toList());
+    }
+
+    public static String getStackTrace(Throwable cause) {
+        StringWriter result = new StringWriter();
+        PrintWriter writer = new PrintWriter(result);
+        cause.printStackTrace(writer);
+        writer.flush();
+        return result.toString();
+    }
+
+    public static RuntimeException rethrow(Throwable cause) {
+        new ExceptionErasure<RuntimeException>().rethrow(cause);
+        
+        // not reachable, wyjątek bez wrappowania leci linię wyżej
+        throw new RuntimeException(cause);  // NOPMD
+    }
+    
+    private static class ExceptionErasure<T extends Throwable> { // NOPMD
+        
+        @SuppressWarnings("unchecked")
+        private void rethrow(Throwable exception) throws T {
+            throw (T) exception;
+        }
+
+    }
+	
+	private static final class CauseSpliterator extends ForwardSpliterator<Throwable> {
+		
+		private Throwable current;
+		
+		public CauseSpliterator(Throwable current) {
+			this.current = current;
+		}
+
+		@Override
+		public boolean tryAdvance(Consumer<? super Throwable> action) {
+            if(null == current) {
+                return false;
+            }
+            action.accept(current);
+            current = current.getCause();
+            return true;
+		}
+
+	}
+    
+    public static <E extends Exception> GroupExceptions<E> group(Supplier<E> supplier) {
+        return new GroupExceptions<>(supplier);
+    }
+    
+    public static <E extends Exception> ConsumeExceptions<E> consume(Consumer<E> consumer) {
+        return new ConsumeExceptions<>(consumer);
+    }
+    
+    public static <E extends Exception> ObserveExceptions observe(EventListener<? super Events.Failure> listener) {
+        return new ObserveExceptions(listener);
+    }
+    
+    public static class ObserveExceptions {
+        
+        private final EventListener<? super Events.Failure> listener;
+        
+        ObserveExceptions(EventListener<? super Events.Failure> listener) {
+            this.listener = listener;
+        }
+        
+        public <T extends Exception> ObserveExceptions run(CheckedRunnable<T> action) {
+            try {
+                action.run();
+            } catch(Exception cause) {
+                listener.handleEvent(Events.failure(cause));
+            }
+            return this;
+        }
+        
+    }
+    
+    public static class ConsumeExceptions<E extends Exception> {
+        
+        private final Consumer<E> consumer;
+        
+        ConsumeExceptions(Consumer<E> consumer) {
+            this.consumer = consumer;
+        }
+        
+        public ConsumeExceptions<E> run(CheckedRunnable<E> action) {
+            try {
+                action.run();
+            } catch(Exception cause) {
+                consumer.accept((E)cause);
+            }
+            return this;
+        }
+        
+    }
+    
+    public static class GroupExceptions<E extends Exception> {
+    
+        private final Supplier<E> supplier;
+        
+        private E exception;
+        
+        GroupExceptions(Supplier<E> supplier) {
+            this.supplier = supplier;
+        }
+        
+        public <T extends Exception> GroupExceptions<E> run(CheckedRunnable<T> action) {
+            try {
+                action.run();
+            } catch(Exception cause) {
+                if(exception == null) {
+                    exception = supplier.get();
+                }
+                exception.addSuppressed(cause);
+            }
+            return this;
+        }
+        
+        public Optional<E> cause() {
+            return Optional.ofNullable(exception);
+        }
+        
+        public void rethrow() throws E {
+            if(null != exception) {
+                throw exception;
+            }
+        }
+        
+    }
+    
+}

+ 120 - 68
assira/src/test/java/net/ranides/assira/trace/ExceptionUtilsTest.java

@@ -1,68 +1,120 @@
-/*
- * @author Ranides Atterwim <ranides@gmail.com>
- * @copyright Ranides Atterwim
- * @license WTFPL
- * @url http://ranides.net/projects/assira
- */
-package net.ranides.assira.trace;
-
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.List;
-import java.util.stream.Collectors;
-import org.junit.Test;
-import static net.ranides.assira.junit.NewAssert.*;
-
-/**
- *
- * @author Ranides Atterwim <ranides@gmail.com>
- */
-public class ExceptionUtilsTest {
-    
-    public ExceptionUtilsTest() {
-    }
-
-    @Test
-    public void testCauseStream() {
-        Exception e = new IOException(new RuntimeException(new IllegalArgumentException()));
-        
-        List<String> s = ExceptionUtils.getCauseStream(e).map(f -> f.getClass().getName()).collect(Collectors.toList());
-        List<String> expected = Arrays.asList(
-            "java.io.IOException", 
-            "java.lang.RuntimeException", 
-            "java.lang.IllegalArgumentException"
-        );
-        assertEquals(expected, s);
-    }
-    
-    @Test
-    public void testCauseList() {
-        Exception e = new IOException(new RuntimeException(new IllegalArgumentException()));
-        
-        List<Throwable> s = ExceptionUtils.getCauseList(e);
-        List<Throwable> expected = Arrays.asList(
-            e, 
-            e.getCause(), 
-            e.getCause().getCause()
-        );
-        assertEquals(expected, s);
-    }
-    
-    @Test
-    public void testStacktrace() {
-        String st = ExceptionUtils.getStackTrace(new IOException());
-        assertTrue(st.contains("ExceptionUtilsTest"));
-        assertTrue(st.contains("org.junit."));
-        assertTrue(st.contains("org.apache.maven."));
-    }
-    
-    @Test
-    public void testRethrow() {
-        assertThrows(IOException.class, this::rm);
-    }
-    
-    private void rm() {
-        ExceptionUtils.rethrow(new IOException());
-    }
-   
-}
+/*
+ * @author Ranides Atterwim <ranides@gmail.com>
+ * @copyright Ranides Atterwim
+ * @license WTFPL
+ * @url http://ranides.net/projects/assira
+ */
+package net.ranides.assira.trace;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+import org.junit.Test;
+import static net.ranides.assira.junit.NewAssert.*;
+
+/**
+ *
+ * @author Ranides Atterwim <ranides@gmail.com>
+ */
+@SuppressWarnings("PMD.AvoidThrowingRawExceptionTypes")
+public class ExceptionUtilsTest {
+
+    @Test
+    public void testCauseStream() {
+        Exception e = new IOException(new RuntimeException(new IllegalArgumentException()));
+        
+        List<String> s = ExceptionUtils.getCauseStream(e).map(f -> f.getClass().getName()).collect(Collectors.toList());
+        List<String> expected = Arrays.asList(
+            "java.io.IOException", 
+            "java.lang.RuntimeException", 
+            "java.lang.IllegalArgumentException"
+        );
+        assertEquals(expected, s);
+    }
+    
+    @Test
+    public void testCauseList() {
+        Exception e = new IOException(new RuntimeException(new IllegalArgumentException()));
+        
+        List<Throwable> s = ExceptionUtils.getCauseList(e);
+        List<Throwable> expected = Arrays.asList(
+            e, 
+            e.getCause(), 
+            e.getCause().getCause()
+        );
+        assertEquals(expected, s);
+    }
+    
+    @Test
+    public void testStacktrace() {
+        String st = ExceptionUtils.getStackTrace(new IOException());
+        assertTrue(st.contains("ExceptionUtilsTest"));
+        assertTrue(st.contains("org.junit."));
+        assertTrue(st.contains("org.apache.maven."));
+    }
+    
+    @Test
+    public void testRethrow() {
+        assertThrows(IOException.class, this::rm);
+    }
+    
+    @Test
+    public void testGroup() {
+        AtomicInteger value = new AtomicInteger(0);
+        try {
+            
+            ExceptionUtils.group(IOException::new)
+                .run(() -> { value.incrementAndGet(); })
+                .run(() -> { throw new RuntimeException("hello"); })
+                .run(() -> { value.incrementAndGet(); })
+                .run(() -> { throw new RuntimeException("world"); })
+                .rethrow();
+            
+            fail("IOException expected");
+        } catch(IOException cause) {
+            String text = ExceptionUtils.toString(cause);
+            assertTrue(text.contains("Suppressed: java.lang.RuntimeException: hello"));
+            assertTrue(text.contains("Suppressed: java.lang.RuntimeException: world"));
+            assertEquals(2, value.get());
+        }
+    }
+    
+    
+    @Test
+    public void testObserve() {
+        AtomicInteger value = new AtomicInteger(0);
+        List<String> out = new ArrayList<>();
+        
+        ExceptionUtils.observe(e -> out.add(e.cause().toString()))
+            .run(() -> { value.incrementAndGet(); })
+            .run(() -> { throw new RuntimeException("hello"); })
+            .run(() -> { value.incrementAndGet(); })
+            .run(() -> { throw new RuntimeException("world"); });
+        
+        assertEquals("[java.lang.RuntimeException: hello, java.lang.RuntimeException: world]", out.toString());
+        assertEquals(2, value.get());
+    }
+    
+    @Test
+    public void testConsume() {
+        AtomicInteger value = new AtomicInteger(0);
+        List<String> out = new ArrayList<>();
+        
+        ExceptionUtils.consume(e -> out.add(e.toString()))
+            .run(() -> { value.incrementAndGet(); })
+            .run(() -> { throw new RuntimeException("hello"); })
+            .run(() -> { value.incrementAndGet(); })
+            .run(() -> { throw new RuntimeException("world"); });
+        
+        assertEquals("[java.lang.RuntimeException: hello, java.lang.RuntimeException: world]", out.toString());
+        assertEquals(2, value.get());
+    }
+    
+    private void rm() {
+        ExceptionUtils.rethrow(new IOException());
+    }
+   
+}