Move package org.argeo.util to bundle org.argeo.enterprise.
authorMathieu Baudier <mbaudier@argeo.org>
Sun, 22 Nov 2020 18:32:22 +0000 (19:32 +0100)
committerMathieu Baudier <mbaudier@argeo.org>
Sun, 22 Nov 2020 18:32:22 +0000 (19:32 +0100)
50 files changed:
org.argeo.core/ext/test/org/argeo/util/CsvParserEncodingTest.java [deleted file]
org.argeo.core/ext/test/org/argeo/util/CsvParserParseFileTest.java [deleted file]
org.argeo.core/ext/test/org/argeo/util/CsvParserTest.java [deleted file]
org.argeo.core/ext/test/org/argeo/util/CsvParserWithQuotedSeparatorTest.java [deleted file]
org.argeo.core/ext/test/org/argeo/util/CsvWriterTest.java [deleted file]
org.argeo.core/ext/test/org/argeo/util/ReferenceFile.csv [deleted file]
org.argeo.core/ext/test/org/argeo/util/TestParse-ISO.csv [deleted file]
org.argeo.core/ext/test/org/argeo/util/TestParse-UTF-8.csv [deleted file]
org.argeo.core/ext/test/org/argeo/util/ThroughputTest.java [deleted file]
org.argeo.core/src/org/argeo/util/CsvParser.java [deleted file]
org.argeo.core/src/org/argeo/util/CsvParserWithLinesAsMap.java [deleted file]
org.argeo.core/src/org/argeo/util/CsvWriter.java [deleted file]
org.argeo.core/src/org/argeo/util/DictionaryKeys.java [deleted file]
org.argeo.core/src/org/argeo/util/DigestUtils.java [deleted file]
org.argeo.core/src/org/argeo/util/DirH.java [deleted file]
org.argeo.core/src/org/argeo/util/LangUtils.java [deleted file]
org.argeo.core/src/org/argeo/util/OS.java [deleted file]
org.argeo.core/src/org/argeo/util/PasswordEncryption.java [deleted file]
org.argeo.core/src/org/argeo/util/ServiceChannel.java [deleted file]
org.argeo.core/src/org/argeo/util/StreamUtils.java [deleted file]
org.argeo.core/src/org/argeo/util/Tester.java [deleted file]
org.argeo.core/src/org/argeo/util/TesterStatus.java [deleted file]
org.argeo.core/src/org/argeo/util/Throughput.java [deleted file]
org.argeo.core/src/org/argeo/util/UuidUtils.java [deleted file]
org.argeo.core/src/org/argeo/util/package-info.java [deleted file]
org.argeo.enterprise/ext/test/org/argeo/util/CsvParserEncodingTest.java [new file with mode: 0644]
org.argeo.enterprise/ext/test/org/argeo/util/CsvParserParseFileTest.java [new file with mode: 0644]
org.argeo.enterprise/ext/test/org/argeo/util/CsvParserTest.java [new file with mode: 0644]
org.argeo.enterprise/ext/test/org/argeo/util/CsvParserWithQuotedSeparatorTest.java [new file with mode: 0644]
org.argeo.enterprise/ext/test/org/argeo/util/CsvWriterTest.java [new file with mode: 0644]
org.argeo.enterprise/ext/test/org/argeo/util/ReferenceFile.csv [new file with mode: 0644]
org.argeo.enterprise/ext/test/org/argeo/util/TestParse-ISO.csv [new file with mode: 0644]
org.argeo.enterprise/ext/test/org/argeo/util/TestParse-UTF-8.csv [new file with mode: 0644]
org.argeo.enterprise/ext/test/org/argeo/util/ThroughputTest.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/util/CsvParser.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/util/CsvParserWithLinesAsMap.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/util/CsvWriter.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/util/DictionaryKeys.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/util/DigestUtils.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/util/DirH.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/util/LangUtils.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/util/OS.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/util/PasswordEncryption.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/util/ServiceChannel.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/util/StreamUtils.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/util/Tester.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/util/TesterStatus.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/util/Throughput.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/util/UuidUtils.java [new file with mode: 0644]
org.argeo.enterprise/src/org/argeo/util/package-info.java [new file with mode: 0644]

diff --git a/org.argeo.core/ext/test/org/argeo/util/CsvParserEncodingTest.java b/org.argeo.core/ext/test/org/argeo/util/CsvParserEncodingTest.java
deleted file mode 100644 (file)
index 09443c2..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-package org.argeo.util;
-
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.util.List;
-
-/** Tests that {@link CsvParser} can deal properly with encodings. */
-public class CsvParserEncodingTest {
-
-       private String iso = "ISO-8859-1";
-       private String utf8 = "UTF-8";
-
-       public void testParse() throws Exception {
-
-               String xml = new String("áéíóúñ,éééé");
-               byte[] utfBytes = xml.getBytes(utf8);
-               byte[] isoBytes = xml.getBytes(iso);
-
-               InputStream inUtf = new ByteArrayInputStream(utfBytes);
-               InputStream inIso = new ByteArrayInputStream(isoBytes);
-
-               CsvParser csvParser = new CsvParser() {
-                       protected void processLine(Integer lineNumber, List<String> header, List<String> tokens) {
-                               assert header.size() == tokens.size();
-                               assert 2 == tokens.size();
-                               assert "áéíóúñ".equals(tokens.get(0));
-                               assert "éééé".equals(tokens.get(1));
-                       }
-               };
-
-               csvParser.parse(inUtf, utf8);
-               inUtf.close();
-               csvParser.parse(inIso, iso);
-               inIso.close();
-       }
-}
diff --git a/org.argeo.core/ext/test/org/argeo/util/CsvParserParseFileTest.java b/org.argeo.core/ext/test/org/argeo/util/CsvParserParseFileTest.java
deleted file mode 100644 (file)
index 5a92c68..0000000
+++ /dev/null
@@ -1,25 +0,0 @@
-package org.argeo.util;
-
-import java.io.InputStream;
-import java.util.HashMap;
-import java.util.Map;
-
-/** Test that {@link CsvParser} can properly parse a CSV file. */
-public class CsvParserParseFileTest {
-       public void testParse() throws Exception {
-
-               final Map<Integer, Map<String, String>> lines = new HashMap<Integer, Map<String, String>>();
-               InputStream in = getClass().getResourceAsStream("/org/argeo/util/ReferenceFile.csv");
-               CsvParserWithLinesAsMap parser = new CsvParserWithLinesAsMap() {
-                       protected void processLine(Integer lineNumber, Map<String, String> line) {
-                               lines.put(lineNumber, line);
-                       }
-               };
-
-               parser.parse(in);
-               in.close();
-
-               assert 5 == lines.size();
-       }
-
-}
diff --git a/org.argeo.core/ext/test/org/argeo/util/CsvParserTest.java b/org.argeo.core/ext/test/org/argeo/util/CsvParserTest.java
deleted file mode 100644 (file)
index e59dbd1..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-package org.argeo.util;
-
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.util.List;
-
-/** {@link CsvParser} tests. */
-public class CsvParserTest {
-       public void testParse() throws Exception {
-               String toParse = "Header1,\"Header\n2\",Header3,\"Header4\"\n" + "Col1,\"Col\n2\",Col3,\"\"\"Col4\"\"\"\n"
-                               + "Col1,\"Col\n2\",Col3,\"\"\"Col4\"\"\"\n" + "Col1,\"Col\n2\",Col3,\"\"\"Col4\"\"\"\n";
-
-               InputStream in = new ByteArrayInputStream(toParse.getBytes());
-
-               CsvParser csvParser = new CsvParser() {
-                       protected void processLine(Integer lineNumber, List<String> header, List<String> tokens) {
-                               assert header.size() == tokens.size();
-                               assert 4 == tokens.size();
-                               assert "Col1".equals(tokens.get(0));
-                               assert "Col\n2".equals(tokens.get(1));
-                               assert "Col3".equals(tokens.get(2));
-                               assert "\"Col4\"".equals(tokens.get(3));
-                       }
-               };
-
-               csvParser.parse(in);
-               in.close();
-       }
-
-}
diff --git a/org.argeo.core/ext/test/org/argeo/util/CsvParserWithQuotedSeparatorTest.java b/org.argeo.core/ext/test/org/argeo/util/CsvParserWithQuotedSeparatorTest.java
deleted file mode 100644 (file)
index 67ba346..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-package org.argeo.util;
-
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/** Test that {@link CsvParser} deals properly with "" quotes. */
-public class CsvParserWithQuotedSeparatorTest {
-       public void testSimpleParse() throws Exception {
-               String toParse = "Header1,\"Header2\",Header3,\"Header4\"\n"
-                               + "\"Col1, Col2\",\"Col\n2\",Col3,\"\"\"Col4\"\"\"\n";
-
-               InputStream in = new ByteArrayInputStream(toParse.getBytes());
-
-               CsvParser csvParser = new CsvParser() {
-                       protected void processLine(Integer lineNumber, List<String> header, List<String> tokens) {
-                               assert header.size() == tokens.size();
-                               assert 4 == tokens.size();
-                               assert "Col1, Col2".equals(tokens.get(0));
-                       }
-               };
-               // System.out.println(toParse);
-               csvParser.parse(in);
-               in.close();
-
-       }
-
-       public void testParseFile() throws Exception {
-
-               final Map<Integer, Map<String, String>> lines = new HashMap<Integer, Map<String, String>>();
-               InputStream in = getClass().getResourceAsStream("/org/argeo/util/ReferenceFile.csv");
-
-               CsvParserWithLinesAsMap parser = new CsvParserWithLinesAsMap() {
-                       protected void processLine(Integer lineNumber, Map<String, String> line) {
-                               // System.out.println("processing line #" + lineNumber);
-                               lines.put(lineNumber, line);
-                       }
-               };
-
-               parser.parse(in);
-               in.close();
-
-               Map<String, String> line = lines.get(2);
-               assert ",,,,".equals(line.get("Coma testing"));
-               line = lines.get(3);
-               assert ",, ,,".equals(line.get("Coma testing"));
-               line = lines.get(4);
-               assert "module1, module2".equals(line.get("Coma testing"));
-               line = lines.get(5);
-               assert "module1,module2".equals(line.get("Coma testing"));
-               line = lines.get(6);
-               assert ",module1,module2, \nmodule3, module4".equals(line.get("Coma testing"));
-               assert 5 == lines.size();
-
-       }
-}
diff --git a/org.argeo.core/ext/test/org/argeo/util/CsvWriterTest.java b/org.argeo.core/ext/test/org/argeo/util/CsvWriterTest.java
deleted file mode 100644 (file)
index ff5dcc5..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-package org.argeo.util;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-/** {@link CsvWriter} tests. */
-public class CsvWriterTest {
-       public void testWrite() throws Exception {
-               ByteArrayOutputStream out = new ByteArrayOutputStream();
-               final CsvWriter csvWriter = new CsvWriter(out);
-
-               String[] header = { "Header1", "Header 2", "Header,3", "Header\n4", "Header\"5\"" };
-               String[] line1 = { "Value1", "Value 2", "Value,3", "Value\n4", "Value\"5\"" };
-               csvWriter.writeLine(Arrays.asList(header));
-               csvWriter.writeLine(Arrays.asList(line1));
-
-               String reference = "Header1,Header 2,\"Header,3\",\"Header\n4\",\"Header\"\"5\"\"\"\n"
-                               + "Value1,Value 2,\"Value,3\",\"Value\n4\",\"Value\"\"5\"\"\"\n";
-               String written = new String(out.toByteArray());
-               assert reference.equals(written);
-               out.close();
-               System.out.println(written);
-
-               final List<String> allTokens = new ArrayList<String>();
-               CsvParser csvParser = new CsvParser() {
-                       protected void processLine(Integer lineNumber, List<String> header, List<String> tokens) {
-                               if (lineNumber == 2)
-                                       allTokens.addAll(header);
-                               allTokens.addAll(tokens);
-                       }
-               };
-               ByteArrayInputStream in = new ByteArrayInputStream(written.getBytes());
-               csvParser.parse(in);
-               in.close();
-               List<String> allTokensRef = new ArrayList<String>();
-               allTokensRef.addAll(Arrays.asList(header));
-               allTokensRef.addAll(Arrays.asList(line1));
-
-               assert allTokensRef.size() == allTokens.size();
-               for (int i = 0; i < allTokensRef.size(); i++)
-                       assert allTokensRef.get(i).equals(allTokens.get(i));
-       }
-
-}
diff --git a/org.argeo.core/ext/test/org/argeo/util/ReferenceFile.csv b/org.argeo.core/ext/test/org/argeo/util/ReferenceFile.csv
deleted file mode 100644 (file)
index 351453d..0000000
+++ /dev/null
@@ -1,37 +0,0 @@
-"ID","A long Text","Name","Other","Number","Reference","Target","Date","Update","Language","ID Ref","Weird chars","line feeds","after line feed","Empty column","Status comment","Comments","Empty","Coma testing"
-"AK251","Everything & with some line feed 
- more “some” quote","Marge S.",,78.6,"A1155222221111268515131",,12/12/12,03/12/08,,9821308500721,"%%%ùù","ao","Nothing special",,,"Some very usefull comment",,",,,,"
-"AG254","same","Roger “wallace” Big","15 – JI",78.5,"A1155222221111268515131","next milestone",12/12/12,03/12/08,"_fr (French - France)",9812309500953,"***µ”","a
-
-
-
-
-o","after line feed",,"Do the job",,,",, ,,"
-"FG211","Very long text with some bullets.
-1 first
-2 second
-3. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long","Father & Son","15 – JI",15.4,"A1155222221111268515131","next milestone",12/12/12,03/12/08,"_fr (French - France)",9812309500952,"///","a
-
-
-
-
-
-
-o","module1,module2",,"Be fast",,,"module1, module2"
-"RRT152","Very long text with some bullets.
-1 first
-2 second
-3. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long","Another $$","15 – JI",12.3,"A1155222221111268515131","next milestone",12/12/12,03/12/08,"_fr (French - France)",9812309500950,"---","a
-
-o
-
-
-","module1,module2",,,,,"module1,module2"
-"YU121","Another use case : “blank line”
-
-After the blank.","nothing with brackets( )","15 – JI",15.2,"A1155222221111268515131",,12/12/12,03/12/08,"_fr (French - France)",9812309500925,",;:?./","ao","
-
-
-
-After line feed again",,,,,",module1,module2, 
-module3, module4"
diff --git a/org.argeo.core/ext/test/org/argeo/util/TestParse-ISO.csv b/org.argeo.core/ext/test/org/argeo/util/TestParse-ISO.csv
deleted file mode 100644 (file)
index 0bec611..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-"Date d'imputation","N° de compte","Code journal","Pièce interne","Pièce externe","Libellé d'écriture","Débit","Crédit","Lettrage","Quantité","Code analytique","Date d'échéance","Date d'imputation origine","Code journal origine","Mode de règlement","Date début de période","Date fin de période"
-26.01.2010,"101300","BQ","BQ01.10",,"Depot société en formation",,"3.000,00",,,,"          ",26.01.2010,"BQ","    ","          ","          "
-26.01.2010,"101300","BQ","BQ01.10",,"Depot société en formation",,"7.000,00",,,,"          ",26.01.2010,"BQ","    ","          ","          "
-26.01.2010,"411OPEN","BQ","BQ01.10",,"Vir Client ",,"2.508,00","A",,,"          ",26.01.2010,"BQ","    ","          ","          "
-26.01.2010,"455100","BQ","BQ01.10",,"Bankomat Raiffeise","250,00",,,,,"          ",26.01.2010,"BQ","    ","          ","          "
-26.01.2010,"512101","BQ","BQ01.10",,"Extrait bancaire 01.10","12.250,55",,,,,"          ",26.01.2010,"BQ","    ","          ","          "
-26.01.2010,"627800","BQ","BQ01.10",,"Envoi de chequier","2,30",,,,,"          ",26.01.2010,"BQ","    ","          ","          "
-26.01.2010,"627800","BQ","BQ01.10",,"Frais d'expedition","5,15",,,,,"          ",26.01.2010,"BQ","    ","          ","          "
diff --git a/org.argeo.core/ext/test/org/argeo/util/TestParse-UTF-8.csv b/org.argeo.core/ext/test/org/argeo/util/TestParse-UTF-8.csv
deleted file mode 100644 (file)
index 0bec611..0000000
+++ /dev/null
@@ -1,8 +0,0 @@
-"Date d'imputation","N° de compte","Code journal","Pièce interne","Pièce externe","Libellé d'écriture","Débit","Crédit","Lettrage","Quantité","Code analytique","Date d'échéance","Date d'imputation origine","Code journal origine","Mode de règlement","Date début de période","Date fin de période"
-26.01.2010,"101300","BQ","BQ01.10",,"Depot société en formation",,"3.000,00",,,,"          ",26.01.2010,"BQ","    ","          ","          "
-26.01.2010,"101300","BQ","BQ01.10",,"Depot société en formation",,"7.000,00",,,,"          ",26.01.2010,"BQ","    ","          ","          "
-26.01.2010,"411OPEN","BQ","BQ01.10",,"Vir Client ",,"2.508,00","A",,,"          ",26.01.2010,"BQ","    ","          ","          "
-26.01.2010,"455100","BQ","BQ01.10",,"Bankomat Raiffeise","250,00",,,,,"          ",26.01.2010,"BQ","    ","          ","          "
-26.01.2010,"512101","BQ","BQ01.10",,"Extrait bancaire 01.10","12.250,55",,,,,"          ",26.01.2010,"BQ","    ","          ","          "
-26.01.2010,"627800","BQ","BQ01.10",,"Envoi de chequier","2,30",,,,,"          ",26.01.2010,"BQ","    ","          ","          "
-26.01.2010,"627800","BQ","BQ01.10",,"Frais d'expedition","5,15",,,,,"          ",26.01.2010,"BQ","    ","          ","          "
diff --git a/org.argeo.core/ext/test/org/argeo/util/ThroughputTest.java b/org.argeo.core/ext/test/org/argeo/util/ThroughputTest.java
deleted file mode 100644 (file)
index d62f55c..0000000
+++ /dev/null
@@ -1,17 +0,0 @@
-package org.argeo.util;
-
-public class ThroughputTest {
-       public void testParse() throws Exception {
-//             assert 0 == 1;
-
-               Throughput t;
-               t = new Throughput("3.54/s");
-               assert 3.54d == t.getValue();
-               assert Throughput.Unit.s.equals(t.getUnit());
-               assert 282l == (long) t.asMsPeriod();
-
-               t = new Throughput("35698.2569/h");
-               assert Throughput.Unit.h.equals(t.getUnit());
-               assert 101l == (long) t.asMsPeriod();
-       }
-}
diff --git a/org.argeo.core/src/org/argeo/util/CsvParser.java b/org.argeo.core/src/org/argeo/util/CsvParser.java
deleted file mode 100644 (file)
index b903f77..0000000
+++ /dev/null
@@ -1,242 +0,0 @@
-package org.argeo.util;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.io.UnsupportedEncodingException;
-import java.nio.charset.Charset;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-/**
- * Parses a CSV file interpreting the first line as a header. The
- * {@link #parse(InputStream)} method and the setters are synchronized so that
- * the object cannot be modified when parsing.
- */
-public abstract class CsvParser {
-       private char separator = ',';
-       private char quote = '\"';
-
-       private Boolean noHeader = false;
-       private Boolean strictLineAsLongAsHeader = true;
-
-       /**
-        * Actually process a parsed line. If
-        * {@link #setStrictLineAsLongAsHeader(Boolean)} is true (default) the header
-        * and the tokens are guaranteed to have the same size.
-        * 
-        * @param lineNumber the current line number, starts at 1 (the header, if header
-        *                   processing is enabled, the first line otherwise)
-        * @param header     the read-only header or null if
-        *                   {@link #setNoHeader(Boolean)} is true (default is false)
-        * @param tokens     the parsed tokens
-        */
-       protected abstract void processLine(Integer lineNumber, List<String> header, List<String> tokens);
-
-       /**
-        * Parses the CSV file (stream is closed at the end)
-        * 
-        * @param in the stream to parse
-        * 
-        * @deprecated Use {@link #parse(InputStream, Charset)} instead.
-        */
-       @Deprecated
-       public synchronized void parse(InputStream in) {
-               parse(in, (Charset) null);
-       }
-
-       /**
-        * Parses the CSV file (stream is closed at the end)
-        * 
-        * @param in       the stream to parse
-        * @param encoding the encoding to use.
-        * 
-        * @deprecated Use {@link #parse(InputStream, Charset)} instead.
-        */
-       @Deprecated
-       public synchronized void parse(InputStream in, String encoding) {
-               Reader reader;
-               if (encoding == null)
-                       reader = new InputStreamReader(in);
-               else
-                       try {
-                               reader = new InputStreamReader(in, encoding);
-                       } catch (UnsupportedEncodingException e) {
-                               throw new IllegalArgumentException(e);
-                       }
-               parse(reader);
-       }
-
-       /**
-        * Parses the CSV file (stream is closed at the end)
-        * 
-        * @param in      the stream to parse
-        * @param charset the charset to use
-        */
-       public synchronized void parse(InputStream in, Charset charset) {
-               Reader reader;
-               if (charset == null)
-                       reader = new InputStreamReader(in);
-               else
-                       reader = new InputStreamReader(in, charset);
-               parse(reader);
-       }
-
-       /**
-        * Parses the CSV file (stream is closed at the end)
-        * 
-        * @param reader the reader to use (it will be buffered)
-        */
-       public synchronized void parse(Reader reader) {
-               Integer lineCount = 0;
-               try (BufferedReader bufferedReader = new BufferedReader(reader)) {
-                       List<String> header = null;
-                       if (!noHeader) {
-                               String headerStr = bufferedReader.readLine();
-                               if (headerStr == null)// empty file
-                                       return;
-                               lineCount++;
-                               header = new ArrayList<String>();
-                               StringBuffer currStr = new StringBuffer("");
-                               Boolean wasInquote = false;
-                               while (parseLine(headerStr, header, currStr, wasInquote)) {
-                                       headerStr = bufferedReader.readLine();
-                                       if (headerStr == null)
-                                               break;
-                                       wasInquote = true;
-                               }
-                               header = Collections.unmodifiableList(header);
-                       }
-
-                       String line = null;
-                       lines: while ((line = bufferedReader.readLine()) != null) {
-                               line = preProcessLine(line);
-                               if (line == null) {
-                                       // skip line
-                                       continue lines;
-                               }
-                               lineCount++;
-                               List<String> tokens = new ArrayList<String>();
-                               StringBuffer currStr = new StringBuffer("");
-                               Boolean wasInquote = false;
-                               sublines: while (parseLine(line, tokens, currStr, wasInquote)) {
-                                       line = bufferedReader.readLine();
-                                       if (line == null)
-                                               break sublines;
-                                       wasInquote = true;
-                               }
-                               if (!noHeader && strictLineAsLongAsHeader) {
-                                       int headerSize = header.size();
-                                       int tokenSize = tokens.size();
-                                       if (tokenSize == 1 && line.trim().equals(""))
-                                               continue lines;// empty line
-                                       if (headerSize != tokenSize) {
-                                               throw new IllegalStateException("Token size " + tokenSize + " is different from header size "
-                                                               + headerSize + " at line " + lineCount + ", line: " + line + ", header: " + header
-                                                               + ", tokens: " + tokens);
-                                       }
-                               }
-                               processLine(lineCount, header, tokens);
-                       }
-               } catch (IOException e) {
-                       throw new RuntimeException("Cannot parse CSV file (line: " + lineCount + ")", e);
-               }
-       }
-
-       /**
-        * Called before each (logical) line is processed, giving a change to modify it
-        * (typically for cleaning dirty files). To be overridden, return the line
-        * unchanged by default. Skip the line if 'null' is returned.
-        */
-       protected String preProcessLine(String line) {
-               return line;
-       }
-
-       /**
-        * Parses a line character by character for performance purpose
-        * 
-        * @return whether to continue parsing this line
-        */
-       protected Boolean parseLine(String str, List<String> tokens, StringBuffer currStr, Boolean wasInquote) {
-               if (wasInquote)
-                       currStr.append('\n');
-
-               char[] arr = str.toCharArray();
-               boolean inQuote = wasInquote;
-               for (int i = 0; i < arr.length; i++) {
-                       char c = arr[i];
-                       if (c == separator) {
-                               if (!inQuote) {
-                                       tokens.add(currStr.toString());
-//                                     currStr.delete(0, currStr.length());
-                                       currStr.setLength(0);
-                                       currStr.trimToSize();
-                               } else {
-                                       // we don't remove separator that are in a quoted substring
-                                       // System.out
-                                       // .println("IN QUOTE, got a separator: [" + c + "]");
-                                       currStr.append(c);
-                               }
-                       } else if (c == quote) {
-                               if (inQuote && (i + 1) < arr.length && arr[i + 1] == quote) {
-                                       // case of double quote
-                                       currStr.append(quote);
-                                       i++;
-                               } else {// standard
-                                       inQuote = inQuote ? false : true;
-                               }
-                       } else {
-                               currStr.append(c);
-                       }
-               }
-
-               if (!inQuote) {
-                       tokens.add(currStr.toString());
-                       // System.out.println("# TOKEN: " + currStr);
-               }
-               // if (inQuote)
-               // throw new ArgeoException("Missing quote at the end of the line "
-               // + str + " (parsed: " + tokens + ")");
-               if (inQuote)
-                       return true;
-               else
-                       return false;
-               // return tokens;
-       }
-
-       public char getSeparator() {
-               return separator;
-       }
-
-       public synchronized void setSeparator(char separator) {
-               this.separator = separator;
-       }
-
-       public char getQuote() {
-               return quote;
-       }
-
-       public synchronized void setQuote(char quote) {
-               this.quote = quote;
-       }
-
-       public Boolean getNoHeader() {
-               return noHeader;
-       }
-
-       public synchronized void setNoHeader(Boolean noHeader) {
-               this.noHeader = noHeader;
-       }
-
-       public Boolean getStrictLineAsLongAsHeader() {
-               return strictLineAsLongAsHeader;
-       }
-
-       public synchronized void setStrictLineAsLongAsHeader(Boolean strictLineAsLongAsHeader) {
-               this.strictLineAsLongAsHeader = strictLineAsLongAsHeader;
-       }
-
-}
diff --git a/org.argeo.core/src/org/argeo/util/CsvParserWithLinesAsMap.java b/org.argeo.core/src/org/argeo/util/CsvParserWithLinesAsMap.java
deleted file mode 100644 (file)
index 8eb6e94..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-package org.argeo.util;
-
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/**
- * CSV parser allowing to process lines as maps whose keys are the header
- * fields.
- */
-public abstract class CsvParserWithLinesAsMap extends CsvParser {
-
-       /**
-        * Actually processes a line.
-        * 
-        * @param lineNumber the current line number, starts at 1 (the header, if header
-        *                   processing is enabled, the first lien otherwise)
-        * @param line       the parsed tokens as a map whose keys are the header fields
-        */
-       protected abstract void processLine(Integer lineNumber, Map<String, String> line);
-
-       protected final void processLine(Integer lineNumber, List<String> header, List<String> tokens) {
-               if (header == null)
-                       throw new IllegalArgumentException("Only CSV with header is supported");
-               Map<String, String> line = new HashMap<String, String>();
-               for (int i = 0; i < header.size(); i++) {
-                       String key = header.get(i);
-                       String value = null;
-                       if (i < tokens.size())
-                               value = tokens.get(i);
-                       line.put(key, value);
-               }
-               processLine(lineNumber, line);
-       }
-
-}
diff --git a/org.argeo.core/src/org/argeo/util/CsvWriter.java b/org.argeo.core/src/org/argeo/util/CsvWriter.java
deleted file mode 100644 (file)
index c3b3a3a..0000000
+++ /dev/null
@@ -1,156 +0,0 @@
-package org.argeo.util;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.UnsupportedEncodingException;
-import java.io.Writer;
-import java.nio.charset.Charset;
-import java.util.Iterator;
-import java.util.List;
-
-/** Write in CSV format. */
-public class CsvWriter {
-       private final Writer out;
-
-       private char separator = ',';
-       private char quote = '\"';
-
-       /**
-        * Creates a CSV writer.
-        * 
-        * @param out the stream to write to. Caller is responsible for closing it.
-        * 
-        * @deprecated Use {@link #CsvWriter(OutputStream, Charset)} instead.
-        * 
-        */
-       @Deprecated
-       public CsvWriter(OutputStream out) {
-               this.out = new OutputStreamWriter(out);
-       }
-
-       /**
-        * Creates a CSV writer.
-        * 
-        * @param out      the stream to write to. Caller is responsible for closing it.
-        * @param encoding the encoding to use.
-        * 
-        * @deprecated Use {@link #CsvWriter(OutputStream, Charset)} instead.
-        */
-       @Deprecated
-       public CsvWriter(OutputStream out, String encoding) {
-               try {
-                       this.out = new OutputStreamWriter(out, encoding);
-               } catch (UnsupportedEncodingException e) {
-                       throw new IllegalArgumentException(e);
-               }
-       }
-
-       /**
-        * Creates a CSV writer.
-        * 
-        * @param out     the stream to write to. Caller is responsible for closing it.
-        * @param charset the charset to use
-        */
-       public CsvWriter(OutputStream out, Charset charset) {
-               this.out = new OutputStreamWriter(out, charset);
-       }
-
-       /**
-        * Creates a CSV writer.
-        * 
-        * @param out the stream to write to. Caller is responsible for closing it.
-        */
-       public CsvWriter(Writer writer) {
-               this.out = writer;
-       }
-
-       /**
-        * Write a CSV line. Also used to write a header if needed (this is transparent
-        * for the CSV writer): simply call it first, before writing the lines.
-        */
-       public void writeLine(List<?> tokens) {
-               try {
-                       Iterator<?> it = tokens.iterator();
-                       while (it.hasNext()) {
-                               Object obj = it.next();
-                               writeToken(obj != null ? obj.toString() : null);
-                               if (it.hasNext())
-                                       out.write(separator);
-                       }
-                       out.write('\n');
-                       out.flush();
-               } catch (IOException e) {
-                       throw new RuntimeException("Could not write " + tokens, e);
-               }
-       }
-
-       /**
-        * Write a CSV line. Also used to write a header if needed (this is transparent
-        * for the CSV writer): simply call it first, before writing the lines.
-        */
-       public void writeLine(Object[] tokens) {
-               try {
-                       for (int i = 0; i < tokens.length; i++) {
-                               if (tokens[i] == null) {
-                                       writeToken(null);
-                               } else {
-                                       writeToken(tokens[i].toString());
-                               }
-                               if (i != (tokens.length - 1))
-                                       out.write(separator);
-                       }
-                       out.write('\n');
-                       out.flush();
-               } catch (IOException e) {
-                       throw new RuntimeException("Could not write " + tokens, e);
-               }
-       }
-
-       protected void writeToken(String token) throws IOException {
-               if (token == null) {
-                       // TODO configure how to deal with null
-                       out.write("");
-                       return;
-               }
-               // +2 for possible quotes, another +2 assuming there would be an already
-               // quoted string where quotes needs to be duplicated
-               // another +2 for safety
-               // we don't want to increase buffer size while writing
-               StringBuffer buf = new StringBuffer(token.length() + 6);
-               char[] arr = token.toCharArray();
-               boolean shouldQuote = false;
-               for (char c : arr) {
-                       if (!shouldQuote) {
-                               if (c == separator)
-                                       shouldQuote = true;
-                               if (c == '\n')
-                                       shouldQuote = true;
-                       }
-
-                       if (c == quote) {
-                               shouldQuote = true;
-                               // duplicate quote
-                               buf.append(quote);
-                       }
-
-                       // generic case
-                       buf.append(c);
-               }
-
-               if (shouldQuote == true)
-                       out.write(quote);
-               out.write(buf.toString());
-               if (shouldQuote == true)
-                       out.write(quote);
-       }
-
-       public void setSeparator(char separator) {
-               this.separator = separator;
-       }
-
-       public void setQuote(char quote) {
-               this.quote = quote;
-       }
-
-}
diff --git a/org.argeo.core/src/org/argeo/util/DictionaryKeys.java b/org.argeo.core/src/org/argeo/util/DictionaryKeys.java
deleted file mode 100644 (file)
index d17c86f..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-package org.argeo.util;
-
-import java.util.Dictionary;
-import java.util.Enumeration;
-import java.util.Iterator;
-
-/**
- * Access the keys of a {@link String}-keyed {@link Dictionary} (common throughout
- * the OSGi APIs) as an {@link Iterable} so that they are easily usable in
- * for-each loops.
- */
-class DictionaryKeys implements Iterable<String> {
-       private final Dictionary<String, ?> dictionary;
-
-       public DictionaryKeys(Dictionary<String, ?> dictionary) {
-               this.dictionary = dictionary;
-       }
-
-       @Override
-       public Iterator<String> iterator() {
-               return new KeyIterator(dictionary.keys());
-       }
-
-       private static class KeyIterator implements Iterator<String> {
-               private final Enumeration<String> keys;
-
-               KeyIterator(Enumeration<String> keys) {
-                       this.keys = keys;
-               }
-
-               @Override
-               public boolean hasNext() {
-                       return keys.hasMoreElements();
-               }
-
-               @Override
-               public String next() {
-                       return keys.nextElement();
-               }
-
-       }
-}
diff --git a/org.argeo.core/src/org/argeo/util/DigestUtils.java b/org.argeo.core/src/org/argeo/util/DigestUtils.java
deleted file mode 100644 (file)
index ce01800..0000000
+++ /dev/null
@@ -1,201 +0,0 @@
-package org.argeo.util;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.ByteBuffer;
-import java.nio.channels.FileChannel;
-import java.nio.channels.FileChannel.MapMode;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-
-/** Utilities around cryptographic digests */
-public class DigestUtils {
-       public final static String MD5 = "MD5";
-       public final static String SHA1 = "SHA1";
-       public final static String SHA256 = "SHA-256";
-       public final static String SHA512 = "SHA-512";
-
-       private static Boolean debug = false;
-       // TODO: make it configurable
-       private final static Integer byteBufferCapacity = 100 * 1024;// 100 KB
-
-       public static byte[] sha1(byte[] bytes) {
-               try {
-                       MessageDigest digest = MessageDigest.getInstance(SHA1);
-                       digest.update(bytes);
-                       byte[] checksum = digest.digest();
-                       return checksum;
-               } catch (NoSuchAlgorithmException e) {
-                       throw new IllegalArgumentException(e);
-               }
-       }
-
-       public static String digest(String algorithm, byte[] bytes) {
-               try {
-                       MessageDigest digest = MessageDigest.getInstance(algorithm);
-                       digest.update(bytes);
-                       byte[] checksum = digest.digest();
-                       String res = encodeHexString(checksum);
-                       return res;
-               } catch (NoSuchAlgorithmException e) {
-                       throw new IllegalArgumentException("Cannot digest with algorithm " + algorithm, e);
-               }
-       }
-
-       public static String digest(String algorithm, InputStream in) {
-               try {
-                       MessageDigest digest = MessageDigest.getInstance(algorithm);
-                       // ReadableByteChannel channel = Channels.newChannel(in);
-                       // ByteBuffer bb = ByteBuffer.allocateDirect(byteBufferCapacity);
-                       // while (channel.read(bb) > 0)
-                       // digest.update(bb);
-                       byte[] buffer = new byte[byteBufferCapacity];
-                       int read = 0;
-                       while ((read = in.read(buffer)) > 0) {
-                               digest.update(buffer, 0, read);
-                       }
-
-                       byte[] checksum = digest.digest();
-                       String res = encodeHexString(checksum);
-                       return res;
-               } catch (NoSuchAlgorithmException e) {
-                       throw new IllegalArgumentException("Cannot digest with algorithm " + algorithm, e);
-               } catch (IOException e) {
-                       throw new RuntimeException(e);
-               } finally {
-                       StreamUtils.closeQuietly(in);
-               }
-       }
-
-       public static String digest(String algorithm, File file) {
-               FileInputStream fis = null;
-               FileChannel fc = null;
-               try {
-                       fis = new FileInputStream(file);
-                       fc = fis.getChannel();
-
-                       // Get the file's size and then map it into memory
-                       int sz = (int) fc.size();
-                       ByteBuffer bb = fc.map(FileChannel.MapMode.READ_ONLY, 0, sz);
-                       return digest(algorithm, bb);
-               } catch (IOException e) {
-                       throw new IllegalArgumentException("Cannot digest " + file + " with algorithm " + algorithm, e);
-               } finally {
-                       StreamUtils.closeQuietly(fis);
-                       if (fc.isOpen())
-                               try {
-                                       fc.close();
-                               } catch (IOException e) {
-                                       // silent
-                               }
-               }
-       }
-
-       protected static String digest(String algorithm, ByteBuffer bb) {
-               long begin = System.currentTimeMillis();
-               try {
-                       MessageDigest digest = MessageDigest.getInstance(algorithm);
-                       digest.update(bb);
-                       byte[] checksum = digest.digest();
-                       String res = encodeHexString(checksum);
-                       long end = System.currentTimeMillis();
-                       if (debug)
-                               System.out.println((end - begin) + " ms / " + ((end - begin) / 1000) + " s");
-                       return res;
-               } catch (NoSuchAlgorithmException e) {
-                       throw new IllegalArgumentException("Cannot digest with algorithm " + algorithm, e);
-               }
-       }
-
-       public static String sha1hex(Path path) {
-               return digest(SHA1, path, byteBufferCapacity);
-       }
-
-       public static String digest(String algorithm, Path path, long bufferSize) {
-               byte[] digest = digestRaw(algorithm, path, bufferSize);
-               return encodeHexString(digest);
-       }
-
-       public static byte[] digestRaw(String algorithm, Path file, long bufferSize) {
-               long begin = System.currentTimeMillis();
-               try {
-                       MessageDigest md = MessageDigest.getInstance(algorithm);
-                       FileChannel fc = FileChannel.open(file);
-                       long fileSize = Files.size(file);
-                       if (fileSize <= bufferSize) {
-                               ByteBuffer bb = fc.map(MapMode.READ_ONLY, 0, fileSize);
-                               md.update(bb);
-                       } else {
-                               long lastCycle = (fileSize / bufferSize) - 1;
-                               long position = 0;
-                               for (int i = 0; i <= lastCycle; i++) {
-                                       ByteBuffer bb;
-                                       if (i != lastCycle) {
-                                               bb = fc.map(MapMode.READ_ONLY, position, bufferSize);
-                                               position = position + bufferSize;
-                                       } else {
-                                               bb = fc.map(MapMode.READ_ONLY, position, fileSize - position);
-                                               position = fileSize;
-                                       }
-                                       md.update(bb);
-                               }
-                       }
-                       long end = System.currentTimeMillis();
-                       if (debug)
-                               System.out.println((end - begin) + " ms / " + ((end - begin) / 1000) + " s");
-                       return md.digest();
-               } catch (NoSuchAlgorithmException e) {
-                       throw new IllegalArgumentException("Cannot digest " + file + "  with algorithm " + algorithm, e);
-               } catch (IOException e) {
-                       throw new RuntimeException("Cannot digest " + file + "  with algorithm " + algorithm, e);
-               }
-       }
-
-       public static void main(String[] args) {
-               File file;
-               if (args.length > 0)
-                       file = new File(args[0]);
-               else {
-                       System.err.println("Usage: <file> [<algorithm>]" + " (see http://java.sun.com/j2se/1.5.0/"
-                                       + "docs/guide/security/CryptoSpec.html#AppA)");
-                       return;
-               }
-
-               if (args.length > 1) {
-                       String algorithm = args[1];
-                       System.out.println(digest(algorithm, file));
-               } else {
-                       String algorithm = "MD5";
-                       System.out.println(algorithm + ": " + digest(algorithm, file));
-                       algorithm = "SHA";
-                       System.out.println(algorithm + ": " + digest(algorithm, file));
-                       System.out.println(algorithm + ": " + sha1hex(file.toPath()));
-                       algorithm = "SHA-256";
-                       System.out.println(algorithm + ": " + digest(algorithm, file));
-                       algorithm = "SHA-512";
-                       System.out.println(algorithm + ": " + digest(algorithm, file));
-               }
-       }
-
-       final private static char[] hexArray = "0123456789abcdef".toCharArray();
-
-       /**
-        * From
-        * http://stackoverflow.com/questions/9655181/how-to-convert-a-byte-array-to
-        * -a-hex-string-in-java
-        */
-       public static String encodeHexString(byte[] bytes) {
-               char[] hexChars = new char[bytes.length * 2];
-               for (int j = 0; j < bytes.length; j++) {
-                       int v = bytes[j] & 0xFF;
-                       hexChars[j * 2] = hexArray[v >>> 4];
-                       hexChars[j * 2 + 1] = hexArray[v & 0x0F];
-               }
-               return new String(hexChars);
-       }
-
-}
diff --git a/org.argeo.core/src/org/argeo/util/DirH.java b/org.argeo.core/src/org/argeo/util/DirH.java
deleted file mode 100644 (file)
index b6d962f..0000000
+++ /dev/null
@@ -1,116 +0,0 @@
-package org.argeo.util;
-
-import java.io.IOException;
-import java.io.PrintStream;
-import java.nio.charset.Charset;
-import java.nio.file.DirectoryStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-/** Hashes the hashes of the files in a directory. */
-public class DirH {
-
-       private final static Charset charset = Charset.forName("UTF-16");
-       private final static long bufferSize = 200 * 1024 * 1024;
-       private final static String algorithm = "SHA";
-
-       private final static byte EOL = (byte) '\n';
-       private final static byte SPACE = (byte) ' ';
-
-       private final int hashSize;
-
-       private final byte[][] hashes;
-       private final byte[][] fileNames;
-       private final byte[] digest;
-       private final byte[] dirName;
-
-       /**
-        * @param dirName can be null or empty
-        */
-       private DirH(byte[][] hashes, byte[][] fileNames, byte[] dirName) {
-               if (hashes.length != fileNames.length)
-                       throw new IllegalArgumentException(hashes.length + " hashes and " + fileNames.length + " file names");
-               this.hashes = hashes;
-               this.fileNames = fileNames;
-               this.dirName = dirName == null ? new byte[0] : dirName;
-               if (hashes.length == 0) {// empty dir
-                       hashSize = 20;
-                       // FIXME what is the digest of an empty dir?
-                       digest = new byte[hashSize];
-                       Arrays.fill(digest, SPACE);
-                       return;
-               }
-               hashSize = hashes[0].length;
-               for (int i = 0; i < hashes.length; i++) {
-                       if (hashes[i].length != hashSize)
-                               throw new IllegalArgumentException(
-                                               "Hash size for " + new String(fileNames[i], charset) + " is " + hashes[i].length);
-               }
-
-               try {
-                       MessageDigest md = MessageDigest.getInstance(algorithm);
-                       for (int i = 0; i < hashes.length; i++) {
-                               md.update(this.hashes[i]);
-                               md.update(SPACE);
-                               md.update(this.fileNames[i]);
-                               md.update(EOL);
-                       }
-                       digest = md.digest();
-               } catch (NoSuchAlgorithmException e) {
-                       throw new IllegalArgumentException("Cannot digest", e);
-               }
-       }
-
-       public void print(PrintStream out) {
-               out.print(DigestUtils.encodeHexString(digest));
-               if (dirName.length > 0) {
-                       out.print(' ');
-                       out.print(new String(dirName, charset));
-               }
-               out.print('\n');
-               for (int i = 0; i < hashes.length; i++) {
-                       out.print(DigestUtils.encodeHexString(hashes[i]));
-                       out.print(' ');
-                       out.print(new String(fileNames[i], charset));
-                       out.print('\n');
-               }
-       }
-
-       public static DirH digest(Path dir) {
-               try (DirectoryStream<Path> files = Files.newDirectoryStream(dir)) {
-                       List<byte[]> hs = new ArrayList<byte[]>();
-                       List<String> fNames = new ArrayList<>();
-                       for (Path file : files) {
-                               if (!Files.isDirectory(file)) {
-                                       byte[] digest = DigestUtils.digestRaw(algorithm, file, bufferSize);
-                                       hs.add(digest);
-                                       fNames.add(file.getFileName().toString());
-                               }
-                       }
-
-                       byte[][] fileNames = new byte[fNames.size()][];
-                       for (int i = 0; i < fNames.size(); i++) {
-                               fileNames[i] = fNames.get(i).getBytes(charset);
-                       }
-                       byte[][] hashes = hs.toArray(new byte[hs.size()][]);
-                       return new DirH(hashes, fileNames, dir.toString().getBytes(charset));
-               } catch (IOException e) {
-                       throw new RuntimeException("Cannot digest " + dir, e);
-               }
-       }
-
-       public static void main(String[] args) {
-               try {
-                       DirH dirH = DirH.digest(Paths.get("/home/mbaudier/tmp/"));
-                       dirH.print(System.out);
-               } catch (Exception e) {
-                       e.printStackTrace();
-               }
-       }
-}
diff --git a/org.argeo.core/src/org/argeo/util/LangUtils.java b/org.argeo.core/src/org/argeo/util/LangUtils.java
deleted file mode 100644 (file)
index 7824d12..0000000
+++ /dev/null
@@ -1,253 +0,0 @@
-package org.argeo.util;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.Writer;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.StandardOpenOption;
-import java.time.ZonedDateTime;
-import java.time.temporal.ChronoUnit;
-import java.time.temporal.Temporal;
-import java.util.Dictionary;
-import java.util.Enumeration;
-import java.util.HashMap;
-import java.util.Hashtable;
-import java.util.Map;
-import java.util.Properties;
-
-import javax.naming.InvalidNameException;
-import javax.naming.ldap.LdapName;
-
-/** Utilities around Java basic features. */
-public class LangUtils {
-       /*
-        * NON-API OSGi
-        */
-       /**
-        * Returns an array with the names of the provided classes. Useful when
-        * registering services with multiple interfaces in OSGi.
-        */
-       public static String[] names(Class<?>... clzz) {
-               String[] res = new String[clzz.length];
-               for (int i = 0; i < clzz.length; i++)
-                       res[i] = clzz[i].getName();
-               return res;
-       }
-
-       /*
-        * MAP
-        */
-       /**
-        * Creates a new {@link Dictionary} with one key-value pair. Key should not be
-        * null, but if the value is null, it returns an empty {@link Dictionary}.
-        */
-       public static Map<String, Object> map(String key, Object value) {
-               assert key != null;
-               HashMap<String, Object> props = new HashMap<>();
-               if (value != null)
-                       props.put(key, value);
-               return props;
-       }
-
-       /*
-        * DICTIONARY
-        */
-
-       /**
-        * Creates a new {@link Dictionary} with one key-value pair. Key should not be
-        * null, but if the value is null, it returns an empty {@link Dictionary}.
-        */
-       public static Dictionary<String, Object> dict(String key, Object value) {
-               assert key != null;
-               Hashtable<String, Object> props = new Hashtable<>();
-               if (value != null)
-                       props.put(key, value);
-               return props;
-       }
-
-       /** @deprecated Use {@link #dict(String, Object)} instead. */
-       @Deprecated
-       public static Dictionary<String, Object> dico(String key, Object value) {
-               return dict(key, value);
-       }
-
-       /** Converts a {@link Dictionary} to a {@link Map} of strings. */
-       public static Map<String, String> dictToStringMap(Dictionary<String, ?> properties) {
-               if (properties == null) {
-                       return null;
-               }
-               Map<String, String> res = new HashMap<>(properties.size());
-               Enumeration<String> keys = properties.keys();
-               while (keys.hasMoreElements()) {
-                       String key = keys.nextElement();
-                       res.put(key, properties.get(key).toString());
-               }
-               return res;
-       }
-
-       /**
-        * Get a string property from this map, expecting to find it, or
-        * <code>null</code> if not found.
-        */
-       public static String get(Map<String, ?> map, String key) {
-               Object res = map.get(key);
-               if (res == null)
-                       return null;
-               return res.toString();
-       }
-
-       /**
-        * Get a string property from this map, expecting to find it.
-        * 
-        * @throws IllegalArgumentException if the key was not found
-        */
-       public static String getNotNull(Map<String, ?> map, String key) {
-               Object res = map.get(key);
-               if (res == null)
-                       throw new IllegalArgumentException("Map " + map + " should contain key " + key);
-               return res.toString();
-       }
-
-       /**
-        * Wraps the keys of the provided {@link Dictionary} as an {@link Iterable}.
-        */
-       public static Iterable<String> keys(Dictionary<String, ?> props) {
-               assert props != null;
-               return new DictionaryKeys(props);
-       }
-
-       static String toJson(Dictionary<String, ?> props) {
-               return toJson(props, false);
-       }
-
-       static String toJson(Dictionary<String, ?> props, boolean pretty) {
-               StringBuilder sb = new StringBuilder();
-               sb.append('{');
-               if (pretty)
-                       sb.append('\n');
-               Enumeration<String> keys = props.keys();
-               while (keys.hasMoreElements()) {
-                       String key = keys.nextElement();
-                       if (pretty)
-                               sb.append(' ');
-                       sb.append('\"').append(key).append('\"');
-                       if (pretty)
-                               sb.append(" : ");
-                       else
-                               sb.append(':');
-                       sb.append('\"').append(props.get(key)).append('\"');
-                       if (keys.hasMoreElements())
-                               sb.append(", ");
-                       if (pretty)
-                               sb.append('\n');
-               }
-               sb.append('}');
-               return sb.toString();
-       }
-
-       static void storeAsProperties(Dictionary<String, Object> props, Path path) throws IOException {
-               if (props == null)
-                       throw new IllegalArgumentException("Props cannot be null");
-               Properties toStore = new Properties();
-               for (Enumeration<String> keys = props.keys(); keys.hasMoreElements();) {
-                       String key = keys.nextElement();
-                       toStore.setProperty(key, props.get(key).toString());
-               }
-               try (OutputStream out = Files.newOutputStream(path)) {
-                       toStore.store(out, null);
-               }
-       }
-
-       static void appendAsLdif(String dnBase, String dnKey, Dictionary<String, Object> props, Path path)
-                       throws IOException {
-               if (props == null)
-                       throw new IllegalArgumentException("Props cannot be null");
-               Object dnValue = props.get(dnKey);
-               String dnStr = dnKey + '=' + dnValue + ',' + dnBase;
-               LdapName dn;
-               try {
-                       dn = new LdapName(dnStr);
-               } catch (InvalidNameException e) {
-                       throw new IllegalArgumentException("Cannot interpret DN " + dnStr, e);
-               }
-               if (dnValue == null)
-                       throw new IllegalArgumentException("DN key " + dnKey + " must have a value");
-               try (Writer writer = Files.newBufferedWriter(path, StandardOpenOption.APPEND, StandardOpenOption.CREATE)) {
-                       writer.append("\ndn: ");
-                       writer.append(dn.toString());
-                       writer.append('\n');
-                       for (Enumeration<String> keys = props.keys(); keys.hasMoreElements();) {
-                               String key = keys.nextElement();
-                               Object value = props.get(key);
-                               writer.append(key);
-                               writer.append(": ");
-                               // FIXME deal with binary and multiple values
-                               writer.append(value.toString());
-                               writer.append('\n');
-                       }
-               }
-       }
-
-       static Dictionary<String, Object> loadFromProperties(Path path) throws IOException {
-               Properties toLoad = new Properties();
-               try (InputStream in = Files.newInputStream(path)) {
-                       toLoad.load(in);
-               }
-               Dictionary<String, Object> res = new Hashtable<String, Object>();
-               for (Object key : toLoad.keySet())
-                       res.put(key.toString(), toLoad.get(key));
-               return res;
-       }
-
-       /*
-        * EXCEPTIONS
-        */
-       /**
-        * Chain the messages of all causes (one per line, <b>starts with a line
-        * return</b>) without all the stack
-        */
-       public static String chainCausesMessages(Throwable t) {
-               StringBuffer buf = new StringBuffer();
-               chainCauseMessage(buf, t);
-               return buf.toString();
-       }
-
-       /** Recursive chaining of messages */
-       private static void chainCauseMessage(StringBuffer buf, Throwable t) {
-               buf.append('\n').append(' ').append(t.getClass().getCanonicalName()).append(": ").append(t.getMessage());
-               if (t.getCause() != null)
-                       chainCauseMessage(buf, t.getCause());
-       }
-
-       /*
-        * TIME
-        */
-       /** Formats time elapsed since start. */
-       public static String since(ZonedDateTime start) {
-               ZonedDateTime now = ZonedDateTime.now();
-               return duration(start, now);
-       }
-
-       /** Formats a duration. */
-       public static String duration(Temporal start, Temporal end) {
-               long count = ChronoUnit.DAYS.between(start, end);
-               if (count != 0)
-                       return count > 1 ? count + " days" : count + " day";
-               count = ChronoUnit.HOURS.between(start, end);
-               if (count != 0)
-                       return count > 1 ? count + " hours" : count + " hours";
-               count = ChronoUnit.MINUTES.between(start, end);
-               if (count != 0)
-                       return count > 1 ? count + " minutes" : count + " minute";
-               count = ChronoUnit.SECONDS.between(start, end);
-               return count > 1 ? count + " seconds" : count + " second";
-       }
-
-       /** Singleton constructor. */
-       private LangUtils() {
-
-       }
-
-}
diff --git a/org.argeo.core/src/org/argeo/util/OS.java b/org.argeo.core/src/org/argeo/util/OS.java
deleted file mode 100644 (file)
index d8127b6..0000000
+++ /dev/null
@@ -1,56 +0,0 @@
-package org.argeo.util;
-
-import java.io.File;
-import java.lang.management.ManagementFactory;
-
-/** When OS specific informations are needed. */
-public class OS {
-       public final static OS LOCAL = new OS();
-
-       private final String arch, name, version;
-
-       /** The OS of the running JVM */
-       protected OS() {
-               arch = System.getProperty("os.arch");
-               name = System.getProperty("os.name");
-               version = System.getProperty("os.version");
-       }
-
-       public String getArch() {
-               return arch;
-       }
-
-       public String getName() {
-               return name;
-       }
-
-       public String getVersion() {
-               return version;
-       }
-
-       public boolean isMSWindows() {
-               // only MS Windows would use such an horrendous separator...
-               return File.separatorChar == '\\';
-       }
-
-       public String[] getDefaultShellCommand() {
-               if (!isMSWindows())
-                       return new String[] { "/bin/sh", "-l", "-i" };
-               else
-                       return new String[] { "cmd.exe", "/C" };
-       }
-
-       public static Integer getJvmPid() {
-               /*
-                * This method works on most platforms (including Linux). Although when Java 9
-                * comes along, there is a better way: long pid =
-                * ProcessHandle.current().getPid();
-                *
-                * See:
-                * http://stackoverflow.com/questions/35842/how-can-a-java-program-get-its-own-
-                * process-id
-                */
-               String pidAndHost = ManagementFactory.getRuntimeMXBean().getName();
-               return Integer.parseInt(pidAndHost.substring(0, pidAndHost.indexOf('@')));
-       }
-}
diff --git a/org.argeo.core/src/org/argeo/util/PasswordEncryption.java b/org.argeo.core/src/org/argeo/util/PasswordEncryption.java
deleted file mode 100644 (file)
index c95c787..0000000
+++ /dev/null
@@ -1,216 +0,0 @@
-package org.argeo.util;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.nio.charset.Charset;
-import java.nio.charset.StandardCharsets;
-import java.security.GeneralSecurityException;
-import java.security.InvalidKeyException;
-import java.security.Key;
-
-import javax.crypto.Cipher;
-import javax.crypto.CipherInputStream;
-import javax.crypto.CipherOutputStream;
-import javax.crypto.SecretKey;
-import javax.crypto.SecretKeyFactory;
-import javax.crypto.spec.IvParameterSpec;
-import javax.crypto.spec.PBEKeySpec;
-import javax.crypto.spec.SecretKeySpec;
-
-public class PasswordEncryption {
-       public final static Integer DEFAULT_ITERATION_COUNT = 1024;
-       /** Stronger with 256, but causes problem with Oracle JVM */
-       public final static Integer DEFAULT_SECRETE_KEY_LENGTH = 256;
-       public final static Integer DEFAULT_SECRETE_KEY_LENGTH_RESTRICTED = 128;
-       public final static String DEFAULT_SECRETE_KEY_FACTORY = "PBKDF2WithHmacSHA1";
-       public final static String DEFAULT_SECRETE_KEY_ENCRYPTION = "AES";
-       public final static String DEFAULT_CIPHER_NAME = "AES/CBC/PKCS5Padding";
-//     public final static String DEFAULT_CHARSET = "UTF-8";
-       public final static Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
-
-       private Integer iterationCount = DEFAULT_ITERATION_COUNT;
-       private Integer secreteKeyLength = DEFAULT_SECRETE_KEY_LENGTH;
-       private String secreteKeyFactoryName = DEFAULT_SECRETE_KEY_FACTORY;
-       private String secreteKeyEncryption = DEFAULT_SECRETE_KEY_ENCRYPTION;
-       private String cipherName = DEFAULT_CIPHER_NAME;
-
-       private static byte[] DEFAULT_SALT_8 = { (byte) 0xA9, (byte) 0x9B, (byte) 0xC8, (byte) 0x32, (byte) 0x56,
-                       (byte) 0x35, (byte) 0xE3, (byte) 0x03 };
-       private static byte[] DEFAULT_IV_16 = { (byte) 0xA9, (byte) 0x9B, (byte) 0xC8, (byte) 0x32, (byte) 0x56,
-                       (byte) 0x35, (byte) 0xE3, (byte) 0x03, (byte) 0xA9, (byte) 0x9B, (byte) 0xC8, (byte) 0x32, (byte) 0x56,
-                       (byte) 0x35, (byte) 0xE3, (byte) 0x03 };
-
-       private Key key;
-       private Cipher ecipher;
-       private Cipher dcipher;
-
-       private String securityProviderName = null;
-
-       /**
-        * This is up to the caller to clear the passed array. Neither copy of nor
-        * reference to the passed array is kept
-        */
-       public PasswordEncryption(char[] password) {
-               this(password, DEFAULT_SALT_8, DEFAULT_IV_16);
-       }
-
-       /**
-        * This is up to the caller to clear the passed array. Neither copies of nor
-        * references to the passed arrays are kept
-        */
-       public PasswordEncryption(char[] password, byte[] passwordSalt, byte[] initializationVector) {
-               try {
-                       initKeyAndCiphers(password, passwordSalt, initializationVector);
-               } catch (InvalidKeyException e) {
-                       Integer previousSecreteKeyLength = secreteKeyLength;
-                       secreteKeyLength = DEFAULT_SECRETE_KEY_LENGTH_RESTRICTED;
-                       System.err.println("'" + e.getMessage() + "', will use " + secreteKeyLength
-                                       + " secrete key length instead of " + previousSecreteKeyLength);
-                       try {
-                               initKeyAndCiphers(password, passwordSalt, initializationVector);
-                       } catch (GeneralSecurityException e1) {
-                               throw new IllegalStateException("Cannot get secret key (with restricted length)", e1);
-                       }
-               } catch (GeneralSecurityException e) {
-                       throw new IllegalStateException("Cannot get secret key", e);
-               }
-       }
-
-       protected void initKeyAndCiphers(char[] password, byte[] passwordSalt, byte[] initializationVector)
-                       throws GeneralSecurityException {
-               byte[] salt = new byte[8];
-               System.arraycopy(passwordSalt, 0, salt, 0, salt.length);
-               // for (int i = 0; i < password.length && i < salt.length; i++)
-               // salt[i] = (byte) password[i];
-               byte[] iv = new byte[16];
-               System.arraycopy(initializationVector, 0, iv, 0, iv.length);
-
-               SecretKeyFactory keyFac = SecretKeyFactory.getInstance(getSecretKeyFactoryName());
-               PBEKeySpec keySpec = new PBEKeySpec(password, salt, getIterationCount(), getKeyLength());
-               String secKeyEncryption = getSecretKeyEncryption();
-               if (secKeyEncryption != null) {
-                       SecretKey tmp = keyFac.generateSecret(keySpec);
-                       key = new SecretKeySpec(tmp.getEncoded(), getSecretKeyEncryption());
-               } else {
-                       key = keyFac.generateSecret(keySpec);
-               }
-               if (securityProviderName != null)
-                       ecipher = Cipher.getInstance(getCipherName(), securityProviderName);
-               else
-                       ecipher = Cipher.getInstance(getCipherName());
-               ecipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
-               dcipher = Cipher.getInstance(getCipherName());
-               dcipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
-       }
-
-       public void encrypt(InputStream decryptedIn, OutputStream encryptedOut) throws IOException {
-               try {
-                       CipherOutputStream out = new CipherOutputStream(encryptedOut, ecipher);
-                       StreamUtils.copy(decryptedIn, out);
-                       StreamUtils.closeQuietly(out);
-               } catch (IOException e) {
-                       throw e;
-               } finally {
-                       StreamUtils.closeQuietly(decryptedIn);
-               }
-       }
-
-       public void decrypt(InputStream encryptedIn, OutputStream decryptedOut) throws IOException {
-               try {
-                       CipherInputStream decryptedIn = new CipherInputStream(encryptedIn, dcipher);
-                       StreamUtils.copy(decryptedIn, decryptedOut);
-               } catch (IOException e) {
-                       throw e;
-               } finally {
-                       StreamUtils.closeQuietly(encryptedIn);
-               }
-       }
-
-       public byte[] encryptString(String str) {
-               ByteArrayOutputStream out = null;
-               ByteArrayInputStream in = null;
-               try {
-                       out = new ByteArrayOutputStream();
-                       in = new ByteArrayInputStream(str.getBytes(DEFAULT_CHARSET));
-                       encrypt(in, out);
-                       return out.toByteArray();
-               } catch (IOException e) {
-                       throw new RuntimeException(e);
-               } finally {
-                       StreamUtils.closeQuietly(out);
-               }
-       }
-
-       /** Closes the input stream */
-       public String decryptAsString(InputStream in) {
-               ByteArrayOutputStream out = null;
-               try {
-                       out = new ByteArrayOutputStream();
-                       decrypt(in, out);
-                       return new String(out.toByteArray(), DEFAULT_CHARSET);
-               } catch (IOException e) {
-                       throw new RuntimeException(e);
-               } finally {
-                       StreamUtils.closeQuietly(out);
-               }
-       }
-
-       protected Key getKey() {
-               return key;
-       }
-
-       protected Cipher getEcipher() {
-               return ecipher;
-       }
-
-       protected Cipher getDcipher() {
-               return dcipher;
-       }
-
-       protected Integer getIterationCount() {
-               return iterationCount;
-       }
-
-       protected Integer getKeyLength() {
-               return secreteKeyLength;
-       }
-
-       protected String getSecretKeyFactoryName() {
-               return secreteKeyFactoryName;
-       }
-
-       protected String getSecretKeyEncryption() {
-               return secreteKeyEncryption;
-       }
-
-       protected String getCipherName() {
-               return cipherName;
-       }
-
-       public void setIterationCount(Integer iterationCount) {
-               this.iterationCount = iterationCount;
-       }
-
-       public void setSecreteKeyLength(Integer keyLength) {
-               this.secreteKeyLength = keyLength;
-       }
-
-       public void setSecreteKeyFactoryName(String secreteKeyFactoryName) {
-               this.secreteKeyFactoryName = secreteKeyFactoryName;
-       }
-
-       public void setSecreteKeyEncryption(String secreteKeyEncryption) {
-               this.secreteKeyEncryption = secreteKeyEncryption;
-       }
-
-       public void setCipherName(String cipherName) {
-               this.cipherName = cipherName;
-       }
-
-       public void setSecurityProviderName(String securityProviderName) {
-               this.securityProviderName = securityProviderName;
-       }
-}
diff --git a/org.argeo.core/src/org/argeo/util/ServiceChannel.java b/org.argeo.core/src/org/argeo/util/ServiceChannel.java
deleted file mode 100644 (file)
index 7997384..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
-package org.argeo.util;
-
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.channels.AsynchronousByteChannel;
-import java.nio.channels.CompletionHandler;
-import java.nio.channels.ReadableByteChannel;
-import java.nio.channels.WritableByteChannel;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Future;
-
-/** An {@link AsynchronousByteChannel} based on an {@link ExecutorService}. */
-public class ServiceChannel implements AsynchronousByteChannel {
-       private final ReadableByteChannel in;
-       private final WritableByteChannel out;
-
-       private boolean open = true;
-
-       private ExecutorService executor;
-
-       public ServiceChannel(ReadableByteChannel in, WritableByteChannel out, ExecutorService executor) {
-               this.in = in;
-               this.out = out;
-               this.executor = executor;
-       }
-
-       @Override
-       public Future<Integer> read(ByteBuffer dst) {
-               return executor.submit(() -> in.read(dst));
-       }
-
-       @Override
-       public <A> void read(ByteBuffer dst, A attachment, CompletionHandler<Integer, ? super A> handler) {
-               try {
-                       Future<Integer> res = read(dst);
-                       handler.completed(res.get(), attachment);
-               } catch (Exception e) {
-                       handler.failed(e, attachment);
-               }
-       }
-
-       @Override
-       public Future<Integer> write(ByteBuffer src) {
-               return executor.submit(() -> out.write(src));
-       }
-
-       @Override
-       public <A> void write(ByteBuffer src, A attachment, CompletionHandler<Integer, ? super A> handler) {
-               try {
-                       Future<Integer> res = write(src);
-                       handler.completed(res.get(), attachment);
-               } catch (Exception e) {
-                       handler.failed(e, attachment);
-               }
-       }
-
-       @Override
-       public synchronized void close() throws IOException {
-               try {
-                       in.close();
-               } catch (Exception e) {
-                       e.printStackTrace();
-               }
-               try {
-                       out.close();
-               } catch (Exception e) {
-                       e.printStackTrace();
-               }
-               open = false;
-               notifyAll();
-       }
-
-       @Override
-       public synchronized boolean isOpen() {
-               return open;
-       }
-
-}
diff --git a/org.argeo.core/src/org/argeo/util/StreamUtils.java b/org.argeo.core/src/org/argeo/util/StreamUtils.java
deleted file mode 100644 (file)
index 6d7d940..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-package org.argeo.util;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.Reader;
-import java.io.Writer;
-
-/** Utilities to be used when Apache Commons IO is not available. */
-class StreamUtils {
-       private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
-
-       /*
-        * APACHE COMMONS IO (inspired)
-        */
-
-       /** @return the number of bytes */
-       public static Long copy(InputStream in, OutputStream out)
-                       throws IOException {
-               Long count = 0l;
-               byte[] buf = new byte[DEFAULT_BUFFER_SIZE];
-               while (true) {
-                       int length = in.read(buf);
-                       if (length < 0)
-                               break;
-                       out.write(buf, 0, length);
-                       count = count + length;
-               }
-               return count;
-       }
-
-       /** @return the number of chars */
-       public static Long copy(Reader in, Writer out) throws IOException {
-               Long count = 0l;
-               char[] buf = new char[DEFAULT_BUFFER_SIZE];
-               while (true) {
-                       int length = in.read(buf);
-                       if (length < 0)
-                               break;
-                       out.write(buf, 0, length);
-                       count = count + length;
-               }
-               return count;
-       }
-
-       public static void closeQuietly(InputStream in) {
-               if (in != null)
-                       try {
-                               in.close();
-                       } catch (Exception e) {
-                               //
-                       }
-       }
-
-       public static void closeQuietly(OutputStream out) {
-               if (out != null)
-                       try {
-                               out.close();
-                       } catch (Exception e) {
-                               //
-                       }
-       }
-
-       public static void closeQuietly(Reader in) {
-               if (in != null)
-                       try {
-                               in.close();
-                       } catch (Exception e) {
-                               //
-                       }
-       }
-
-       public static void closeQuietly(Writer out) {
-               if (out != null)
-                       try {
-                               out.close();
-                       } catch (Exception e) {
-                               //
-                       }
-       }
-}
diff --git a/org.argeo.core/src/org/argeo/util/Tester.java b/org.argeo.core/src/org/argeo/util/Tester.java
deleted file mode 100644 (file)
index 31a2be4..0000000
+++ /dev/null
@@ -1,126 +0,0 @@
-package org.argeo.util;
-
-import java.lang.reflect.Method;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
-
-/** A generic tester based on Java assertions and functional programming. */
-public class Tester {
-       private Map<String, TesterStatus> results = Collections.synchronizedSortedMap(new TreeMap<>());
-
-       private ClassLoader classLoader;
-
-       /** Use {@link Thread#getContextClassLoader()} by default. */
-       public Tester() {
-               this(Thread.currentThread().getContextClassLoader());
-       }
-
-       public Tester(ClassLoader classLoader) {
-               this.classLoader = classLoader;
-       }
-
-       public void execute(String className) {
-               Class<?> clss;
-               try {
-                       clss = classLoader.loadClass(className);
-                       boolean assertionsEnabled = clss.desiredAssertionStatus();
-                       if (!assertionsEnabled)
-                               throw new IllegalStateException("Test runner " + getClass().getName()
-                                               + " requires Java assertions to be enabled. Call the JVM with the -ea argument.");
-               } catch (Exception e1) {
-                       throw new IllegalArgumentException("Cannot initalise test for " + className, e1);
-
-               }
-               List<Method> methods = findMethods(clss);
-               if (methods.size() == 0)
-                       throw new IllegalArgumentException("No test method found in " + clss);
-               // TODO make order more predictable?
-               for (Method method : methods) {
-                       String uid = method.getDeclaringClass().getName() + "#" + method.getName();
-                       TesterStatus testStatus = new TesterStatus(uid);
-                       Object obj = null;
-                       try {
-                               beforeTest(uid, method);
-                               obj = clss.getDeclaredConstructor().newInstance();
-                               method.invoke(obj);
-                               testStatus.setPassed();
-                               afterTestPassed(uid, method, obj);
-                       } catch (Exception e) {
-                               testStatus.setFailed(e);
-                               afterTestFailed(uid, method, obj, e);
-                       } finally {
-                               results.put(uid, testStatus);
-                       }
-               }
-       }
-
-       protected void beforeTest(String uid, Method method) {
-               // System.out.println(uid + ": STARTING");
-       }
-
-       protected void afterTestPassed(String uid, Method method, Object obj) {
-               System.out.println(uid + ": PASSED");
-       }
-
-       protected void afterTestFailed(String uid, Method method, Object obj, Throwable e) {
-               System.out.println(uid + ": FAILED");
-               e.printStackTrace();
-       }
-
-       protected List<Method> findMethods(Class<?> clss) {
-               List<Method> methods = new ArrayList<Method>();
-//             Method call = getMethod(clss, "call");
-//             if (call != null)
-//                     methods.add(call);
-//
-               for (Method method : clss.getMethods()) {
-                       if (method.getName().startsWith("test")) {
-                               methods.add(method);
-                       }
-               }
-               return methods;
-       }
-
-       protected Method getMethod(Class<?> clss, String name, Class<?>... parameterTypes) {
-               try {
-                       return clss.getMethod(name, parameterTypes);
-               } catch (NoSuchMethodException e) {
-                       return null;
-               } catch (SecurityException e) {
-                       throw new IllegalStateException(e);
-               }
-       }
-
-       public static void main(String[] args) {
-               // deal with arguments
-               String className;
-               if (args.length < 1) {
-                       System.err.println(usage());
-                       System.exit(1);
-                       throw new IllegalArgumentException();
-               } else {
-                       className = args[0];
-               }
-
-               Tester test = new Tester();
-               try {
-                       test.execute(className);
-               } catch (Throwable e) {
-                       e.printStackTrace();
-               }
-
-               Map<String, TesterStatus> r = test.results;
-               for (String uid : r.keySet()) {
-                       TesterStatus testStatus = r.get(uid);
-                       System.out.println(testStatus);
-               }
-       }
-
-       public static String usage() {
-               return "java " + Tester.class.getName() + " [test class name]";
-
-       }
-}
diff --git a/org.argeo.core/src/org/argeo/util/TesterStatus.java b/org.argeo.core/src/org/argeo/util/TesterStatus.java
deleted file mode 100644 (file)
index d1d14ed..0000000
+++ /dev/null
@@ -1,98 +0,0 @@
-package org.argeo.util;
-
-import java.io.Serializable;
-
-/** The status of a test. */
-public class TesterStatus implements Serializable {
-       private static final long serialVersionUID = 6272975746885487000L;
-
-       private Boolean passed = null;
-       private final String uid;
-       private Throwable throwable = null;
-
-       public TesterStatus(String uid) {
-               this.uid = uid;
-       }
-
-       /** For cloning. */
-       public TesterStatus(String uid, Boolean passed, Throwable throwable) {
-               this(uid);
-               this.passed = passed;
-               this.throwable = throwable;
-       }
-
-       public synchronized Boolean isRunning() {
-               return passed == null;
-       }
-
-       public synchronized Boolean isPassed() {
-               assert passed != null;
-               return passed;
-       }
-
-       public synchronized Boolean isFailed() {
-               assert passed != null;
-               return !passed;
-       }
-
-       public synchronized void setPassed() {
-               setStatus(true);
-       }
-
-       public synchronized void setFailed() {
-               setStatus(false);
-       }
-
-       public synchronized void setFailed(Throwable throwable) {
-               setStatus(false);
-               setThrowable(throwable);
-       }
-
-       protected void setStatus(Boolean passed) {
-               if (this.passed != null)
-                       throw new IllegalStateException("Passed status of test " + uid + " is already set (to " + passed + ")");
-               this.passed = passed;
-       }
-
-       protected void setThrowable(Throwable throwable) {
-               if (this.throwable != null)
-                       throw new IllegalStateException("Throwable of test " + uid + " is already set (to " + passed + ")");
-               this.throwable = throwable;
-       }
-
-       public String getUid() {
-               return uid;
-       }
-
-       public Throwable getThrowable() {
-               return throwable;
-       }
-
-       @Override
-       protected Object clone() throws CloneNotSupportedException {
-               // TODO Auto-generated method stub
-               return super.clone();
-       }
-
-       @Override
-       public boolean equals(Object o) {
-               if (o instanceof TesterStatus) {
-                       TesterStatus other = (TesterStatus) o;
-                       // we don't check consistency for performance purposes
-                       // this equals() is supposed to be used in collections or for transfer
-                       return other.uid.equals(uid);
-               }
-               return false;
-       }
-
-       @Override
-       public int hashCode() {
-               return uid.hashCode();
-       }
-
-       @Override
-       public String toString() {
-               return uid + "\t" + (passed ? "passed" : "failed");
-       }
-
-}
diff --git a/org.argeo.core/src/org/argeo/util/Throughput.java b/org.argeo.core/src/org/argeo/util/Throughput.java
deleted file mode 100644 (file)
index 266ddbc..0000000
+++ /dev/null
@@ -1,82 +0,0 @@
-package org.argeo.util;
-
-import java.text.NumberFormat;
-import java.text.ParseException;
-import java.util.Locale;
-
-/** A throughput, that is, a value per unit of time. */
-public class Throughput {
-       private final static NumberFormat usNumberFormat = NumberFormat.getInstance(Locale.US);
-
-       public enum Unit {
-               s, m, h, d
-       }
-
-       private final Double value;
-       private final Unit unit;
-
-       public Throughput(Double value, Unit unit) {
-               this.value = value;
-               this.unit = unit;
-       }
-
-       public Throughput(Long periodMs, Long count, Unit unit) {
-               if (unit.equals(Unit.s))
-                       value = ((double) count * 1000d) / periodMs;
-               else if (unit.equals(Unit.m))
-                       value = ((double) count * 60d * 1000d) / periodMs;
-               else if (unit.equals(Unit.h))
-                       value = ((double) count * 60d * 60d * 1000d) / periodMs;
-               else if (unit.equals(Unit.d))
-                       value = ((double) count * 24d * 60d * 60d * 1000d) / periodMs;
-               else
-                       throw new IllegalArgumentException("Unsupported unit " + unit);
-               this.unit = unit;
-       }
-
-       public Throughput(Double value, String unitStr) {
-               this(value, Unit.valueOf(unitStr));
-       }
-
-       public Throughput(String def) {
-               int index = def.indexOf('/');
-               if (def.length() < 3 || index <= 0 || index != def.length() - 2)
-                       throw new IllegalArgumentException(
-                                       def + " no a proper throughput definition" + " (should be <value>/<unit>, e.g. 3.54/s or 1500/h");
-               String valueStr = def.substring(0, index);
-               String unitStr = def.substring(index + 1);
-               try {
-                       this.value = usNumberFormat.parse(valueStr).doubleValue();
-               } catch (ParseException e) {
-                       throw new IllegalArgumentException("Cannot parse " + valueStr + " as a number.", e);
-               }
-               this.unit = Unit.valueOf(unitStr);
-       }
-
-       public Long asMsPeriod() {
-               if (unit.equals(Unit.s))
-                       return Math.round(1000d / value);
-               else if (unit.equals(Unit.m))
-                       return Math.round((60d * 1000d) / value);
-               else if (unit.equals(Unit.h))
-                       return Math.round((60d * 60d * 1000d) / value);
-               else if (unit.equals(Unit.d))
-                       return Math.round((24d * 60d * 60d * 1000d) / value);
-               else
-                       throw new IllegalArgumentException("Unsupported unit " + unit);
-       }
-
-       @Override
-       public String toString() {
-               return usNumberFormat.format(value) + '/' + unit;
-       }
-
-       public Double getValue() {
-               return value;
-       }
-
-       public Unit getUnit() {
-               return unit;
-       }
-
-}
diff --git a/org.argeo.core/src/org/argeo/util/UuidUtils.java b/org.argeo.core/src/org/argeo/util/UuidUtils.java
deleted file mode 100644 (file)
index 7584abc..0000000
+++ /dev/null
@@ -1,378 +0,0 @@
-package org.argeo.util;
-
-import java.net.InetAddress;
-import java.net.NetworkInterface;
-import java.net.SocketException;
-import java.net.UnknownHostException;
-import java.security.SecureRandom;
-import java.time.Duration;
-import java.time.LocalDateTime;
-import java.time.ZoneOffset;
-import java.util.BitSet;
-import java.util.Random;
-import java.util.UUID;
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * Utilities to simplify and extends usage of {@link UUID}. Only the RFC 4122
- * variant (also known as Leach–Salz variant) is supported.
- */
-public class UuidUtils {
-       /** Nil UUID (00000000-0000-0000-0000-000000000000). */
-       public final static UUID NIL_UUID = UUID.fromString("00000000-0000-0000-0000-000000000000");
-       public final static LocalDateTime GREGORIAN_START = LocalDateTime.of(1582, 10, 15, 0, 0, 0);
-
-       private final static long MOST_SIG_VERSION1 = (1l << 12);
-       private final static long LEAST_SIG_RFC4122_VARIANT = (1l << 63);
-
-       private final static SecureRandom RANDOM;
-       private final static AtomicInteger CLOCK_SEQUENCE;
-       private final static byte[] HARDWARE_ADDRESS;
-       /** A start timestamp to which {@link System#nanoTime()}/100 can be added. */
-       private final static long START_TIMESTAMP;
-       static {
-               RANDOM = new SecureRandom();
-               CLOCK_SEQUENCE = new AtomicInteger(RANDOM.nextInt(16384));
-               HARDWARE_ADDRESS = getHardwareAddress();
-
-               long nowVm = System.nanoTime() / 100;
-               Duration duration = Duration.between(GREGORIAN_START, LocalDateTime.now(ZoneOffset.UTC));
-               START_TIMESTAMP = (duration.getSeconds() * 10000000 + duration.getNano() / 100) - nowVm;
-       }
-
-       private static byte[] getHardwareAddress() {
-               InetAddress localHost;
-               try {
-                       localHost = InetAddress.getLocalHost();
-                       try {
-                               NetworkInterface nic = NetworkInterface.getByInetAddress(localHost);
-                               return nic.getHardwareAddress();
-                       } catch (SocketException e) {
-                               return null;
-                       }
-               } catch (UnknownHostException e) {
-                       return null;
-               }
-
-       }
-
-       public static UUID timeUUIDwithRandomNode() {
-               long timestamp = START_TIMESTAMP + System.nanoTime() / 100;
-               return timeUUID(timestamp, RANDOM);
-       }
-
-       public static UUID timeUUID(long timestamp, Random random) {
-               byte[] node = new byte[6];
-               random.nextBytes(node);
-               node[0] = (byte) (node[0] | 1);
-               long clockSequence = CLOCK_SEQUENCE.incrementAndGet();
-               return timeUUID(timestamp, clockSequence, node);
-       }
-
-       public static UUID timeUUID() {
-               long timestamp = START_TIMESTAMP + System.nanoTime() / 100;
-               return timeUUID(timestamp);
-       }
-
-       public static UUID timeUUID(long timestamp) {
-               if (HARDWARE_ADDRESS == null)
-                       return timeUUID(timestamp, RANDOM);
-               long clockSequence = CLOCK_SEQUENCE.incrementAndGet();
-               return timeUUID(timestamp, clockSequence, HARDWARE_ADDRESS);
-       }
-
-       public static UUID timeUUID(long timestamp, NetworkInterface nic) {
-               byte[] node;
-               try {
-                       node = nic.getHardwareAddress();
-               } catch (SocketException e) {
-                       throw new IllegalStateException("Cannot get hardware address", e);
-               }
-               long clockSequence = CLOCK_SEQUENCE.incrementAndGet();
-               return timeUUID(timestamp, clockSequence, node);
-       }
-
-       public static UUID timeUUID(LocalDateTime time, long clockSequence, byte[] node) {
-               Duration duration = Duration.between(GREGORIAN_START, time);
-               // Number of 100 ns intervals in one second: 1000000000 / 100 = 10000000
-               long timestamp = duration.getSeconds() * 10000000 + duration.getNano() / 100;
-               return timeUUID(timestamp, clockSequence, node);
-       }
-
-       public static UUID timeUUID(long timestamp, long clockSequence, byte[] node) {
-               assert node.length >= 6;
-
-               long mostSig = MOST_SIG_VERSION1 // base for version 1 UUID
-                               | ((timestamp & 0xFFFFFFFFL) << 32) // time_low
-                               | (((timestamp >> 32) & 0xFFFFL) << 16) // time_mid
-                               | ((timestamp >> 48) & 0x0FFFL);// time_hi_and_version
-
-               long leastSig = LEAST_SIG_RFC4122_VARIANT // base for Leach–Salz UUID
-                               | (((clockSequence & 0x3F00) >> 8) << 56) // clk_seq_hi_res
-                               | ((clockSequence & 0xFF) << 48) // clk_seq_low
-                               | (node[0] & 0xFFL) //
-                               | ((node[1] & 0xFFL) << 8) //
-                               | ((node[2] & 0xFFL) << 16) //
-                               | ((node[3] & 0xFFL) << 24) //
-                               | ((node[4] & 0xFFL) << 32) //
-                               | ((node[5] & 0xFFL) << 40); //
-//             for (int i = 0; i < 6; i++) {
-//                     leastSig = leastSig | ((node[i] & 0xFFL) << (8 * i));
-//             }
-               UUID uuid = new UUID(mostSig, leastSig);
-
-               // tests
-               assert uuid.node() == BitSet.valueOf(node).toLongArray()[0];
-               assert uuid.timestamp() == timestamp;
-               assert uuid.clockSequence() == clockSequence;
-               assert uuid.version() == 1;
-               assert uuid.variant() == 2;
-               return uuid;
-       }
-
-       @Deprecated
-       public static UUID timeBasedUUID() {
-               return timeBasedUUID(LocalDateTime.now(ZoneOffset.UTC));
-       }
-
-       @Deprecated
-       public static UUID timeBasedRandomUUID() {
-               return timeBasedRandomUUID(LocalDateTime.now(ZoneOffset.UTC), RANDOM);
-       }
-
-       @Deprecated
-       public static UUID timeBasedUUID(LocalDateTime time) {
-               if (HARDWARE_ADDRESS == null)
-                       return timeBasedRandomUUID(time, RANDOM);
-               return timeBasedUUID(time, BitSet.valueOf(HARDWARE_ADDRESS));
-       }
-
-       @Deprecated
-       public static UUID timeBasedAddressUUID(LocalDateTime time, NetworkInterface nic) throws SocketException {
-               byte[] nodeBytes = nic.getHardwareAddress();
-               BitSet node = BitSet.valueOf(nodeBytes);
-               return timeBasedUUID(time, node);
-       }
-
-       @Deprecated
-       public static UUID timeBasedRandomUUID(LocalDateTime time, Random random) {
-               byte[] nodeBytes = new byte[6];
-               random.nextBytes(nodeBytes);
-               BitSet node = BitSet.valueOf(nodeBytes);
-               // set random marker
-               node.set(0, true);
-               return timeBasedUUID(time, node);
-       }
-
-       @Deprecated
-       public static UUID timeBasedUUID(LocalDateTime time, BitSet node) {
-               // most significant
-               Duration duration = Duration.between(GREGORIAN_START, time);
-
-               // Number of 100 ns intervals in one second: 1000000000 / 100 = 10000000
-               long timeNanos = duration.getSeconds() * 10000000 + duration.getNano() / 100;
-               BitSet timeBits = BitSet.valueOf(new long[] { timeNanos });
-               assert timeBits.length() <= 60;
-
-               int clockSequence;
-               synchronized (CLOCK_SEQUENCE) {
-                       clockSequence = CLOCK_SEQUENCE.incrementAndGet();
-                       if (clockSequence > 16384)
-                               CLOCK_SEQUENCE.set(0);
-               }
-               BitSet clockSequenceBits = BitSet.valueOf(new long[] { clockSequence });
-
-               // Build the UUID, bit by bit
-               // see https://tools.ietf.org/html/rfc4122#section-4.2.2
-               // time
-               BitSet time_low = new BitSet(32);
-               BitSet time_mid = new BitSet(16);
-               BitSet time_hi_and_version = new BitSet(16);
-
-               for (int i = 0; i < 60; i++) {
-                       if (i < 32)
-                               time_low.set(i, timeBits.get(i));
-                       else if (i < 48)
-                               time_mid.set(i - 32, timeBits.get(i));
-                       else
-                               time_hi_and_version.set(i - 48, timeBits.get(i));
-               }
-               // version
-               time_hi_and_version.set(12, true);
-               time_hi_and_version.set(13, false);
-               time_hi_and_version.set(14, false);
-               time_hi_and_version.set(15, false);
-
-               // clock sequence
-               BitSet clk_seq_hi_res = new BitSet(8);
-               BitSet clk_seq_low = new BitSet(8);
-               for (int i = 0; i < 8; i++) {
-                       clk_seq_low.set(i, clockSequenceBits.get(i));
-               }
-               for (int i = 8; i < 14; i++) {
-                       clk_seq_hi_res.set(i - 8, clockSequenceBits.get(i));
-               }
-               // variant
-               clk_seq_hi_res.set(6, false);
-               clk_seq_hi_res.set(7, true);
-
-//             String str = toHexString(time_low.toLongArray()[0]) + "-" + toHexString(time_mid.toLongArray()[0]) + "-"
-//                             + toHexString(time_hi_and_version.toLongArray()[0]) + "-"
-//                             + toHexString(clock_seq_hi_and_reserved.toLongArray()[0]) + toHexString(clock_seq_low.toLongArray()[0])
-//                             + "-" + toHexString(node.toLongArray()[0]);
-//             UUID uuid = UUID.fromString(str);
-
-               BitSet uuidBits = new BitSet(128);
-               for (int i = 0; i < 128; i++) {
-                       if (i < 48)
-                               uuidBits.set(i, node.get(i));
-                       else if (i < 56)
-                               uuidBits.set(i, clk_seq_low.get(i - 48));
-                       else if (i < 64)
-                               uuidBits.set(i, clk_seq_hi_res.get(i - 56));
-                       else if (i < 80)
-                               uuidBits.set(i, time_hi_and_version.get(i - 64));
-                       else if (i < 96)
-                               uuidBits.set(i, time_mid.get(i - 80));
-                       else
-                               uuidBits.set(i, time_low.get(i - 96));
-               }
-
-               long[] uuidLongs = uuidBits.toLongArray();
-               assert uuidLongs.length == 2;
-               UUID uuid = new UUID(uuidLongs[1], uuidLongs[0]);
-
-               // tests
-               assert uuid.node() == node.toLongArray()[0];
-               assert uuid.timestamp() == timeNanos;
-               assert uuid.clockSequence() == clockSequence;
-               assert uuid.version() == 1;
-               assert uuid.variant() == 2;
-               return uuid;
-       }
-
-       public static String toBinaryString(UUID uuid, int charsPerSegment, char separator) {
-               String binaryString = toBinaryString(uuid);
-               StringBuilder sb = new StringBuilder(128 + (128 / charsPerSegment));
-               for (int i = 0; i < binaryString.length(); i++) {
-                       if (i != 0 && i % charsPerSegment == 0)
-                               sb.append(separator);
-                       sb.append(binaryString.charAt(i));
-               }
-               return sb.toString();
-       }
-
-       public static String toBinaryString(UUID uuid) {
-               String most = zeroTo64Chars(Long.toBinaryString(uuid.getMostSignificantBits()));
-               String least = zeroTo64Chars(Long.toBinaryString(uuid.getLeastSignificantBits()));
-               String binaryString = most + least;
-               assert binaryString.length() == 128;
-               return binaryString;
-       }
-
-       private static String zeroTo64Chars(String str) {
-               assert str.length() <= 64;
-               if (str.length() < 64) {
-                       StringBuilder sb = new StringBuilder(64);
-                       for (int i = 0; i < 64 - str.length(); i++)
-                               sb.append('0');
-                       sb.append(str);
-                       return sb.toString();
-               } else
-                       return str;
-       }
-
-       public static String compactToStd(String compact) {
-               if (compact.length() != 32)
-                       throw new IllegalArgumentException(
-                                       "Compact UUID '" + compact + "' has length " + compact.length() + " and not 32.");
-               StringBuilder sb = new StringBuilder(36);
-               for (int i = 0; i < 32; i++) {
-                       if (i == 8 || i == 12 || i == 16 || i == 20)
-                               sb.append('-');
-                       sb.append(compact.charAt(i));
-               }
-               String std = sb.toString();
-               assert std.length() == 36;
-               assert UUID.fromString(std).toString().equals(std);
-               return std;
-       }
-
-       public static UUID compactToUuid(String compact) {
-               return UUID.fromString(compactToStd(compact));
-       }
-       
-       public static String firstBlock(UUID uuid) {
-               return uuid.toString().substring(0, 8);
-       }
-
-       public static boolean isRandom(UUID uuid) {
-               return uuid.version() == 4;
-       }
-
-       public static boolean isTimeBased(UUID uuid) {
-               return uuid.version() == 1;
-       }
-
-       public static boolean isTimeBasedRandom(UUID uuid) {
-               if (uuid.version() == 1) {
-                       BitSet node = BitSet.valueOf(new long[] { uuid.node() });
-                       return node.get(0);
-               } else
-                       return false;
-       }
-
-       public static boolean isNameBased(UUID uuid) {
-               return uuid.version() == 3 || uuid.version() == 5;
-       }
-
-       /** Singleton. */
-       private UuidUtils() {
-       }
-
-       public final static void main(String[] args) throws Exception {
-               UUID uuid;
-
-//             uuid = compactToUuid("996b1f5122de4b2f94e49168d32f22d1");
-//             System.out.println(uuid.toString() + ", isRandom=" + isRandom(uuid));
-
-               // warm up before measuring perf
-               for (int i = 0; i < 10; i++) {
-                       UUID.randomUUID();
-                       timeUUID();
-                       timeUUIDwithRandomNode();
-                       timeBasedRandomUUID();
-                       timeBasedUUID();
-               }
-
-               long begin;
-               long duration;
-
-               begin = System.nanoTime();
-               uuid = UUID.randomUUID();
-               duration = System.nanoTime() - begin;
-               System.out.println(uuid.toString() + " in " + duration + " ns, isRandom=" + isRandom(uuid));
-
-               begin = System.nanoTime();
-               uuid = timeUUID();
-               duration = System.nanoTime() - begin;
-               System.out.println(uuid.toString() + " in " + duration + " ns, isTimeBasedRandom=" + isTimeBasedRandom(uuid));
-
-               begin = System.nanoTime();
-               uuid = timeUUIDwithRandomNode();
-               duration = System.nanoTime() - begin;
-               System.out.println(uuid.toString() + " in " + duration + " ns, isTimeBasedRandom=" + isTimeBasedRandom(uuid));
-
-               begin = System.nanoTime();
-               uuid = timeBasedUUID();
-               duration = System.nanoTime() - begin;
-               System.out.println(uuid.toString() + " in " + duration + " ns, isTimeBasedRandom=" + isTimeBasedRandom(uuid));
-
-               begin = System.nanoTime();
-               uuid = timeBasedRandomUUID();
-               duration = System.nanoTime() - begin;
-               System.out.println(uuid.toString() + " in " + duration + " ns, isTimeBasedRandom=" + isTimeBasedRandom(uuid));
-//             System.out.println(toBinaryString(uuid, 8, ' '));
-//             System.out.println(toBinaryString(uuid, 16, '\n'));
-       }
-}
diff --git a/org.argeo.core/src/org/argeo/util/package-info.java b/org.argeo.core/src/org/argeo/util/package-info.java
deleted file mode 100644 (file)
index 4354b0a..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-/** Generic Java utilities. */
-package org.argeo.util;
\ No newline at end of file
diff --git a/org.argeo.enterprise/ext/test/org/argeo/util/CsvParserEncodingTest.java b/org.argeo.enterprise/ext/test/org/argeo/util/CsvParserEncodingTest.java
new file mode 100644 (file)
index 0000000..09443c2
--- /dev/null
@@ -0,0 +1,36 @@
+package org.argeo.util;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.List;
+
+/** Tests that {@link CsvParser} can deal properly with encodings. */
+public class CsvParserEncodingTest {
+
+       private String iso = "ISO-8859-1";
+       private String utf8 = "UTF-8";
+
+       public void testParse() throws Exception {
+
+               String xml = new String("áéíóúñ,éééé");
+               byte[] utfBytes = xml.getBytes(utf8);
+               byte[] isoBytes = xml.getBytes(iso);
+
+               InputStream inUtf = new ByteArrayInputStream(utfBytes);
+               InputStream inIso = new ByteArrayInputStream(isoBytes);
+
+               CsvParser csvParser = new CsvParser() {
+                       protected void processLine(Integer lineNumber, List<String> header, List<String> tokens) {
+                               assert header.size() == tokens.size();
+                               assert 2 == tokens.size();
+                               assert "áéíóúñ".equals(tokens.get(0));
+                               assert "éééé".equals(tokens.get(1));
+                       }
+               };
+
+               csvParser.parse(inUtf, utf8);
+               inUtf.close();
+               csvParser.parse(inIso, iso);
+               inIso.close();
+       }
+}
diff --git a/org.argeo.enterprise/ext/test/org/argeo/util/CsvParserParseFileTest.java b/org.argeo.enterprise/ext/test/org/argeo/util/CsvParserParseFileTest.java
new file mode 100644 (file)
index 0000000..5a92c68
--- /dev/null
@@ -0,0 +1,25 @@
+package org.argeo.util;
+
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Test that {@link CsvParser} can properly parse a CSV file. */
+public class CsvParserParseFileTest {
+       public void testParse() throws Exception {
+
+               final Map<Integer, Map<String, String>> lines = new HashMap<Integer, Map<String, String>>();
+               InputStream in = getClass().getResourceAsStream("/org/argeo/util/ReferenceFile.csv");
+               CsvParserWithLinesAsMap parser = new CsvParserWithLinesAsMap() {
+                       protected void processLine(Integer lineNumber, Map<String, String> line) {
+                               lines.put(lineNumber, line);
+                       }
+               };
+
+               parser.parse(in);
+               in.close();
+
+               assert 5 == lines.size();
+       }
+
+}
diff --git a/org.argeo.enterprise/ext/test/org/argeo/util/CsvParserTest.java b/org.argeo.enterprise/ext/test/org/argeo/util/CsvParserTest.java
new file mode 100644 (file)
index 0000000..e59dbd1
--- /dev/null
@@ -0,0 +1,30 @@
+package org.argeo.util;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.List;
+
+/** {@link CsvParser} tests. */
+public class CsvParserTest {
+       public void testParse() throws Exception {
+               String toParse = "Header1,\"Header\n2\",Header3,\"Header4\"\n" + "Col1,\"Col\n2\",Col3,\"\"\"Col4\"\"\"\n"
+                               + "Col1,\"Col\n2\",Col3,\"\"\"Col4\"\"\"\n" + "Col1,\"Col\n2\",Col3,\"\"\"Col4\"\"\"\n";
+
+               InputStream in = new ByteArrayInputStream(toParse.getBytes());
+
+               CsvParser csvParser = new CsvParser() {
+                       protected void processLine(Integer lineNumber, List<String> header, List<String> tokens) {
+                               assert header.size() == tokens.size();
+                               assert 4 == tokens.size();
+                               assert "Col1".equals(tokens.get(0));
+                               assert "Col\n2".equals(tokens.get(1));
+                               assert "Col3".equals(tokens.get(2));
+                               assert "\"Col4\"".equals(tokens.get(3));
+                       }
+               };
+
+               csvParser.parse(in);
+               in.close();
+       }
+
+}
diff --git a/org.argeo.enterprise/ext/test/org/argeo/util/CsvParserWithQuotedSeparatorTest.java b/org.argeo.enterprise/ext/test/org/argeo/util/CsvParserWithQuotedSeparatorTest.java
new file mode 100644 (file)
index 0000000..67ba346
--- /dev/null
@@ -0,0 +1,58 @@
+package org.argeo.util;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Test that {@link CsvParser} deals properly with "" quotes. */
+public class CsvParserWithQuotedSeparatorTest {
+       public void testSimpleParse() throws Exception {
+               String toParse = "Header1,\"Header2\",Header3,\"Header4\"\n"
+                               + "\"Col1, Col2\",\"Col\n2\",Col3,\"\"\"Col4\"\"\"\n";
+
+               InputStream in = new ByteArrayInputStream(toParse.getBytes());
+
+               CsvParser csvParser = new CsvParser() {
+                       protected void processLine(Integer lineNumber, List<String> header, List<String> tokens) {
+                               assert header.size() == tokens.size();
+                               assert 4 == tokens.size();
+                               assert "Col1, Col2".equals(tokens.get(0));
+                       }
+               };
+               // System.out.println(toParse);
+               csvParser.parse(in);
+               in.close();
+
+       }
+
+       public void testParseFile() throws Exception {
+
+               final Map<Integer, Map<String, String>> lines = new HashMap<Integer, Map<String, String>>();
+               InputStream in = getClass().getResourceAsStream("/org/argeo/util/ReferenceFile.csv");
+
+               CsvParserWithLinesAsMap parser = new CsvParserWithLinesAsMap() {
+                       protected void processLine(Integer lineNumber, Map<String, String> line) {
+                               // System.out.println("processing line #" + lineNumber);
+                               lines.put(lineNumber, line);
+                       }
+               };
+
+               parser.parse(in);
+               in.close();
+
+               Map<String, String> line = lines.get(2);
+               assert ",,,,".equals(line.get("Coma testing"));
+               line = lines.get(3);
+               assert ",, ,,".equals(line.get("Coma testing"));
+               line = lines.get(4);
+               assert "module1, module2".equals(line.get("Coma testing"));
+               line = lines.get(5);
+               assert "module1,module2".equals(line.get("Coma testing"));
+               line = lines.get(6);
+               assert ",module1,module2, \nmodule3, module4".equals(line.get("Coma testing"));
+               assert 5 == lines.size();
+
+       }
+}
diff --git a/org.argeo.enterprise/ext/test/org/argeo/util/CsvWriterTest.java b/org.argeo.enterprise/ext/test/org/argeo/util/CsvWriterTest.java
new file mode 100644 (file)
index 0000000..ff5dcc5
--- /dev/null
@@ -0,0 +1,47 @@
+package org.argeo.util;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/** {@link CsvWriter} tests. */
+public class CsvWriterTest {
+       public void testWrite() throws Exception {
+               ByteArrayOutputStream out = new ByteArrayOutputStream();
+               final CsvWriter csvWriter = new CsvWriter(out);
+
+               String[] header = { "Header1", "Header 2", "Header,3", "Header\n4", "Header\"5\"" };
+               String[] line1 = { "Value1", "Value 2", "Value,3", "Value\n4", "Value\"5\"" };
+               csvWriter.writeLine(Arrays.asList(header));
+               csvWriter.writeLine(Arrays.asList(line1));
+
+               String reference = "Header1,Header 2,\"Header,3\",\"Header\n4\",\"Header\"\"5\"\"\"\n"
+                               + "Value1,Value 2,\"Value,3\",\"Value\n4\",\"Value\"\"5\"\"\"\n";
+               String written = new String(out.toByteArray());
+               assert reference.equals(written);
+               out.close();
+               System.out.println(written);
+
+               final List<String> allTokens = new ArrayList<String>();
+               CsvParser csvParser = new CsvParser() {
+                       protected void processLine(Integer lineNumber, List<String> header, List<String> tokens) {
+                               if (lineNumber == 2)
+                                       allTokens.addAll(header);
+                               allTokens.addAll(tokens);
+                       }
+               };
+               ByteArrayInputStream in = new ByteArrayInputStream(written.getBytes());
+               csvParser.parse(in);
+               in.close();
+               List<String> allTokensRef = new ArrayList<String>();
+               allTokensRef.addAll(Arrays.asList(header));
+               allTokensRef.addAll(Arrays.asList(line1));
+
+               assert allTokensRef.size() == allTokens.size();
+               for (int i = 0; i < allTokensRef.size(); i++)
+                       assert allTokensRef.get(i).equals(allTokens.get(i));
+       }
+
+}
diff --git a/org.argeo.enterprise/ext/test/org/argeo/util/ReferenceFile.csv b/org.argeo.enterprise/ext/test/org/argeo/util/ReferenceFile.csv
new file mode 100644 (file)
index 0000000..351453d
--- /dev/null
@@ -0,0 +1,37 @@
+"ID","A long Text","Name","Other","Number","Reference","Target","Date","Update","Language","ID Ref","Weird chars","line feeds","after line feed","Empty column","Status comment","Comments","Empty","Coma testing"
+"AK251","Everything & with some line feed 
+ more “some” quote","Marge S.",,78.6,"A1155222221111268515131",,12/12/12,03/12/08,,9821308500721,"%%%ùù","ao","Nothing special",,,"Some very usefull comment",,",,,,"
+"AG254","same","Roger “wallace” Big","15 – JI",78.5,"A1155222221111268515131","next milestone",12/12/12,03/12/08,"_fr (French - France)",9812309500953,"***µ”","a
+
+
+
+
+o","after line feed",,"Do the job",,,",, ,,"
+"FG211","Very long text with some bullets.
+1 first
+2 second
+3. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long","Father & Son","15 – JI",15.4,"A1155222221111268515131","next milestone",12/12/12,03/12/08,"_fr (French - France)",9812309500952,"///","a
+
+
+
+
+
+
+o","module1,module2",,"Be fast",,,"module1, module2"
+"RRT152","Very long text with some bullets.
+1 first
+2 second
+3. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long. some more very very very long","Another $$","15 – JI",12.3,"A1155222221111268515131","next milestone",12/12/12,03/12/08,"_fr (French - France)",9812309500950,"---","a
+
+o
+
+
+","module1,module2",,,,,"module1,module2"
+"YU121","Another use case : “blank line”
+
+After the blank.","nothing with brackets( )","15 – JI",15.2,"A1155222221111268515131",,12/12/12,03/12/08,"_fr (French - France)",9812309500925,",;:?./","ao","
+
+
+
+After line feed again",,,,,",module1,module2, 
+module3, module4"
diff --git a/org.argeo.enterprise/ext/test/org/argeo/util/TestParse-ISO.csv b/org.argeo.enterprise/ext/test/org/argeo/util/TestParse-ISO.csv
new file mode 100644 (file)
index 0000000..0bec611
--- /dev/null
@@ -0,0 +1,8 @@
+"Date d'imputation","N° de compte","Code journal","Pièce interne","Pièce externe","Libellé d'écriture","Débit","Crédit","Lettrage","Quantité","Code analytique","Date d'échéance","Date d'imputation origine","Code journal origine","Mode de règlement","Date début de période","Date fin de période"
+26.01.2010,"101300","BQ","BQ01.10",,"Depot société en formation",,"3.000,00",,,,"          ",26.01.2010,"BQ","    ","          ","          "
+26.01.2010,"101300","BQ","BQ01.10",,"Depot société en formation",,"7.000,00",,,,"          ",26.01.2010,"BQ","    ","          ","          "
+26.01.2010,"411OPEN","BQ","BQ01.10",,"Vir Client ",,"2.508,00","A",,,"          ",26.01.2010,"BQ","    ","          ","          "
+26.01.2010,"455100","BQ","BQ01.10",,"Bankomat Raiffeise","250,00",,,,,"          ",26.01.2010,"BQ","    ","          ","          "
+26.01.2010,"512101","BQ","BQ01.10",,"Extrait bancaire 01.10","12.250,55",,,,,"          ",26.01.2010,"BQ","    ","          ","          "
+26.01.2010,"627800","BQ","BQ01.10",,"Envoi de chequier","2,30",,,,,"          ",26.01.2010,"BQ","    ","          ","          "
+26.01.2010,"627800","BQ","BQ01.10",,"Frais d'expedition","5,15",,,,,"          ",26.01.2010,"BQ","    ","          ","          "
diff --git a/org.argeo.enterprise/ext/test/org/argeo/util/TestParse-UTF-8.csv b/org.argeo.enterprise/ext/test/org/argeo/util/TestParse-UTF-8.csv
new file mode 100644 (file)
index 0000000..0bec611
--- /dev/null
@@ -0,0 +1,8 @@
+"Date d'imputation","N° de compte","Code journal","Pièce interne","Pièce externe","Libellé d'écriture","Débit","Crédit","Lettrage","Quantité","Code analytique","Date d'échéance","Date d'imputation origine","Code journal origine","Mode de règlement","Date début de période","Date fin de période"
+26.01.2010,"101300","BQ","BQ01.10",,"Depot société en formation",,"3.000,00",,,,"          ",26.01.2010,"BQ","    ","          ","          "
+26.01.2010,"101300","BQ","BQ01.10",,"Depot société en formation",,"7.000,00",,,,"          ",26.01.2010,"BQ","    ","          ","          "
+26.01.2010,"411OPEN","BQ","BQ01.10",,"Vir Client ",,"2.508,00","A",,,"          ",26.01.2010,"BQ","    ","          ","          "
+26.01.2010,"455100","BQ","BQ01.10",,"Bankomat Raiffeise","250,00",,,,,"          ",26.01.2010,"BQ","    ","          ","          "
+26.01.2010,"512101","BQ","BQ01.10",,"Extrait bancaire 01.10","12.250,55",,,,,"          ",26.01.2010,"BQ","    ","          ","          "
+26.01.2010,"627800","BQ","BQ01.10",,"Envoi de chequier","2,30",,,,,"          ",26.01.2010,"BQ","    ","          ","          "
+26.01.2010,"627800","BQ","BQ01.10",,"Frais d'expedition","5,15",,,,,"          ",26.01.2010,"BQ","    ","          ","          "
diff --git a/org.argeo.enterprise/ext/test/org/argeo/util/ThroughputTest.java b/org.argeo.enterprise/ext/test/org/argeo/util/ThroughputTest.java
new file mode 100644 (file)
index 0000000..d62f55c
--- /dev/null
@@ -0,0 +1,17 @@
+package org.argeo.util;
+
+public class ThroughputTest {
+       public void testParse() throws Exception {
+//             assert 0 == 1;
+
+               Throughput t;
+               t = new Throughput("3.54/s");
+               assert 3.54d == t.getValue();
+               assert Throughput.Unit.s.equals(t.getUnit());
+               assert 282l == (long) t.asMsPeriod();
+
+               t = new Throughput("35698.2569/h");
+               assert Throughput.Unit.h.equals(t.getUnit());
+               assert 101l == (long) t.asMsPeriod();
+       }
+}
diff --git a/org.argeo.enterprise/src/org/argeo/util/CsvParser.java b/org.argeo.enterprise/src/org/argeo/util/CsvParser.java
new file mode 100644 (file)
index 0000000..b903f77
--- /dev/null
@@ -0,0 +1,242 @@
+package org.argeo.util;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Parses a CSV file interpreting the first line as a header. The
+ * {@link #parse(InputStream)} method and the setters are synchronized so that
+ * the object cannot be modified when parsing.
+ */
+public abstract class CsvParser {
+       private char separator = ',';
+       private char quote = '\"';
+
+       private Boolean noHeader = false;
+       private Boolean strictLineAsLongAsHeader = true;
+
+       /**
+        * Actually process a parsed line. If
+        * {@link #setStrictLineAsLongAsHeader(Boolean)} is true (default) the header
+        * and the tokens are guaranteed to have the same size.
+        * 
+        * @param lineNumber the current line number, starts at 1 (the header, if header
+        *                   processing is enabled, the first line otherwise)
+        * @param header     the read-only header or null if
+        *                   {@link #setNoHeader(Boolean)} is true (default is false)
+        * @param tokens     the parsed tokens
+        */
+       protected abstract void processLine(Integer lineNumber, List<String> header, List<String> tokens);
+
+       /**
+        * Parses the CSV file (stream is closed at the end)
+        * 
+        * @param in the stream to parse
+        * 
+        * @deprecated Use {@link #parse(InputStream, Charset)} instead.
+        */
+       @Deprecated
+       public synchronized void parse(InputStream in) {
+               parse(in, (Charset) null);
+       }
+
+       /**
+        * Parses the CSV file (stream is closed at the end)
+        * 
+        * @param in       the stream to parse
+        * @param encoding the encoding to use.
+        * 
+        * @deprecated Use {@link #parse(InputStream, Charset)} instead.
+        */
+       @Deprecated
+       public synchronized void parse(InputStream in, String encoding) {
+               Reader reader;
+               if (encoding == null)
+                       reader = new InputStreamReader(in);
+               else
+                       try {
+                               reader = new InputStreamReader(in, encoding);
+                       } catch (UnsupportedEncodingException e) {
+                               throw new IllegalArgumentException(e);
+                       }
+               parse(reader);
+       }
+
+       /**
+        * Parses the CSV file (stream is closed at the end)
+        * 
+        * @param in      the stream to parse
+        * @param charset the charset to use
+        */
+       public synchronized void parse(InputStream in, Charset charset) {
+               Reader reader;
+               if (charset == null)
+                       reader = new InputStreamReader(in);
+               else
+                       reader = new InputStreamReader(in, charset);
+               parse(reader);
+       }
+
+       /**
+        * Parses the CSV file (stream is closed at the end)
+        * 
+        * @param reader the reader to use (it will be buffered)
+        */
+       public synchronized void parse(Reader reader) {
+               Integer lineCount = 0;
+               try (BufferedReader bufferedReader = new BufferedReader(reader)) {
+                       List<String> header = null;
+                       if (!noHeader) {
+                               String headerStr = bufferedReader.readLine();
+                               if (headerStr == null)// empty file
+                                       return;
+                               lineCount++;
+                               header = new ArrayList<String>();
+                               StringBuffer currStr = new StringBuffer("");
+                               Boolean wasInquote = false;
+                               while (parseLine(headerStr, header, currStr, wasInquote)) {
+                                       headerStr = bufferedReader.readLine();
+                                       if (headerStr == null)
+                                               break;
+                                       wasInquote = true;
+                               }
+                               header = Collections.unmodifiableList(header);
+                       }
+
+                       String line = null;
+                       lines: while ((line = bufferedReader.readLine()) != null) {
+                               line = preProcessLine(line);
+                               if (line == null) {
+                                       // skip line
+                                       continue lines;
+                               }
+                               lineCount++;
+                               List<String> tokens = new ArrayList<String>();
+                               StringBuffer currStr = new StringBuffer("");
+                               Boolean wasInquote = false;
+                               sublines: while (parseLine(line, tokens, currStr, wasInquote)) {
+                                       line = bufferedReader.readLine();
+                                       if (line == null)
+                                               break sublines;
+                                       wasInquote = true;
+                               }
+                               if (!noHeader && strictLineAsLongAsHeader) {
+                                       int headerSize = header.size();
+                                       int tokenSize = tokens.size();
+                                       if (tokenSize == 1 && line.trim().equals(""))
+                                               continue lines;// empty line
+                                       if (headerSize != tokenSize) {
+                                               throw new IllegalStateException("Token size " + tokenSize + " is different from header size "
+                                                               + headerSize + " at line " + lineCount + ", line: " + line + ", header: " + header
+                                                               + ", tokens: " + tokens);
+                                       }
+                               }
+                               processLine(lineCount, header, tokens);
+                       }
+               } catch (IOException e) {
+                       throw new RuntimeException("Cannot parse CSV file (line: " + lineCount + ")", e);
+               }
+       }
+
+       /**
+        * Called before each (logical) line is processed, giving a change to modify it
+        * (typically for cleaning dirty files). To be overridden, return the line
+        * unchanged by default. Skip the line if 'null' is returned.
+        */
+       protected String preProcessLine(String line) {
+               return line;
+       }
+
+       /**
+        * Parses a line character by character for performance purpose
+        * 
+        * @return whether to continue parsing this line
+        */
+       protected Boolean parseLine(String str, List<String> tokens, StringBuffer currStr, Boolean wasInquote) {
+               if (wasInquote)
+                       currStr.append('\n');
+
+               char[] arr = str.toCharArray();
+               boolean inQuote = wasInquote;
+               for (int i = 0; i < arr.length; i++) {
+                       char c = arr[i];
+                       if (c == separator) {
+                               if (!inQuote) {
+                                       tokens.add(currStr.toString());
+//                                     currStr.delete(0, currStr.length());
+                                       currStr.setLength(0);
+                                       currStr.trimToSize();
+                               } else {
+                                       // we don't remove separator that are in a quoted substring
+                                       // System.out
+                                       // .println("IN QUOTE, got a separator: [" + c + "]");
+                                       currStr.append(c);
+                               }
+                       } else if (c == quote) {
+                               if (inQuote && (i + 1) < arr.length && arr[i + 1] == quote) {
+                                       // case of double quote
+                                       currStr.append(quote);
+                                       i++;
+                               } else {// standard
+                                       inQuote = inQuote ? false : true;
+                               }
+                       } else {
+                               currStr.append(c);
+                       }
+               }
+
+               if (!inQuote) {
+                       tokens.add(currStr.toString());
+                       // System.out.println("# TOKEN: " + currStr);
+               }
+               // if (inQuote)
+               // throw new ArgeoException("Missing quote at the end of the line "
+               // + str + " (parsed: " + tokens + ")");
+               if (inQuote)
+                       return true;
+               else
+                       return false;
+               // return tokens;
+       }
+
+       public char getSeparator() {
+               return separator;
+       }
+
+       public synchronized void setSeparator(char separator) {
+               this.separator = separator;
+       }
+
+       public char getQuote() {
+               return quote;
+       }
+
+       public synchronized void setQuote(char quote) {
+               this.quote = quote;
+       }
+
+       public Boolean getNoHeader() {
+               return noHeader;
+       }
+
+       public synchronized void setNoHeader(Boolean noHeader) {
+               this.noHeader = noHeader;
+       }
+
+       public Boolean getStrictLineAsLongAsHeader() {
+               return strictLineAsLongAsHeader;
+       }
+
+       public synchronized void setStrictLineAsLongAsHeader(Boolean strictLineAsLongAsHeader) {
+               this.strictLineAsLongAsHeader = strictLineAsLongAsHeader;
+       }
+
+}
diff --git a/org.argeo.enterprise/src/org/argeo/util/CsvParserWithLinesAsMap.java b/org.argeo.enterprise/src/org/argeo/util/CsvParserWithLinesAsMap.java
new file mode 100644 (file)
index 0000000..8eb6e94
--- /dev/null
@@ -0,0 +1,36 @@
+package org.argeo.util;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * CSV parser allowing to process lines as maps whose keys are the header
+ * fields.
+ */
+public abstract class CsvParserWithLinesAsMap extends CsvParser {
+
+       /**
+        * Actually processes a line.
+        * 
+        * @param lineNumber the current line number, starts at 1 (the header, if header
+        *                   processing is enabled, the first lien otherwise)
+        * @param line       the parsed tokens as a map whose keys are the header fields
+        */
+       protected abstract void processLine(Integer lineNumber, Map<String, String> line);
+
+       protected final void processLine(Integer lineNumber, List<String> header, List<String> tokens) {
+               if (header == null)
+                       throw new IllegalArgumentException("Only CSV with header is supported");
+               Map<String, String> line = new HashMap<String, String>();
+               for (int i = 0; i < header.size(); i++) {
+                       String key = header.get(i);
+                       String value = null;
+                       if (i < tokens.size())
+                               value = tokens.get(i);
+                       line.put(key, value);
+               }
+               processLine(lineNumber, line);
+       }
+
+}
diff --git a/org.argeo.enterprise/src/org/argeo/util/CsvWriter.java b/org.argeo.enterprise/src/org/argeo/util/CsvWriter.java
new file mode 100644 (file)
index 0000000..c3b3a3a
--- /dev/null
@@ -0,0 +1,156 @@
+package org.argeo.util;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+import java.nio.charset.Charset;
+import java.util.Iterator;
+import java.util.List;
+
+/** Write in CSV format. */
+public class CsvWriter {
+       private final Writer out;
+
+       private char separator = ',';
+       private char quote = '\"';
+
+       /**
+        * Creates a CSV writer.
+        * 
+        * @param out the stream to write to. Caller is responsible for closing it.
+        * 
+        * @deprecated Use {@link #CsvWriter(OutputStream, Charset)} instead.
+        * 
+        */
+       @Deprecated
+       public CsvWriter(OutputStream out) {
+               this.out = new OutputStreamWriter(out);
+       }
+
+       /**
+        * Creates a CSV writer.
+        * 
+        * @param out      the stream to write to. Caller is responsible for closing it.
+        * @param encoding the encoding to use.
+        * 
+        * @deprecated Use {@link #CsvWriter(OutputStream, Charset)} instead.
+        */
+       @Deprecated
+       public CsvWriter(OutputStream out, String encoding) {
+               try {
+                       this.out = new OutputStreamWriter(out, encoding);
+               } catch (UnsupportedEncodingException e) {
+                       throw new IllegalArgumentException(e);
+               }
+       }
+
+       /**
+        * Creates a CSV writer.
+        * 
+        * @param out     the stream to write to. Caller is responsible for closing it.
+        * @param charset the charset to use
+        */
+       public CsvWriter(OutputStream out, Charset charset) {
+               this.out = new OutputStreamWriter(out, charset);
+       }
+
+       /**
+        * Creates a CSV writer.
+        * 
+        * @param out the stream to write to. Caller is responsible for closing it.
+        */
+       public CsvWriter(Writer writer) {
+               this.out = writer;
+       }
+
+       /**
+        * Write a CSV line. Also used to write a header if needed (this is transparent
+        * for the CSV writer): simply call it first, before writing the lines.
+        */
+       public void writeLine(List<?> tokens) {
+               try {
+                       Iterator<?> it = tokens.iterator();
+                       while (it.hasNext()) {
+                               Object obj = it.next();
+                               writeToken(obj != null ? obj.toString() : null);
+                               if (it.hasNext())
+                                       out.write(separator);
+                       }
+                       out.write('\n');
+                       out.flush();
+               } catch (IOException e) {
+                       throw new RuntimeException("Could not write " + tokens, e);
+               }
+       }
+
+       /**
+        * Write a CSV line. Also used to write a header if needed (this is transparent
+        * for the CSV writer): simply call it first, before writing the lines.
+        */
+       public void writeLine(Object[] tokens) {
+               try {
+                       for (int i = 0; i < tokens.length; i++) {
+                               if (tokens[i] == null) {
+                                       writeToken(null);
+                               } else {
+                                       writeToken(tokens[i].toString());
+                               }
+                               if (i != (tokens.length - 1))
+                                       out.write(separator);
+                       }
+                       out.write('\n');
+                       out.flush();
+               } catch (IOException e) {
+                       throw new RuntimeException("Could not write " + tokens, e);
+               }
+       }
+
+       protected void writeToken(String token) throws IOException {
+               if (token == null) {
+                       // TODO configure how to deal with null
+                       out.write("");
+                       return;
+               }
+               // +2 for possible quotes, another +2 assuming there would be an already
+               // quoted string where quotes needs to be duplicated
+               // another +2 for safety
+               // we don't want to increase buffer size while writing
+               StringBuffer buf = new StringBuffer(token.length() + 6);
+               char[] arr = token.toCharArray();
+               boolean shouldQuote = false;
+               for (char c : arr) {
+                       if (!shouldQuote) {
+                               if (c == separator)
+                                       shouldQuote = true;
+                               if (c == '\n')
+                                       shouldQuote = true;
+                       }
+
+                       if (c == quote) {
+                               shouldQuote = true;
+                               // duplicate quote
+                               buf.append(quote);
+                       }
+
+                       // generic case
+                       buf.append(c);
+               }
+
+               if (shouldQuote == true)
+                       out.write(quote);
+               out.write(buf.toString());
+               if (shouldQuote == true)
+                       out.write(quote);
+       }
+
+       public void setSeparator(char separator) {
+               this.separator = separator;
+       }
+
+       public void setQuote(char quote) {
+               this.quote = quote;
+       }
+
+}
diff --git a/org.argeo.enterprise/src/org/argeo/util/DictionaryKeys.java b/org.argeo.enterprise/src/org/argeo/util/DictionaryKeys.java
new file mode 100644 (file)
index 0000000..d17c86f
--- /dev/null
@@ -0,0 +1,42 @@
+package org.argeo.util;
+
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.Iterator;
+
+/**
+ * Access the keys of a {@link String}-keyed {@link Dictionary} (common throughout
+ * the OSGi APIs) as an {@link Iterable} so that they are easily usable in
+ * for-each loops.
+ */
+class DictionaryKeys implements Iterable<String> {
+       private final Dictionary<String, ?> dictionary;
+
+       public DictionaryKeys(Dictionary<String, ?> dictionary) {
+               this.dictionary = dictionary;
+       }
+
+       @Override
+       public Iterator<String> iterator() {
+               return new KeyIterator(dictionary.keys());
+       }
+
+       private static class KeyIterator implements Iterator<String> {
+               private final Enumeration<String> keys;
+
+               KeyIterator(Enumeration<String> keys) {
+                       this.keys = keys;
+               }
+
+               @Override
+               public boolean hasNext() {
+                       return keys.hasMoreElements();
+               }
+
+               @Override
+               public String next() {
+                       return keys.nextElement();
+               }
+
+       }
+}
diff --git a/org.argeo.enterprise/src/org/argeo/util/DigestUtils.java b/org.argeo.enterprise/src/org/argeo/util/DigestUtils.java
new file mode 100644 (file)
index 0000000..ce01800
--- /dev/null
@@ -0,0 +1,201 @@
+package org.argeo.util;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileChannel.MapMode;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/** Utilities around cryptographic digests */
+public class DigestUtils {
+       public final static String MD5 = "MD5";
+       public final static String SHA1 = "SHA1";
+       public final static String SHA256 = "SHA-256";
+       public final static String SHA512 = "SHA-512";
+
+       private static Boolean debug = false;
+       // TODO: make it configurable
+       private final static Integer byteBufferCapacity = 100 * 1024;// 100 KB
+
+       public static byte[] sha1(byte[] bytes) {
+               try {
+                       MessageDigest digest = MessageDigest.getInstance(SHA1);
+                       digest.update(bytes);
+                       byte[] checksum = digest.digest();
+                       return checksum;
+               } catch (NoSuchAlgorithmException e) {
+                       throw new IllegalArgumentException(e);
+               }
+       }
+
+       public static String digest(String algorithm, byte[] bytes) {
+               try {
+                       MessageDigest digest = MessageDigest.getInstance(algorithm);
+                       digest.update(bytes);
+                       byte[] checksum = digest.digest();
+                       String res = encodeHexString(checksum);
+                       return res;
+               } catch (NoSuchAlgorithmException e) {
+                       throw new IllegalArgumentException("Cannot digest with algorithm " + algorithm, e);
+               }
+       }
+
+       public static String digest(String algorithm, InputStream in) {
+               try {
+                       MessageDigest digest = MessageDigest.getInstance(algorithm);
+                       // ReadableByteChannel channel = Channels.newChannel(in);
+                       // ByteBuffer bb = ByteBuffer.allocateDirect(byteBufferCapacity);
+                       // while (channel.read(bb) > 0)
+                       // digest.update(bb);
+                       byte[] buffer = new byte[byteBufferCapacity];
+                       int read = 0;
+                       while ((read = in.read(buffer)) > 0) {
+                               digest.update(buffer, 0, read);
+                       }
+
+                       byte[] checksum = digest.digest();
+                       String res = encodeHexString(checksum);
+                       return res;
+               } catch (NoSuchAlgorithmException e) {
+                       throw new IllegalArgumentException("Cannot digest with algorithm " + algorithm, e);
+               } catch (IOException e) {
+                       throw new RuntimeException(e);
+               } finally {
+                       StreamUtils.closeQuietly(in);
+               }
+       }
+
+       public static String digest(String algorithm, File file) {
+               FileInputStream fis = null;
+               FileChannel fc = null;
+               try {
+                       fis = new FileInputStream(file);
+                       fc = fis.getChannel();
+
+                       // Get the file's size and then map it into memory
+                       int sz = (int) fc.size();
+                       ByteBuffer bb = fc.map(FileChannel.MapMode.READ_ONLY, 0, sz);
+                       return digest(algorithm, bb);
+               } catch (IOException e) {
+                       throw new IllegalArgumentException("Cannot digest " + file + " with algorithm " + algorithm, e);
+               } finally {
+                       StreamUtils.closeQuietly(fis);
+                       if (fc.isOpen())
+                               try {
+                                       fc.close();
+                               } catch (IOException e) {
+                                       // silent
+                               }
+               }
+       }
+
+       protected static String digest(String algorithm, ByteBuffer bb) {
+               long begin = System.currentTimeMillis();
+               try {
+                       MessageDigest digest = MessageDigest.getInstance(algorithm);
+                       digest.update(bb);
+                       byte[] checksum = digest.digest();
+                       String res = encodeHexString(checksum);
+                       long end = System.currentTimeMillis();
+                       if (debug)
+                               System.out.println((end - begin) + " ms / " + ((end - begin) / 1000) + " s");
+                       return res;
+               } catch (NoSuchAlgorithmException e) {
+                       throw new IllegalArgumentException("Cannot digest with algorithm " + algorithm, e);
+               }
+       }
+
+       public static String sha1hex(Path path) {
+               return digest(SHA1, path, byteBufferCapacity);
+       }
+
+       public static String digest(String algorithm, Path path, long bufferSize) {
+               byte[] digest = digestRaw(algorithm, path, bufferSize);
+               return encodeHexString(digest);
+       }
+
+       public static byte[] digestRaw(String algorithm, Path file, long bufferSize) {
+               long begin = System.currentTimeMillis();
+               try {
+                       MessageDigest md = MessageDigest.getInstance(algorithm);
+                       FileChannel fc = FileChannel.open(file);
+                       long fileSize = Files.size(file);
+                       if (fileSize <= bufferSize) {
+                               ByteBuffer bb = fc.map(MapMode.READ_ONLY, 0, fileSize);
+                               md.update(bb);
+                       } else {
+                               long lastCycle = (fileSize / bufferSize) - 1;
+                               long position = 0;
+                               for (int i = 0; i <= lastCycle; i++) {
+                                       ByteBuffer bb;
+                                       if (i != lastCycle) {
+                                               bb = fc.map(MapMode.READ_ONLY, position, bufferSize);
+                                               position = position + bufferSize;
+                                       } else {
+                                               bb = fc.map(MapMode.READ_ONLY, position, fileSize - position);
+                                               position = fileSize;
+                                       }
+                                       md.update(bb);
+                               }
+                       }
+                       long end = System.currentTimeMillis();
+                       if (debug)
+                               System.out.println((end - begin) + " ms / " + ((end - begin) / 1000) + " s");
+                       return md.digest();
+               } catch (NoSuchAlgorithmException e) {
+                       throw new IllegalArgumentException("Cannot digest " + file + "  with algorithm " + algorithm, e);
+               } catch (IOException e) {
+                       throw new RuntimeException("Cannot digest " + file + "  with algorithm " + algorithm, e);
+               }
+       }
+
+       public static void main(String[] args) {
+               File file;
+               if (args.length > 0)
+                       file = new File(args[0]);
+               else {
+                       System.err.println("Usage: <file> [<algorithm>]" + " (see http://java.sun.com/j2se/1.5.0/"
+                                       + "docs/guide/security/CryptoSpec.html#AppA)");
+                       return;
+               }
+
+               if (args.length > 1) {
+                       String algorithm = args[1];
+                       System.out.println(digest(algorithm, file));
+               } else {
+                       String algorithm = "MD5";
+                       System.out.println(algorithm + ": " + digest(algorithm, file));
+                       algorithm = "SHA";
+                       System.out.println(algorithm + ": " + digest(algorithm, file));
+                       System.out.println(algorithm + ": " + sha1hex(file.toPath()));
+                       algorithm = "SHA-256";
+                       System.out.println(algorithm + ": " + digest(algorithm, file));
+                       algorithm = "SHA-512";
+                       System.out.println(algorithm + ": " + digest(algorithm, file));
+               }
+       }
+
+       final private static char[] hexArray = "0123456789abcdef".toCharArray();
+
+       /**
+        * From
+        * http://stackoverflow.com/questions/9655181/how-to-convert-a-byte-array-to
+        * -a-hex-string-in-java
+        */
+       public static String encodeHexString(byte[] bytes) {
+               char[] hexChars = new char[bytes.length * 2];
+               for (int j = 0; j < bytes.length; j++) {
+                       int v = bytes[j] & 0xFF;
+                       hexChars[j * 2] = hexArray[v >>> 4];
+                       hexChars[j * 2 + 1] = hexArray[v & 0x0F];
+               }
+               return new String(hexChars);
+       }
+
+}
diff --git a/org.argeo.enterprise/src/org/argeo/util/DirH.java b/org.argeo.enterprise/src/org/argeo/util/DirH.java
new file mode 100644 (file)
index 0000000..b6d962f
--- /dev/null
@@ -0,0 +1,116 @@
+package org.argeo.util;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.charset.Charset;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/** Hashes the hashes of the files in a directory. */
+public class DirH {
+
+       private final static Charset charset = Charset.forName("UTF-16");
+       private final static long bufferSize = 200 * 1024 * 1024;
+       private final static String algorithm = "SHA";
+
+       private final static byte EOL = (byte) '\n';
+       private final static byte SPACE = (byte) ' ';
+
+       private final int hashSize;
+
+       private final byte[][] hashes;
+       private final byte[][] fileNames;
+       private final byte[] digest;
+       private final byte[] dirName;
+
+       /**
+        * @param dirName can be null or empty
+        */
+       private DirH(byte[][] hashes, byte[][] fileNames, byte[] dirName) {
+               if (hashes.length != fileNames.length)
+                       throw new IllegalArgumentException(hashes.length + " hashes and " + fileNames.length + " file names");
+               this.hashes = hashes;
+               this.fileNames = fileNames;
+               this.dirName = dirName == null ? new byte[0] : dirName;
+               if (hashes.length == 0) {// empty dir
+                       hashSize = 20;
+                       // FIXME what is the digest of an empty dir?
+                       digest = new byte[hashSize];
+                       Arrays.fill(digest, SPACE);
+                       return;
+               }
+               hashSize = hashes[0].length;
+               for (int i = 0; i < hashes.length; i++) {
+                       if (hashes[i].length != hashSize)
+                               throw new IllegalArgumentException(
+                                               "Hash size for " + new String(fileNames[i], charset) + " is " + hashes[i].length);
+               }
+
+               try {
+                       MessageDigest md = MessageDigest.getInstance(algorithm);
+                       for (int i = 0; i < hashes.length; i++) {
+                               md.update(this.hashes[i]);
+                               md.update(SPACE);
+                               md.update(this.fileNames[i]);
+                               md.update(EOL);
+                       }
+                       digest = md.digest();
+               } catch (NoSuchAlgorithmException e) {
+                       throw new IllegalArgumentException("Cannot digest", e);
+               }
+       }
+
+       public void print(PrintStream out) {
+               out.print(DigestUtils.encodeHexString(digest));
+               if (dirName.length > 0) {
+                       out.print(' ');
+                       out.print(new String(dirName, charset));
+               }
+               out.print('\n');
+               for (int i = 0; i < hashes.length; i++) {
+                       out.print(DigestUtils.encodeHexString(hashes[i]));
+                       out.print(' ');
+                       out.print(new String(fileNames[i], charset));
+                       out.print('\n');
+               }
+       }
+
+       public static DirH digest(Path dir) {
+               try (DirectoryStream<Path> files = Files.newDirectoryStream(dir)) {
+                       List<byte[]> hs = new ArrayList<byte[]>();
+                       List<String> fNames = new ArrayList<>();
+                       for (Path file : files) {
+                               if (!Files.isDirectory(file)) {
+                                       byte[] digest = DigestUtils.digestRaw(algorithm, file, bufferSize);
+                                       hs.add(digest);
+                                       fNames.add(file.getFileName().toString());
+                               }
+                       }
+
+                       byte[][] fileNames = new byte[fNames.size()][];
+                       for (int i = 0; i < fNames.size(); i++) {
+                               fileNames[i] = fNames.get(i).getBytes(charset);
+                       }
+                       byte[][] hashes = hs.toArray(new byte[hs.size()][]);
+                       return new DirH(hashes, fileNames, dir.toString().getBytes(charset));
+               } catch (IOException e) {
+                       throw new RuntimeException("Cannot digest " + dir, e);
+               }
+       }
+
+       public static void main(String[] args) {
+               try {
+                       DirH dirH = DirH.digest(Paths.get("/home/mbaudier/tmp/"));
+                       dirH.print(System.out);
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
+       }
+}
diff --git a/org.argeo.enterprise/src/org/argeo/util/LangUtils.java b/org.argeo.enterprise/src/org/argeo/util/LangUtils.java
new file mode 100644 (file)
index 0000000..7824d12
--- /dev/null
@@ -0,0 +1,253 @@
+package org.argeo.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Writer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+import java.time.temporal.Temporal;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.Properties;
+
+import javax.naming.InvalidNameException;
+import javax.naming.ldap.LdapName;
+
+/** Utilities around Java basic features. */
+public class LangUtils {
+       /*
+        * NON-API OSGi
+        */
+       /**
+        * Returns an array with the names of the provided classes. Useful when
+        * registering services with multiple interfaces in OSGi.
+        */
+       public static String[] names(Class<?>... clzz) {
+               String[] res = new String[clzz.length];
+               for (int i = 0; i < clzz.length; i++)
+                       res[i] = clzz[i].getName();
+               return res;
+       }
+
+       /*
+        * MAP
+        */
+       /**
+        * Creates a new {@link Dictionary} with one key-value pair. Key should not be
+        * null, but if the value is null, it returns an empty {@link Dictionary}.
+        */
+       public static Map<String, Object> map(String key, Object value) {
+               assert key != null;
+               HashMap<String, Object> props = new HashMap<>();
+               if (value != null)
+                       props.put(key, value);
+               return props;
+       }
+
+       /*
+        * DICTIONARY
+        */
+
+       /**
+        * Creates a new {@link Dictionary} with one key-value pair. Key should not be
+        * null, but if the value is null, it returns an empty {@link Dictionary}.
+        */
+       public static Dictionary<String, Object> dict(String key, Object value) {
+               assert key != null;
+               Hashtable<String, Object> props = new Hashtable<>();
+               if (value != null)
+                       props.put(key, value);
+               return props;
+       }
+
+       /** @deprecated Use {@link #dict(String, Object)} instead. */
+       @Deprecated
+       public static Dictionary<String, Object> dico(String key, Object value) {
+               return dict(key, value);
+       }
+
+       /** Converts a {@link Dictionary} to a {@link Map} of strings. */
+       public static Map<String, String> dictToStringMap(Dictionary<String, ?> properties) {
+               if (properties == null) {
+                       return null;
+               }
+               Map<String, String> res = new HashMap<>(properties.size());
+               Enumeration<String> keys = properties.keys();
+               while (keys.hasMoreElements()) {
+                       String key = keys.nextElement();
+                       res.put(key, properties.get(key).toString());
+               }
+               return res;
+       }
+
+       /**
+        * Get a string property from this map, expecting to find it, or
+        * <code>null</code> if not found.
+        */
+       public static String get(Map<String, ?> map, String key) {
+               Object res = map.get(key);
+               if (res == null)
+                       return null;
+               return res.toString();
+       }
+
+       /**
+        * Get a string property from this map, expecting to find it.
+        * 
+        * @throws IllegalArgumentException if the key was not found
+        */
+       public static String getNotNull(Map<String, ?> map, String key) {
+               Object res = map.get(key);
+               if (res == null)
+                       throw new IllegalArgumentException("Map " + map + " should contain key " + key);
+               return res.toString();
+       }
+
+       /**
+        * Wraps the keys of the provided {@link Dictionary} as an {@link Iterable}.
+        */
+       public static Iterable<String> keys(Dictionary<String, ?> props) {
+               assert props != null;
+               return new DictionaryKeys(props);
+       }
+
+       static String toJson(Dictionary<String, ?> props) {
+               return toJson(props, false);
+       }
+
+       static String toJson(Dictionary<String, ?> props, boolean pretty) {
+               StringBuilder sb = new StringBuilder();
+               sb.append('{');
+               if (pretty)
+                       sb.append('\n');
+               Enumeration<String> keys = props.keys();
+               while (keys.hasMoreElements()) {
+                       String key = keys.nextElement();
+                       if (pretty)
+                               sb.append(' ');
+                       sb.append('\"').append(key).append('\"');
+                       if (pretty)
+                               sb.append(" : ");
+                       else
+                               sb.append(':');
+                       sb.append('\"').append(props.get(key)).append('\"');
+                       if (keys.hasMoreElements())
+                               sb.append(", ");
+                       if (pretty)
+                               sb.append('\n');
+               }
+               sb.append('}');
+               return sb.toString();
+       }
+
+       static void storeAsProperties(Dictionary<String, Object> props, Path path) throws IOException {
+               if (props == null)
+                       throw new IllegalArgumentException("Props cannot be null");
+               Properties toStore = new Properties();
+               for (Enumeration<String> keys = props.keys(); keys.hasMoreElements();) {
+                       String key = keys.nextElement();
+                       toStore.setProperty(key, props.get(key).toString());
+               }
+               try (OutputStream out = Files.newOutputStream(path)) {
+                       toStore.store(out, null);
+               }
+       }
+
+       static void appendAsLdif(String dnBase, String dnKey, Dictionary<String, Object> props, Path path)
+                       throws IOException {
+               if (props == null)
+                       throw new IllegalArgumentException("Props cannot be null");
+               Object dnValue = props.get(dnKey);
+               String dnStr = dnKey + '=' + dnValue + ',' + dnBase;
+               LdapName dn;
+               try {
+                       dn = new LdapName(dnStr);
+               } catch (InvalidNameException e) {
+                       throw new IllegalArgumentException("Cannot interpret DN " + dnStr, e);
+               }
+               if (dnValue == null)
+                       throw new IllegalArgumentException("DN key " + dnKey + " must have a value");
+               try (Writer writer = Files.newBufferedWriter(path, StandardOpenOption.APPEND, StandardOpenOption.CREATE)) {
+                       writer.append("\ndn: ");
+                       writer.append(dn.toString());
+                       writer.append('\n');
+                       for (Enumeration<String> keys = props.keys(); keys.hasMoreElements();) {
+                               String key = keys.nextElement();
+                               Object value = props.get(key);
+                               writer.append(key);
+                               writer.append(": ");
+                               // FIXME deal with binary and multiple values
+                               writer.append(value.toString());
+                               writer.append('\n');
+                       }
+               }
+       }
+
+       static Dictionary<String, Object> loadFromProperties(Path path) throws IOException {
+               Properties toLoad = new Properties();
+               try (InputStream in = Files.newInputStream(path)) {
+                       toLoad.load(in);
+               }
+               Dictionary<String, Object> res = new Hashtable<String, Object>();
+               for (Object key : toLoad.keySet())
+                       res.put(key.toString(), toLoad.get(key));
+               return res;
+       }
+
+       /*
+        * EXCEPTIONS
+        */
+       /**
+        * Chain the messages of all causes (one per line, <b>starts with a line
+        * return</b>) without all the stack
+        */
+       public static String chainCausesMessages(Throwable t) {
+               StringBuffer buf = new StringBuffer();
+               chainCauseMessage(buf, t);
+               return buf.toString();
+       }
+
+       /** Recursive chaining of messages */
+       private static void chainCauseMessage(StringBuffer buf, Throwable t) {
+               buf.append('\n').append(' ').append(t.getClass().getCanonicalName()).append(": ").append(t.getMessage());
+               if (t.getCause() != null)
+                       chainCauseMessage(buf, t.getCause());
+       }
+
+       /*
+        * TIME
+        */
+       /** Formats time elapsed since start. */
+       public static String since(ZonedDateTime start) {
+               ZonedDateTime now = ZonedDateTime.now();
+               return duration(start, now);
+       }
+
+       /** Formats a duration. */
+       public static String duration(Temporal start, Temporal end) {
+               long count = ChronoUnit.DAYS.between(start, end);
+               if (count != 0)
+                       return count > 1 ? count + " days" : count + " day";
+               count = ChronoUnit.HOURS.between(start, end);
+               if (count != 0)
+                       return count > 1 ? count + " hours" : count + " hours";
+               count = ChronoUnit.MINUTES.between(start, end);
+               if (count != 0)
+                       return count > 1 ? count + " minutes" : count + " minute";
+               count = ChronoUnit.SECONDS.between(start, end);
+               return count > 1 ? count + " seconds" : count + " second";
+       }
+
+       /** Singleton constructor. */
+       private LangUtils() {
+
+       }
+
+}
diff --git a/org.argeo.enterprise/src/org/argeo/util/OS.java b/org.argeo.enterprise/src/org/argeo/util/OS.java
new file mode 100644 (file)
index 0000000..d8127b6
--- /dev/null
@@ -0,0 +1,56 @@
+package org.argeo.util;
+
+import java.io.File;
+import java.lang.management.ManagementFactory;
+
+/** When OS specific informations are needed. */
+public class OS {
+       public final static OS LOCAL = new OS();
+
+       private final String arch, name, version;
+
+       /** The OS of the running JVM */
+       protected OS() {
+               arch = System.getProperty("os.arch");
+               name = System.getProperty("os.name");
+               version = System.getProperty("os.version");
+       }
+
+       public String getArch() {
+               return arch;
+       }
+
+       public String getName() {
+               return name;
+       }
+
+       public String getVersion() {
+               return version;
+       }
+
+       public boolean isMSWindows() {
+               // only MS Windows would use such an horrendous separator...
+               return File.separatorChar == '\\';
+       }
+
+       public String[] getDefaultShellCommand() {
+               if (!isMSWindows())
+                       return new String[] { "/bin/sh", "-l", "-i" };
+               else
+                       return new String[] { "cmd.exe", "/C" };
+       }
+
+       public static Integer getJvmPid() {
+               /*
+                * This method works on most platforms (including Linux). Although when Java 9
+                * comes along, there is a better way: long pid =
+                * ProcessHandle.current().getPid();
+                *
+                * See:
+                * http://stackoverflow.com/questions/35842/how-can-a-java-program-get-its-own-
+                * process-id
+                */
+               String pidAndHost = ManagementFactory.getRuntimeMXBean().getName();
+               return Integer.parseInt(pidAndHost.substring(0, pidAndHost.indexOf('@')));
+       }
+}
diff --git a/org.argeo.enterprise/src/org/argeo/util/PasswordEncryption.java b/org.argeo.enterprise/src/org/argeo/util/PasswordEncryption.java
new file mode 100644 (file)
index 0000000..c95c787
--- /dev/null
@@ -0,0 +1,216 @@
+package org.argeo.util;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.CipherOutputStream;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+
+public class PasswordEncryption {
+       public final static Integer DEFAULT_ITERATION_COUNT = 1024;
+       /** Stronger with 256, but causes problem with Oracle JVM */
+       public final static Integer DEFAULT_SECRETE_KEY_LENGTH = 256;
+       public final static Integer DEFAULT_SECRETE_KEY_LENGTH_RESTRICTED = 128;
+       public final static String DEFAULT_SECRETE_KEY_FACTORY = "PBKDF2WithHmacSHA1";
+       public final static String DEFAULT_SECRETE_KEY_ENCRYPTION = "AES";
+       public final static String DEFAULT_CIPHER_NAME = "AES/CBC/PKCS5Padding";
+//     public final static String DEFAULT_CHARSET = "UTF-8";
+       public final static Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
+
+       private Integer iterationCount = DEFAULT_ITERATION_COUNT;
+       private Integer secreteKeyLength = DEFAULT_SECRETE_KEY_LENGTH;
+       private String secreteKeyFactoryName = DEFAULT_SECRETE_KEY_FACTORY;
+       private String secreteKeyEncryption = DEFAULT_SECRETE_KEY_ENCRYPTION;
+       private String cipherName = DEFAULT_CIPHER_NAME;
+
+       private static byte[] DEFAULT_SALT_8 = { (byte) 0xA9, (byte) 0x9B, (byte) 0xC8, (byte) 0x32, (byte) 0x56,
+                       (byte) 0x35, (byte) 0xE3, (byte) 0x03 };
+       private static byte[] DEFAULT_IV_16 = { (byte) 0xA9, (byte) 0x9B, (byte) 0xC8, (byte) 0x32, (byte) 0x56,
+                       (byte) 0x35, (byte) 0xE3, (byte) 0x03, (byte) 0xA9, (byte) 0x9B, (byte) 0xC8, (byte) 0x32, (byte) 0x56,
+                       (byte) 0x35, (byte) 0xE3, (byte) 0x03 };
+
+       private Key key;
+       private Cipher ecipher;
+       private Cipher dcipher;
+
+       private String securityProviderName = null;
+
+       /**
+        * This is up to the caller to clear the passed array. Neither copy of nor
+        * reference to the passed array is kept
+        */
+       public PasswordEncryption(char[] password) {
+               this(password, DEFAULT_SALT_8, DEFAULT_IV_16);
+       }
+
+       /**
+        * This is up to the caller to clear the passed array. Neither copies of nor
+        * references to the passed arrays are kept
+        */
+       public PasswordEncryption(char[] password, byte[] passwordSalt, byte[] initializationVector) {
+               try {
+                       initKeyAndCiphers(password, passwordSalt, initializationVector);
+               } catch (InvalidKeyException e) {
+                       Integer previousSecreteKeyLength = secreteKeyLength;
+                       secreteKeyLength = DEFAULT_SECRETE_KEY_LENGTH_RESTRICTED;
+                       System.err.println("'" + e.getMessage() + "', will use " + secreteKeyLength
+                                       + " secrete key length instead of " + previousSecreteKeyLength);
+                       try {
+                               initKeyAndCiphers(password, passwordSalt, initializationVector);
+                       } catch (GeneralSecurityException e1) {
+                               throw new IllegalStateException("Cannot get secret key (with restricted length)", e1);
+                       }
+               } catch (GeneralSecurityException e) {
+                       throw new IllegalStateException("Cannot get secret key", e);
+               }
+       }
+
+       protected void initKeyAndCiphers(char[] password, byte[] passwordSalt, byte[] initializationVector)
+                       throws GeneralSecurityException {
+               byte[] salt = new byte[8];
+               System.arraycopy(passwordSalt, 0, salt, 0, salt.length);
+               // for (int i = 0; i < password.length && i < salt.length; i++)
+               // salt[i] = (byte) password[i];
+               byte[] iv = new byte[16];
+               System.arraycopy(initializationVector, 0, iv, 0, iv.length);
+
+               SecretKeyFactory keyFac = SecretKeyFactory.getInstance(getSecretKeyFactoryName());
+               PBEKeySpec keySpec = new PBEKeySpec(password, salt, getIterationCount(), getKeyLength());
+               String secKeyEncryption = getSecretKeyEncryption();
+               if (secKeyEncryption != null) {
+                       SecretKey tmp = keyFac.generateSecret(keySpec);
+                       key = new SecretKeySpec(tmp.getEncoded(), getSecretKeyEncryption());
+               } else {
+                       key = keyFac.generateSecret(keySpec);
+               }
+               if (securityProviderName != null)
+                       ecipher = Cipher.getInstance(getCipherName(), securityProviderName);
+               else
+                       ecipher = Cipher.getInstance(getCipherName());
+               ecipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
+               dcipher = Cipher.getInstance(getCipherName());
+               dcipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
+       }
+
+       public void encrypt(InputStream decryptedIn, OutputStream encryptedOut) throws IOException {
+               try {
+                       CipherOutputStream out = new CipherOutputStream(encryptedOut, ecipher);
+                       StreamUtils.copy(decryptedIn, out);
+                       StreamUtils.closeQuietly(out);
+               } catch (IOException e) {
+                       throw e;
+               } finally {
+                       StreamUtils.closeQuietly(decryptedIn);
+               }
+       }
+
+       public void decrypt(InputStream encryptedIn, OutputStream decryptedOut) throws IOException {
+               try {
+                       CipherInputStream decryptedIn = new CipherInputStream(encryptedIn, dcipher);
+                       StreamUtils.copy(decryptedIn, decryptedOut);
+               } catch (IOException e) {
+                       throw e;
+               } finally {
+                       StreamUtils.closeQuietly(encryptedIn);
+               }
+       }
+
+       public byte[] encryptString(String str) {
+               ByteArrayOutputStream out = null;
+               ByteArrayInputStream in = null;
+               try {
+                       out = new ByteArrayOutputStream();
+                       in = new ByteArrayInputStream(str.getBytes(DEFAULT_CHARSET));
+                       encrypt(in, out);
+                       return out.toByteArray();
+               } catch (IOException e) {
+                       throw new RuntimeException(e);
+               } finally {
+                       StreamUtils.closeQuietly(out);
+               }
+       }
+
+       /** Closes the input stream */
+       public String decryptAsString(InputStream in) {
+               ByteArrayOutputStream out = null;
+               try {
+                       out = new ByteArrayOutputStream();
+                       decrypt(in, out);
+                       return new String(out.toByteArray(), DEFAULT_CHARSET);
+               } catch (IOException e) {
+                       throw new RuntimeException(e);
+               } finally {
+                       StreamUtils.closeQuietly(out);
+               }
+       }
+
+       protected Key getKey() {
+               return key;
+       }
+
+       protected Cipher getEcipher() {
+               return ecipher;
+       }
+
+       protected Cipher getDcipher() {
+               return dcipher;
+       }
+
+       protected Integer getIterationCount() {
+               return iterationCount;
+       }
+
+       protected Integer getKeyLength() {
+               return secreteKeyLength;
+       }
+
+       protected String getSecretKeyFactoryName() {
+               return secreteKeyFactoryName;
+       }
+
+       protected String getSecretKeyEncryption() {
+               return secreteKeyEncryption;
+       }
+
+       protected String getCipherName() {
+               return cipherName;
+       }
+
+       public void setIterationCount(Integer iterationCount) {
+               this.iterationCount = iterationCount;
+       }
+
+       public void setSecreteKeyLength(Integer keyLength) {
+               this.secreteKeyLength = keyLength;
+       }
+
+       public void setSecreteKeyFactoryName(String secreteKeyFactoryName) {
+               this.secreteKeyFactoryName = secreteKeyFactoryName;
+       }
+
+       public void setSecreteKeyEncryption(String secreteKeyEncryption) {
+               this.secreteKeyEncryption = secreteKeyEncryption;
+       }
+
+       public void setCipherName(String cipherName) {
+               this.cipherName = cipherName;
+       }
+
+       public void setSecurityProviderName(String securityProviderName) {
+               this.securityProviderName = securityProviderName;
+       }
+}
diff --git a/org.argeo.enterprise/src/org/argeo/util/ServiceChannel.java b/org.argeo.enterprise/src/org/argeo/util/ServiceChannel.java
new file mode 100644 (file)
index 0000000..7997384
--- /dev/null
@@ -0,0 +1,78 @@
+package org.argeo.util;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.AsynchronousByteChannel;
+import java.nio.channels.CompletionHandler;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.WritableByteChannel;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+/** An {@link AsynchronousByteChannel} based on an {@link ExecutorService}. */
+public class ServiceChannel implements AsynchronousByteChannel {
+       private final ReadableByteChannel in;
+       private final WritableByteChannel out;
+
+       private boolean open = true;
+
+       private ExecutorService executor;
+
+       public ServiceChannel(ReadableByteChannel in, WritableByteChannel out, ExecutorService executor) {
+               this.in = in;
+               this.out = out;
+               this.executor = executor;
+       }
+
+       @Override
+       public Future<Integer> read(ByteBuffer dst) {
+               return executor.submit(() -> in.read(dst));
+       }
+
+       @Override
+       public <A> void read(ByteBuffer dst, A attachment, CompletionHandler<Integer, ? super A> handler) {
+               try {
+                       Future<Integer> res = read(dst);
+                       handler.completed(res.get(), attachment);
+               } catch (Exception e) {
+                       handler.failed(e, attachment);
+               }
+       }
+
+       @Override
+       public Future<Integer> write(ByteBuffer src) {
+               return executor.submit(() -> out.write(src));
+       }
+
+       @Override
+       public <A> void write(ByteBuffer src, A attachment, CompletionHandler<Integer, ? super A> handler) {
+               try {
+                       Future<Integer> res = write(src);
+                       handler.completed(res.get(), attachment);
+               } catch (Exception e) {
+                       handler.failed(e, attachment);
+               }
+       }
+
+       @Override
+       public synchronized void close() throws IOException {
+               try {
+                       in.close();
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
+               try {
+                       out.close();
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
+               open = false;
+               notifyAll();
+       }
+
+       @Override
+       public synchronized boolean isOpen() {
+               return open;
+       }
+
+}
diff --git a/org.argeo.enterprise/src/org/argeo/util/StreamUtils.java b/org.argeo.enterprise/src/org/argeo/util/StreamUtils.java
new file mode 100644 (file)
index 0000000..6d7d940
--- /dev/null
@@ -0,0 +1,81 @@
+package org.argeo.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.Writer;
+
+/** Utilities to be used when Apache Commons IO is not available. */
+class StreamUtils {
+       private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
+
+       /*
+        * APACHE COMMONS IO (inspired)
+        */
+
+       /** @return the number of bytes */
+       public static Long copy(InputStream in, OutputStream out)
+                       throws IOException {
+               Long count = 0l;
+               byte[] buf = new byte[DEFAULT_BUFFER_SIZE];
+               while (true) {
+                       int length = in.read(buf);
+                       if (length < 0)
+                               break;
+                       out.write(buf, 0, length);
+                       count = count + length;
+               }
+               return count;
+       }
+
+       /** @return the number of chars */
+       public static Long copy(Reader in, Writer out) throws IOException {
+               Long count = 0l;
+               char[] buf = new char[DEFAULT_BUFFER_SIZE];
+               while (true) {
+                       int length = in.read(buf);
+                       if (length < 0)
+                               break;
+                       out.write(buf, 0, length);
+                       count = count + length;
+               }
+               return count;
+       }
+
+       public static void closeQuietly(InputStream in) {
+               if (in != null)
+                       try {
+                               in.close();
+                       } catch (Exception e) {
+                               //
+                       }
+       }
+
+       public static void closeQuietly(OutputStream out) {
+               if (out != null)
+                       try {
+                               out.close();
+                       } catch (Exception e) {
+                               //
+                       }
+       }
+
+       public static void closeQuietly(Reader in) {
+               if (in != null)
+                       try {
+                               in.close();
+                       } catch (Exception e) {
+                               //
+                       }
+       }
+
+       public static void closeQuietly(Writer out) {
+               if (out != null)
+                       try {
+                               out.close();
+                       } catch (Exception e) {
+                               //
+                       }
+       }
+}
diff --git a/org.argeo.enterprise/src/org/argeo/util/Tester.java b/org.argeo.enterprise/src/org/argeo/util/Tester.java
new file mode 100644 (file)
index 0000000..31a2be4
--- /dev/null
@@ -0,0 +1,126 @@
+package org.argeo.util;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/** A generic tester based on Java assertions and functional programming. */
+public class Tester {
+       private Map<String, TesterStatus> results = Collections.synchronizedSortedMap(new TreeMap<>());
+
+       private ClassLoader classLoader;
+
+       /** Use {@link Thread#getContextClassLoader()} by default. */
+       public Tester() {
+               this(Thread.currentThread().getContextClassLoader());
+       }
+
+       public Tester(ClassLoader classLoader) {
+               this.classLoader = classLoader;
+       }
+
+       public void execute(String className) {
+               Class<?> clss;
+               try {
+                       clss = classLoader.loadClass(className);
+                       boolean assertionsEnabled = clss.desiredAssertionStatus();
+                       if (!assertionsEnabled)
+                               throw new IllegalStateException("Test runner " + getClass().getName()
+                                               + " requires Java assertions to be enabled. Call the JVM with the -ea argument.");
+               } catch (Exception e1) {
+                       throw new IllegalArgumentException("Cannot initalise test for " + className, e1);
+
+               }
+               List<Method> methods = findMethods(clss);
+               if (methods.size() == 0)
+                       throw new IllegalArgumentException("No test method found in " + clss);
+               // TODO make order more predictable?
+               for (Method method : methods) {
+                       String uid = method.getDeclaringClass().getName() + "#" + method.getName();
+                       TesterStatus testStatus = new TesterStatus(uid);
+                       Object obj = null;
+                       try {
+                               beforeTest(uid, method);
+                               obj = clss.getDeclaredConstructor().newInstance();
+                               method.invoke(obj);
+                               testStatus.setPassed();
+                               afterTestPassed(uid, method, obj);
+                       } catch (Exception e) {
+                               testStatus.setFailed(e);
+                               afterTestFailed(uid, method, obj, e);
+                       } finally {
+                               results.put(uid, testStatus);
+                       }
+               }
+       }
+
+       protected void beforeTest(String uid, Method method) {
+               // System.out.println(uid + ": STARTING");
+       }
+
+       protected void afterTestPassed(String uid, Method method, Object obj) {
+               System.out.println(uid + ": PASSED");
+       }
+
+       protected void afterTestFailed(String uid, Method method, Object obj, Throwable e) {
+               System.out.println(uid + ": FAILED");
+               e.printStackTrace();
+       }
+
+       protected List<Method> findMethods(Class<?> clss) {
+               List<Method> methods = new ArrayList<Method>();
+//             Method call = getMethod(clss, "call");
+//             if (call != null)
+//                     methods.add(call);
+//
+               for (Method method : clss.getMethods()) {
+                       if (method.getName().startsWith("test")) {
+                               methods.add(method);
+                       }
+               }
+               return methods;
+       }
+
+       protected Method getMethod(Class<?> clss, String name, Class<?>... parameterTypes) {
+               try {
+                       return clss.getMethod(name, parameterTypes);
+               } catch (NoSuchMethodException e) {
+                       return null;
+               } catch (SecurityException e) {
+                       throw new IllegalStateException(e);
+               }
+       }
+
+       public static void main(String[] args) {
+               // deal with arguments
+               String className;
+               if (args.length < 1) {
+                       System.err.println(usage());
+                       System.exit(1);
+                       throw new IllegalArgumentException();
+               } else {
+                       className = args[0];
+               }
+
+               Tester test = new Tester();
+               try {
+                       test.execute(className);
+               } catch (Throwable e) {
+                       e.printStackTrace();
+               }
+
+               Map<String, TesterStatus> r = test.results;
+               for (String uid : r.keySet()) {
+                       TesterStatus testStatus = r.get(uid);
+                       System.out.println(testStatus);
+               }
+       }
+
+       public static String usage() {
+               return "java " + Tester.class.getName() + " [test class name]";
+
+       }
+}
diff --git a/org.argeo.enterprise/src/org/argeo/util/TesterStatus.java b/org.argeo.enterprise/src/org/argeo/util/TesterStatus.java
new file mode 100644 (file)
index 0000000..d1d14ed
--- /dev/null
@@ -0,0 +1,98 @@
+package org.argeo.util;
+
+import java.io.Serializable;
+
+/** The status of a test. */
+public class TesterStatus implements Serializable {
+       private static final long serialVersionUID = 6272975746885487000L;
+
+       private Boolean passed = null;
+       private final String uid;
+       private Throwable throwable = null;
+
+       public TesterStatus(String uid) {
+               this.uid = uid;
+       }
+
+       /** For cloning. */
+       public TesterStatus(String uid, Boolean passed, Throwable throwable) {
+               this(uid);
+               this.passed = passed;
+               this.throwable = throwable;
+       }
+
+       public synchronized Boolean isRunning() {
+               return passed == null;
+       }
+
+       public synchronized Boolean isPassed() {
+               assert passed != null;
+               return passed;
+       }
+
+       public synchronized Boolean isFailed() {
+               assert passed != null;
+               return !passed;
+       }
+
+       public synchronized void setPassed() {
+               setStatus(true);
+       }
+
+       public synchronized void setFailed() {
+               setStatus(false);
+       }
+
+       public synchronized void setFailed(Throwable throwable) {
+               setStatus(false);
+               setThrowable(throwable);
+       }
+
+       protected void setStatus(Boolean passed) {
+               if (this.passed != null)
+                       throw new IllegalStateException("Passed status of test " + uid + " is already set (to " + passed + ")");
+               this.passed = passed;
+       }
+
+       protected void setThrowable(Throwable throwable) {
+               if (this.throwable != null)
+                       throw new IllegalStateException("Throwable of test " + uid + " is already set (to " + passed + ")");
+               this.throwable = throwable;
+       }
+
+       public String getUid() {
+               return uid;
+       }
+
+       public Throwable getThrowable() {
+               return throwable;
+       }
+
+       @Override
+       protected Object clone() throws CloneNotSupportedException {
+               // TODO Auto-generated method stub
+               return super.clone();
+       }
+
+       @Override
+       public boolean equals(Object o) {
+               if (o instanceof TesterStatus) {
+                       TesterStatus other = (TesterStatus) o;
+                       // we don't check consistency for performance purposes
+                       // this equals() is supposed to be used in collections or for transfer
+                       return other.uid.equals(uid);
+               }
+               return false;
+       }
+
+       @Override
+       public int hashCode() {
+               return uid.hashCode();
+       }
+
+       @Override
+       public String toString() {
+               return uid + "\t" + (passed ? "passed" : "failed");
+       }
+
+}
diff --git a/org.argeo.enterprise/src/org/argeo/util/Throughput.java b/org.argeo.enterprise/src/org/argeo/util/Throughput.java
new file mode 100644 (file)
index 0000000..266ddbc
--- /dev/null
@@ -0,0 +1,82 @@
+package org.argeo.util;
+
+import java.text.NumberFormat;
+import java.text.ParseException;
+import java.util.Locale;
+
+/** A throughput, that is, a value per unit of time. */
+public class Throughput {
+       private final static NumberFormat usNumberFormat = NumberFormat.getInstance(Locale.US);
+
+       public enum Unit {
+               s, m, h, d
+       }
+
+       private final Double value;
+       private final Unit unit;
+
+       public Throughput(Double value, Unit unit) {
+               this.value = value;
+               this.unit = unit;
+       }
+
+       public Throughput(Long periodMs, Long count, Unit unit) {
+               if (unit.equals(Unit.s))
+                       value = ((double) count * 1000d) / periodMs;
+               else if (unit.equals(Unit.m))
+                       value = ((double) count * 60d * 1000d) / periodMs;
+               else if (unit.equals(Unit.h))
+                       value = ((double) count * 60d * 60d * 1000d) / periodMs;
+               else if (unit.equals(Unit.d))
+                       value = ((double) count * 24d * 60d * 60d * 1000d) / periodMs;
+               else
+                       throw new IllegalArgumentException("Unsupported unit " + unit);
+               this.unit = unit;
+       }
+
+       public Throughput(Double value, String unitStr) {
+               this(value, Unit.valueOf(unitStr));
+       }
+
+       public Throughput(String def) {
+               int index = def.indexOf('/');
+               if (def.length() < 3 || index <= 0 || index != def.length() - 2)
+                       throw new IllegalArgumentException(
+                                       def + " no a proper throughput definition" + " (should be <value>/<unit>, e.g. 3.54/s or 1500/h");
+               String valueStr = def.substring(0, index);
+               String unitStr = def.substring(index + 1);
+               try {
+                       this.value = usNumberFormat.parse(valueStr).doubleValue();
+               } catch (ParseException e) {
+                       throw new IllegalArgumentException("Cannot parse " + valueStr + " as a number.", e);
+               }
+               this.unit = Unit.valueOf(unitStr);
+       }
+
+       public Long asMsPeriod() {
+               if (unit.equals(Unit.s))
+                       return Math.round(1000d / value);
+               else if (unit.equals(Unit.m))
+                       return Math.round((60d * 1000d) / value);
+               else if (unit.equals(Unit.h))
+                       return Math.round((60d * 60d * 1000d) / value);
+               else if (unit.equals(Unit.d))
+                       return Math.round((24d * 60d * 60d * 1000d) / value);
+               else
+                       throw new IllegalArgumentException("Unsupported unit " + unit);
+       }
+
+       @Override
+       public String toString() {
+               return usNumberFormat.format(value) + '/' + unit;
+       }
+
+       public Double getValue() {
+               return value;
+       }
+
+       public Unit getUnit() {
+               return unit;
+       }
+
+}
diff --git a/org.argeo.enterprise/src/org/argeo/util/UuidUtils.java b/org.argeo.enterprise/src/org/argeo/util/UuidUtils.java
new file mode 100644 (file)
index 0000000..7584abc
--- /dev/null
@@ -0,0 +1,378 @@
+package org.argeo.util;
+
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.security.SecureRandom;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.util.BitSet;
+import java.util.Random;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Utilities to simplify and extends usage of {@link UUID}. Only the RFC 4122
+ * variant (also known as Leach–Salz variant) is supported.
+ */
+public class UuidUtils {
+       /** Nil UUID (00000000-0000-0000-0000-000000000000). */
+       public final static UUID NIL_UUID = UUID.fromString("00000000-0000-0000-0000-000000000000");
+       public final static LocalDateTime GREGORIAN_START = LocalDateTime.of(1582, 10, 15, 0, 0, 0);
+
+       private final static long MOST_SIG_VERSION1 = (1l << 12);
+       private final static long LEAST_SIG_RFC4122_VARIANT = (1l << 63);
+
+       private final static SecureRandom RANDOM;
+       private final static AtomicInteger CLOCK_SEQUENCE;
+       private final static byte[] HARDWARE_ADDRESS;
+       /** A start timestamp to which {@link System#nanoTime()}/100 can be added. */
+       private final static long START_TIMESTAMP;
+       static {
+               RANDOM = new SecureRandom();
+               CLOCK_SEQUENCE = new AtomicInteger(RANDOM.nextInt(16384));
+               HARDWARE_ADDRESS = getHardwareAddress();
+
+               long nowVm = System.nanoTime() / 100;
+               Duration duration = Duration.between(GREGORIAN_START, LocalDateTime.now(ZoneOffset.UTC));
+               START_TIMESTAMP = (duration.getSeconds() * 10000000 + duration.getNano() / 100) - nowVm;
+       }
+
+       private static byte[] getHardwareAddress() {
+               InetAddress localHost;
+               try {
+                       localHost = InetAddress.getLocalHost();
+                       try {
+                               NetworkInterface nic = NetworkInterface.getByInetAddress(localHost);
+                               return nic.getHardwareAddress();
+                       } catch (SocketException e) {
+                               return null;
+                       }
+               } catch (UnknownHostException e) {
+                       return null;
+               }
+
+       }
+
+       public static UUID timeUUIDwithRandomNode() {
+               long timestamp = START_TIMESTAMP + System.nanoTime() / 100;
+               return timeUUID(timestamp, RANDOM);
+       }
+
+       public static UUID timeUUID(long timestamp, Random random) {
+               byte[] node = new byte[6];
+               random.nextBytes(node);
+               node[0] = (byte) (node[0] | 1);
+               long clockSequence = CLOCK_SEQUENCE.incrementAndGet();
+               return timeUUID(timestamp, clockSequence, node);
+       }
+
+       public static UUID timeUUID() {
+               long timestamp = START_TIMESTAMP + System.nanoTime() / 100;
+               return timeUUID(timestamp);
+       }
+
+       public static UUID timeUUID(long timestamp) {
+               if (HARDWARE_ADDRESS == null)
+                       return timeUUID(timestamp, RANDOM);
+               long clockSequence = CLOCK_SEQUENCE.incrementAndGet();
+               return timeUUID(timestamp, clockSequence, HARDWARE_ADDRESS);
+       }
+
+       public static UUID timeUUID(long timestamp, NetworkInterface nic) {
+               byte[] node;
+               try {
+                       node = nic.getHardwareAddress();
+               } catch (SocketException e) {
+                       throw new IllegalStateException("Cannot get hardware address", e);
+               }
+               long clockSequence = CLOCK_SEQUENCE.incrementAndGet();
+               return timeUUID(timestamp, clockSequence, node);
+       }
+
+       public static UUID timeUUID(LocalDateTime time, long clockSequence, byte[] node) {
+               Duration duration = Duration.between(GREGORIAN_START, time);
+               // Number of 100 ns intervals in one second: 1000000000 / 100 = 10000000
+               long timestamp = duration.getSeconds() * 10000000 + duration.getNano() / 100;
+               return timeUUID(timestamp, clockSequence, node);
+       }
+
+       public static UUID timeUUID(long timestamp, long clockSequence, byte[] node) {
+               assert node.length >= 6;
+
+               long mostSig = MOST_SIG_VERSION1 // base for version 1 UUID
+                               | ((timestamp & 0xFFFFFFFFL) << 32) // time_low
+                               | (((timestamp >> 32) & 0xFFFFL) << 16) // time_mid
+                               | ((timestamp >> 48) & 0x0FFFL);// time_hi_and_version
+
+               long leastSig = LEAST_SIG_RFC4122_VARIANT // base for Leach–Salz UUID
+                               | (((clockSequence & 0x3F00) >> 8) << 56) // clk_seq_hi_res
+                               | ((clockSequence & 0xFF) << 48) // clk_seq_low
+                               | (node[0] & 0xFFL) //
+                               | ((node[1] & 0xFFL) << 8) //
+                               | ((node[2] & 0xFFL) << 16) //
+                               | ((node[3] & 0xFFL) << 24) //
+                               | ((node[4] & 0xFFL) << 32) //
+                               | ((node[5] & 0xFFL) << 40); //
+//             for (int i = 0; i < 6; i++) {
+//                     leastSig = leastSig | ((node[i] & 0xFFL) << (8 * i));
+//             }
+               UUID uuid = new UUID(mostSig, leastSig);
+
+               // tests
+               assert uuid.node() == BitSet.valueOf(node).toLongArray()[0];
+               assert uuid.timestamp() == timestamp;
+               assert uuid.clockSequence() == clockSequence;
+               assert uuid.version() == 1;
+               assert uuid.variant() == 2;
+               return uuid;
+       }
+
+       @Deprecated
+       public static UUID timeBasedUUID() {
+               return timeBasedUUID(LocalDateTime.now(ZoneOffset.UTC));
+       }
+
+       @Deprecated
+       public static UUID timeBasedRandomUUID() {
+               return timeBasedRandomUUID(LocalDateTime.now(ZoneOffset.UTC), RANDOM);
+       }
+
+       @Deprecated
+       public static UUID timeBasedUUID(LocalDateTime time) {
+               if (HARDWARE_ADDRESS == null)
+                       return timeBasedRandomUUID(time, RANDOM);
+               return timeBasedUUID(time, BitSet.valueOf(HARDWARE_ADDRESS));
+       }
+
+       @Deprecated
+       public static UUID timeBasedAddressUUID(LocalDateTime time, NetworkInterface nic) throws SocketException {
+               byte[] nodeBytes = nic.getHardwareAddress();
+               BitSet node = BitSet.valueOf(nodeBytes);
+               return timeBasedUUID(time, node);
+       }
+
+       @Deprecated
+       public static UUID timeBasedRandomUUID(LocalDateTime time, Random random) {
+               byte[] nodeBytes = new byte[6];
+               random.nextBytes(nodeBytes);
+               BitSet node = BitSet.valueOf(nodeBytes);
+               // set random marker
+               node.set(0, true);
+               return timeBasedUUID(time, node);
+       }
+
+       @Deprecated
+       public static UUID timeBasedUUID(LocalDateTime time, BitSet node) {
+               // most significant
+               Duration duration = Duration.between(GREGORIAN_START, time);
+
+               // Number of 100 ns intervals in one second: 1000000000 / 100 = 10000000
+               long timeNanos = duration.getSeconds() * 10000000 + duration.getNano() / 100;
+               BitSet timeBits = BitSet.valueOf(new long[] { timeNanos });
+               assert timeBits.length() <= 60;
+
+               int clockSequence;
+               synchronized (CLOCK_SEQUENCE) {
+                       clockSequence = CLOCK_SEQUENCE.incrementAndGet();
+                       if (clockSequence > 16384)
+                               CLOCK_SEQUENCE.set(0);
+               }
+               BitSet clockSequenceBits = BitSet.valueOf(new long[] { clockSequence });
+
+               // Build the UUID, bit by bit
+               // see https://tools.ietf.org/html/rfc4122#section-4.2.2
+               // time
+               BitSet time_low = new BitSet(32);
+               BitSet time_mid = new BitSet(16);
+               BitSet time_hi_and_version = new BitSet(16);
+
+               for (int i = 0; i < 60; i++) {
+                       if (i < 32)
+                               time_low.set(i, timeBits.get(i));
+                       else if (i < 48)
+                               time_mid.set(i - 32, timeBits.get(i));
+                       else
+                               time_hi_and_version.set(i - 48, timeBits.get(i));
+               }
+               // version
+               time_hi_and_version.set(12, true);
+               time_hi_and_version.set(13, false);
+               time_hi_and_version.set(14, false);
+               time_hi_and_version.set(15, false);
+
+               // clock sequence
+               BitSet clk_seq_hi_res = new BitSet(8);
+               BitSet clk_seq_low = new BitSet(8);
+               for (int i = 0; i < 8; i++) {
+                       clk_seq_low.set(i, clockSequenceBits.get(i));
+               }
+               for (int i = 8; i < 14; i++) {
+                       clk_seq_hi_res.set(i - 8, clockSequenceBits.get(i));
+               }
+               // variant
+               clk_seq_hi_res.set(6, false);
+               clk_seq_hi_res.set(7, true);
+
+//             String str = toHexString(time_low.toLongArray()[0]) + "-" + toHexString(time_mid.toLongArray()[0]) + "-"
+//                             + toHexString(time_hi_and_version.toLongArray()[0]) + "-"
+//                             + toHexString(clock_seq_hi_and_reserved.toLongArray()[0]) + toHexString(clock_seq_low.toLongArray()[0])
+//                             + "-" + toHexString(node.toLongArray()[0]);
+//             UUID uuid = UUID.fromString(str);
+
+               BitSet uuidBits = new BitSet(128);
+               for (int i = 0; i < 128; i++) {
+                       if (i < 48)
+                               uuidBits.set(i, node.get(i));
+                       else if (i < 56)
+                               uuidBits.set(i, clk_seq_low.get(i - 48));
+                       else if (i < 64)
+                               uuidBits.set(i, clk_seq_hi_res.get(i - 56));
+                       else if (i < 80)
+                               uuidBits.set(i, time_hi_and_version.get(i - 64));
+                       else if (i < 96)
+                               uuidBits.set(i, time_mid.get(i - 80));
+                       else
+                               uuidBits.set(i, time_low.get(i - 96));
+               }
+
+               long[] uuidLongs = uuidBits.toLongArray();
+               assert uuidLongs.length == 2;
+               UUID uuid = new UUID(uuidLongs[1], uuidLongs[0]);
+
+               // tests
+               assert uuid.node() == node.toLongArray()[0];
+               assert uuid.timestamp() == timeNanos;
+               assert uuid.clockSequence() == clockSequence;
+               assert uuid.version() == 1;
+               assert uuid.variant() == 2;
+               return uuid;
+       }
+
+       public static String toBinaryString(UUID uuid, int charsPerSegment, char separator) {
+               String binaryString = toBinaryString(uuid);
+               StringBuilder sb = new StringBuilder(128 + (128 / charsPerSegment));
+               for (int i = 0; i < binaryString.length(); i++) {
+                       if (i != 0 && i % charsPerSegment == 0)
+                               sb.append(separator);
+                       sb.append(binaryString.charAt(i));
+               }
+               return sb.toString();
+       }
+
+       public static String toBinaryString(UUID uuid) {
+               String most = zeroTo64Chars(Long.toBinaryString(uuid.getMostSignificantBits()));
+               String least = zeroTo64Chars(Long.toBinaryString(uuid.getLeastSignificantBits()));
+               String binaryString = most + least;
+               assert binaryString.length() == 128;
+               return binaryString;
+       }
+
+       private static String zeroTo64Chars(String str) {
+               assert str.length() <= 64;
+               if (str.length() < 64) {
+                       StringBuilder sb = new StringBuilder(64);
+                       for (int i = 0; i < 64 - str.length(); i++)
+                               sb.append('0');
+                       sb.append(str);
+                       return sb.toString();
+               } else
+                       return str;
+       }
+
+       public static String compactToStd(String compact) {
+               if (compact.length() != 32)
+                       throw new IllegalArgumentException(
+                                       "Compact UUID '" + compact + "' has length " + compact.length() + " and not 32.");
+               StringBuilder sb = new StringBuilder(36);
+               for (int i = 0; i < 32; i++) {
+                       if (i == 8 || i == 12 || i == 16 || i == 20)
+                               sb.append('-');
+                       sb.append(compact.charAt(i));
+               }
+               String std = sb.toString();
+               assert std.length() == 36;
+               assert UUID.fromString(std).toString().equals(std);
+               return std;
+       }
+
+       public static UUID compactToUuid(String compact) {
+               return UUID.fromString(compactToStd(compact));
+       }
+       
+       public static String firstBlock(UUID uuid) {
+               return uuid.toString().substring(0, 8);
+       }
+
+       public static boolean isRandom(UUID uuid) {
+               return uuid.version() == 4;
+       }
+
+       public static boolean isTimeBased(UUID uuid) {
+               return uuid.version() == 1;
+       }
+
+       public static boolean isTimeBasedRandom(UUID uuid) {
+               if (uuid.version() == 1) {
+                       BitSet node = BitSet.valueOf(new long[] { uuid.node() });
+                       return node.get(0);
+               } else
+                       return false;
+       }
+
+       public static boolean isNameBased(UUID uuid) {
+               return uuid.version() == 3 || uuid.version() == 5;
+       }
+
+       /** Singleton. */
+       private UuidUtils() {
+       }
+
+       public final static void main(String[] args) throws Exception {
+               UUID uuid;
+
+//             uuid = compactToUuid("996b1f5122de4b2f94e49168d32f22d1");
+//             System.out.println(uuid.toString() + ", isRandom=" + isRandom(uuid));
+
+               // warm up before measuring perf
+               for (int i = 0; i < 10; i++) {
+                       UUID.randomUUID();
+                       timeUUID();
+                       timeUUIDwithRandomNode();
+                       timeBasedRandomUUID();
+                       timeBasedUUID();
+               }
+
+               long begin;
+               long duration;
+
+               begin = System.nanoTime();
+               uuid = UUID.randomUUID();
+               duration = System.nanoTime() - begin;
+               System.out.println(uuid.toString() + " in " + duration + " ns, isRandom=" + isRandom(uuid));
+
+               begin = System.nanoTime();
+               uuid = timeUUID();
+               duration = System.nanoTime() - begin;
+               System.out.println(uuid.toString() + " in " + duration + " ns, isTimeBasedRandom=" + isTimeBasedRandom(uuid));
+
+               begin = System.nanoTime();
+               uuid = timeUUIDwithRandomNode();
+               duration = System.nanoTime() - begin;
+               System.out.println(uuid.toString() + " in " + duration + " ns, isTimeBasedRandom=" + isTimeBasedRandom(uuid));
+
+               begin = System.nanoTime();
+               uuid = timeBasedUUID();
+               duration = System.nanoTime() - begin;
+               System.out.println(uuid.toString() + " in " + duration + " ns, isTimeBasedRandom=" + isTimeBasedRandom(uuid));
+
+               begin = System.nanoTime();
+               uuid = timeBasedRandomUUID();
+               duration = System.nanoTime() - begin;
+               System.out.println(uuid.toString() + " in " + duration + " ns, isTimeBasedRandom=" + isTimeBasedRandom(uuid));
+//             System.out.println(toBinaryString(uuid, 8, ' '));
+//             System.out.println(toBinaryString(uuid, 16, '\n'));
+       }
+}
diff --git a/org.argeo.enterprise/src/org/argeo/util/package-info.java b/org.argeo.enterprise/src/org/argeo/util/package-info.java
new file mode 100644 (file)
index 0000000..4354b0a
--- /dev/null
@@ -0,0 +1,2 @@
+/** Generic Java utilities. */
+package org.argeo.util;
\ No newline at end of file