initial commit
Stefan Bund [Mon, 20 Sep 2010 11:56:32 +0000 (13:56 +0200)]
26 files changed:
.classpath [new file with mode: 0644]
.gitignore [new file with mode: 0644]
.project [new file with mode: 0644]
.springBeans [new file with mode: 0644]
build.xml [new file with mode: 0644]
src/de/j32/avmfritz/FritzBox.java [new file with mode: 0644]
src/de/j32/avmfritz/LoginXML.java [new file with mode: 0644]
src/de/j32/httplib/HttpGETRequest.java [new file with mode: 0644]
src/de/j32/httplib/HttpPOSTRequest.java [new file with mode: 0644]
src/de/j32/httplib/HttpRequest.java [new file with mode: 0644]
src/de/j32/httplib/HttpResponse.java [new file with mode: 0644]
src/de/j32/pimstuff/Main.java [new file with mode: 0644]
src/de/j32/pimstuff/conduit/Exporter.java [new file with mode: 0644]
src/de/j32/pimstuff/conduit/FritzAddressbookReader.java [new file with mode: 0644]
src/de/j32/pimstuff/conduit/FritzAddressbookWriter.java [new file with mode: 0644]
src/de/j32/pimstuff/conduit/Importer.java [new file with mode: 0644]
src/de/j32/pimstuff/data/Addressbook.java [new file with mode: 0644]
src/de/j32/pimstuff/data/Attribute.java [new file with mode: 0644]
src/de/j32/pimstuff/data/Entry.java [new file with mode: 0644]
src/de/j32/pimstuff/data/EntryConsumer.java [new file with mode: 0644]
src/de/j32/pimstuff/data/EntryProducer.java [new file with mode: 0644]
src/de/j32/util/Filter.java [new file with mode: 0644]
src/de/j32/util/FilteredIterator.java [new file with mode: 0644]
src/de/j32/util/SimpleXmlGenerator.java [new file with mode: 0644]
src/de/j32/util/Util.java [new file with mode: 0644]
src/de/j32/util/XmlUtil.java [new file with mode: 0644]

diff --git a/.classpath b/.classpath
new file mode 100644 (file)
index 0000000..0cbf9cd
--- /dev/null
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+       <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+       <classpathentry kind="src" path="src"/>
+       <classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..4ce8045
--- /dev/null
@@ -0,0 +1,2 @@
+/bin/
+/config.xml
diff --git a/.project b/.project
new file mode 100644 (file)
index 0000000..fab5f6e
--- /dev/null
+++ b/.project
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+       <name>PIMStuff</name>
+       <comment></comment>
+       <projects>
+       </projects>
+       <buildSpec>
+               <buildCommand>
+                       <name>org.eclipse.jdt.core.javabuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+               <buildCommand>
+                       <name>org.springframework.ide.eclipse.core.springbuilder</name>
+                       <arguments>
+                       </arguments>
+               </buildCommand>
+       </buildSpec>
+       <natures>
+               <nature>org.springframework.ide.eclipse.core.springnature</nature>
+               <nature>org.eclipse.jdt.core.javanature</nature>
+       </natures>
+</projectDescription>
diff --git a/.springBeans b/.springBeans
new file mode 100644 (file)
index 0000000..4dff647
--- /dev/null
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beansProjectDescription>
+       <version>1</version>
+       <pluginVersion><![CDATA[2.3.2.201003220227-RELEASE]]></pluginVersion>
+       <configSuffixes>
+               <configSuffix><![CDATA[xml]]></configSuffix>
+       </configSuffixes>
+       <enableImports><![CDATA[false]]></enableImports>
+       <configs>
+       </configs>
+       <configSets>
+       </configSets>
+</beansProjectDescription>
diff --git a/build.xml b/build.xml
new file mode 100644 (file)
index 0000000..a0c6ac6
--- /dev/null
+++ b/build.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project name="PIMStuff" default="build" basedir=".">
+
+       <property name="src" location="src"/>
+       <property name="build" location="bin"/>
+       <property name="main" value="de.j32.pimstuff.Main"/>
+
+       <target name="makedirs">
+               <mkdir dir="${build}"/>
+       </target>
+       
+       <target name="build" depends="makedirs"
+                       description="Compile project to ${build} directory">
+               <javac srcdir="${src}" destdir="${build}"/>
+       </target>
+       
+       <target name="clean"
+                       description="Clean up ${build} directory">
+               <delete><fileset dir="${build}"/></delete>
+       </target>
+
+       <target name="run" depends="build"
+                       description="Start main class">
+               <java fork="true" classpath="${build}" classname="${main}"/>
+       </target>
+
+</project>
diff --git a/src/de/j32/avmfritz/FritzBox.java b/src/de/j32/avmfritz/FritzBox.java
new file mode 100644 (file)
index 0000000..de8e63f
--- /dev/null
@@ -0,0 +1,102 @@
+package de.j32.avmfritz;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.xml.sax.SAXException;
+
+import de.j32.httplib.HttpGETRequest;
+import de.j32.httplib.HttpPOSTRequest;
+import de.j32.httplib.HttpRequest;
+import de.j32.httplib.HttpResponse;
+import de.j32.util.Util;
+
+public class FritzBox
+{
+       String url_;
+       String sid_;
+       
+       public FritzBox(String password, String url)
+               throws SAXException, IOException
+       {
+               url_ = url;
+               
+               HttpResponse response = null;
+               try {
+                       response = httpGet("cgi-bin/webcm")
+                               .addParameter("getpage", "../html/login_sid.xml")
+                               .execute();
+                       LoginXML loginxml = new LoginXML(response);
+                       response.close();
+                       response = null;
+
+                       if (loginxml.iswriteaccess()) {
+                               sid_ = loginxml.sid();
+                               return;
+                       }
+
+                       response = httpPost("cgi-bin/webcm")
+                               .addParameter("getpage", "../html/login_sid.xml")
+                               .addParameter("var:lang", "de")
+                               .addParameter("login:command/response",
+                                       loginxml.response(password)).execute();
+                       loginxml = new LoginXML(response);
+                       response.close();
+                       response = null;
+
+                       if (!loginxml.iswriteaccess())
+                               throw new RuntimeException("FritzBox login failed");
+
+                       sid_ = loginxml.sid();
+               }
+               finally {
+                       Util.nothrowClose(response);
+               }
+               
+       }
+       
+       public InputStream exportAddressbook()
+               throws IOException
+       {
+               return httpPostMultipart("cgi-bin/firmwarecfg")
+                                       .addParameter("sid", sid_)
+                                       .addParameter("PhonebookId", "0")
+                                       .addParameter("PhonebookExportName", "Telefonbuch")
+                                       .addParameter("PhonebookExport", "").execute();
+       }
+       
+       public OutputStream importAddressbook()
+               throws IOException
+       {
+               return new ByteArrayOutputStream() {
+                       public void close()
+                               throws IOException
+                       {
+                               System.out.println("sending to fritzbox");
+                               httpPostMultipart("cgi-bin/firmwarecfg")
+                                       .addParameter("sid", sid_)
+                                       .addParameter("PhonebookId", "0")
+                                       .addParameter("PhonebookImportFile", toByteArray(), "iso-8859-1")
+                                               .execute()
+                                               .close();
+                       }
+               };
+       }
+       
+       HttpRequest httpGet(String path)
+       {
+               return new HttpGETRequest(url_ + "/" + path);
+       }
+       
+       HttpRequest httpPost(String path)
+       {
+               return new HttpPOSTRequest(url_ + "/" + path);
+       }
+       
+       HttpRequest httpPostMultipart(String path)
+       {
+               return new HttpPOSTRequest(url_ + "/" + path).setMultipart(true);
+       }
+}
diff --git a/src/de/j32/avmfritz/LoginXML.java b/src/de/j32/avmfritz/LoginXML.java
new file mode 100644 (file)
index 0000000..c50f80a
--- /dev/null
@@ -0,0 +1,75 @@
+package de.j32.avmfritz;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import javax.xml.bind.annotation.adapters.HexBinaryAdapter;
+
+import org.w3c.dom.Document;
+import org.xml.sax.SAXException;
+
+import de.j32.util.Util;
+import de.j32.util.XmlUtil;
+
+public class LoginXML
+{
+       Document xml_;
+       
+       public LoginXML(InputStream is) 
+               throws SAXException, IOException
+       {
+               xml_ = XmlUtil.parse(is);
+       }
+       
+       public boolean iswriteaccess()
+               throws SAXException
+       {
+               try {
+                       return xml_.getElementsByTagName("iswriteaccess").item(0).getTextContent().equals("1"); 
+               }
+               catch (NullPointerException e) {
+                       throw new SAXException();
+               }
+       }
+       
+       public String sid() 
+               throws SAXException
+       {
+               try {
+                       return Util.nonnull(xml_.getElementsByTagName("SID").item(0).getTextContent());
+               }
+               catch (NullPointerException e) {
+                       throw new SAXException();
+               }
+       }
+       
+       public String challenge()
+               throws SAXException
+       {
+               try {
+                       return Util.nonnull(xml_.getElementsByTagName("Challenge").item(0).getTextContent());
+               }
+               catch (NullPointerException e) {
+                       throw new SAXException();
+               }
+       }
+       
+       public String response(String password)
+               throws SAXException
+       {
+               try {
+                       String c = challenge();
+                       MessageDigest md = MessageDigest.getInstance("MD5");
+                       md.update((c + "-" + password).getBytes("UTF-16LE"));
+                       return c + "-" + new HexBinaryAdapter().marshal(md.digest()).toLowerCase();
+               } catch (NoSuchAlgorithmException e) {
+                       // Is it at all feasible for this to happen ?
+                       throw new RuntimeException("missing MD5 implementation");
+               } catch (UnsupportedEncodingException e) {
+                       throw new RuntimeException("missing UTF-16LE encoding");
+               }
+       }
+}
diff --git a/src/de/j32/httplib/HttpGETRequest.java b/src/de/j32/httplib/HttpGETRequest.java
new file mode 100644 (file)
index 0000000..3929df6
--- /dev/null
@@ -0,0 +1,18 @@
+package de.j32.httplib;
+
+public class HttpGETRequest
+       extends HttpRequest
+{
+       public HttpGETRequest(String url)
+       {
+               super(url,"GET");
+       }
+
+       @Override
+       public HttpGETRequest addParameter(String name, byte[] value, String encoding)
+       {
+               appendParameter(query(), query().length() == 0, name, value);
+               return this;
+       }
+
+}
diff --git a/src/de/j32/httplib/HttpPOSTRequest.java b/src/de/j32/httplib/HttpPOSTRequest.java
new file mode 100644 (file)
index 0000000..f59134e
--- /dev/null
@@ -0,0 +1,70 @@
+package de.j32.httplib;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+
+public class HttpPOSTRequest 
+       extends HttpRequest
+{
+       boolean multipart_ = false;
+       static final String separator_ = 
+               "----MultiPartFormData--MultiPartFormData--MultiPartFormData----";
+       
+       public HttpPOSTRequest(String url)
+       {
+               super(url, "POST");
+               setContentType("application/x-www-form-urlencoded; charset=utf-8");
+       }
+       
+       public HttpPOSTRequest setMultipart(boolean flag)
+       {
+               multipart_ = flag;
+               if (multipart_) {
+                       setContentType("multipart/form-data; boundary=" + separator_);
+                       try {
+                               OutputStream body = body();
+                               body.write("--".getBytes());
+                               body.write(separator_.getBytes());
+                               body.write("\r\n".getBytes());
+                       }
+                       catch (IOException e) {
+                               throw new AssertionError("ByteArrayOutputStream throwing IOExcpetion");                 
+                       }
+               }
+               return this;
+       }
+       
+       @Override
+       public HttpRequest addParameter(String name, byte[] value, String encoding)
+       {
+               try {
+                       if (multipart_) {
+                               OutputStream body = body();
+                               body.write("Content-Disposition: form-data; name=\"".getBytes());
+                               body.write(name.getBytes());
+                               body.write("\"\r\n".getBytes());
+                               body.write("Content-Type: text/plain; charset=".getBytes());
+                               body.write(encoding.getBytes());
+                               body.write("\r\n".getBytes());
+                               body.write(("Content-Length: " + value.length).getBytes());
+                               body.write("\r\n\r\n".getBytes());
+                               body.write(value);
+                               body.write("\r\n--".getBytes());
+                               body.write(separator_.getBytes());
+                               body.write("\r\n".getBytes());
+                       }
+                       else {
+                               // Encoding not really relevant here since url-encoding is plain ASCII
+                               Writer writer = new OutputStreamWriter(body(),"ascii");
+                               appendParameter(writer, body().size() == 0, name, value);
+                               writer.flush();
+                       }
+               }
+               catch (IOException e) {
+                       throw new AssertionError("ByteArrayOutputStream throwing IOExcpetion");                 
+               }
+               return this;
+       }
+}
diff --git a/src/de/j32/httplib/HttpRequest.java b/src/de/j32/httplib/HttpRequest.java
new file mode 100644 (file)
index 0000000..7f9c32e
--- /dev/null
@@ -0,0 +1,102 @@
+package de.j32.httplib;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLEncoder;
+
+public abstract class HttpRequest
+{
+       StringBuffer url_;
+       StringBuffer query_ = new StringBuffer();
+       String method_;
+       ByteArrayOutputStream body_ = new ByteArrayOutputStream();
+       String contentType_;
+       
+       public HttpRequest(String url, String method)
+       {
+               url_ = new StringBuffer(url);
+               method_ = method;
+       }
+
+       protected StringBuffer query()
+       {
+               return query_;
+       }
+       
+       protected ByteArrayOutputStream body()
+       {
+               return body_;
+       }
+       
+       protected void setContentType(String c)
+       {
+               contentType_ = c;
+       }
+       
+       protected static void appendParameter(Appendable buffer, boolean first, String name, byte[] value)
+       {
+               try {
+                       if (! first)
+                               buffer.append("&");
+                       buffer.append(URLEncoder.encode(name,"utf-8"));
+                       buffer.append("=");
+                       // We really would need a URLEncoder for byte[] (pre-encoded or raw date)
+                       buffer.append(URLEncoder.encode(new String(value,"ascii"),"ascii"));
+               }
+               catch (UnsupportedEncodingException e)
+               {
+                       throw new AssertionError("Missing encoding");
+               }
+               catch (IOException e)
+               {
+                       throw new AssertionError("IOException on buffer-based Appendable");
+               }
+       }
+       
+       public HttpResponse execute()
+               throws MalformedURLException, IOException
+       {
+               if (query_.length() > 0)
+                       url_.append("?");
+                       url_.append(query_);
+               System.out.println("Opening: " + url_);
+               HttpURLConnection connection = 
+                       (HttpURLConnection) new URL(new String(url_)).openConnection();
+               System.out.println("Request method: " + method_);
+               connection.setRequestMethod(method_);
+               if (contentType_ != null) {
+                       System.out.println("Content-Type: " + contentType_);
+                       connection.setRequestProperty("Content-Type", contentType_);
+                       connection.setDoOutput(true);
+                       System.out.println("Body size: " + body_.size());
+                       connection.setFixedLengthStreamingMode(body_.size());
+                       System.out.println("Body:");
+                       System.out.println(body_.toString());
+                       connection.getOutputStream().write(body_.toByteArray());
+                       System.out.println("done...");
+               }
+               if (connection.getResponseCode() != HttpURLConnection.HTTP_OK)
+                       throw new IOException("HTTP request failed: " 
+                                       + connection.getResponseCode() + " " + connection.getResponseMessage());
+               System.out.println("response ok");
+               return new HttpResponse(connection);
+       }
+       
+       public HttpRequest addParameter(String name, String value)
+               throws UnsupportedEncodingException
+       {
+               return addParameter(name, value, "utf-8");
+       }
+       
+       public HttpRequest addParameter(String name, String value, String encoding)
+               throws UnsupportedEncodingException
+       {
+               return addParameter(name, value.getBytes(encoding), encoding);
+       }
+       
+       abstract public HttpRequest addParameter(String name, byte[] value, String encoding);
+}
diff --git a/src/de/j32/httplib/HttpResponse.java b/src/de/j32/httplib/HttpResponse.java
new file mode 100644 (file)
index 0000000..c35adb4
--- /dev/null
@@ -0,0 +1,34 @@
+package de.j32.httplib;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+
+public class HttpResponse
+       extends InputStream
+{
+       HttpURLConnection connection_;
+       InputStream stream_;
+       
+       public HttpResponse(HttpURLConnection connection)
+               throws IOException
+       {
+               connection_ = connection;
+               stream_ = connection_.getInputStream();
+       }
+       
+       @Override
+       public int read()
+               throws IOException
+       {
+               return stream_.read();
+       }
+
+       @Override
+       public void close()
+               throws IOException
+       {
+               stream_.close();
+       }
+       
+}
diff --git a/src/de/j32/pimstuff/Main.java b/src/de/j32/pimstuff/Main.java
new file mode 100644 (file)
index 0000000..173df38
--- /dev/null
@@ -0,0 +1,96 @@
+package de.j32.pimstuff;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Properties;
+
+import org.xml.sax.SAXException;
+
+import de.j32.avmfritz.FritzBox;
+import de.j32.pimstuff.conduit.FritzAddressbookReader;
+import de.j32.pimstuff.conduit.FritzAddressbookWriter;
+import de.j32.pimstuff.data.Addressbook;
+import de.j32.util.Util;
+
+public class Main {
+
+       public static void main(String[] args)
+       {       
+               try {
+                       
+                       System.out.println("Launching pimstuff ...");
+                       Properties config = new Properties();
+                       
+                       try {
+                               config.loadFromXML(new FileInputStream("config.xml"));
+                       }
+                       catch (FileNotFoundException e) {
+                               config.setProperty("password", "password");
+                               config.setProperty("url", "http://fritz.box");
+                               
+                               config.storeToXML(new FileOutputStream("config.xml"), null, "UTF-8");
+                       }
+                       
+                       String pw, url;
+
+                       try {
+                               pw = Util.nonnull(config.getProperty("password"));
+                               url = Util.nonnull(config.getProperty("url"));
+                       }
+                       catch (NullPointerException e) {
+                               throw new RuntimeException("Missing configuration parameter");
+                       }
+
+                       Addressbook ab = new Addressbook();
+                       FritzBox fb = new FritzBox(pw, url);
+
+                       System.out.println("loading ...");
+                       // Load Addressbook from FritzBox
+                       InputStream is = null;
+                       FritzAddressbookReader reader = null;
+                       try {
+                               ab = new Addressbook();
+                               is = fb.exportAddressbook();
+                               reader = new FritzAddressbookReader(is);
+                               reader.sendTo(ab);
+                               is.close();
+                               is = null;
+                               reader.close();
+                               reader = null;
+                       }
+                       finally {
+                               Util.nothrowClose(is);
+                       }
+                       
+                       System.out.println("saving ...");
+                       // Save Addressbook back to FritzBox
+                       FritzAddressbookWriter writer = null;
+                       OutputStream os = null;
+                       try {
+                               os = fb.importAddressbook();
+                               writer = new FritzAddressbookWriter(os);
+                               ab.sendTo(writer);
+                               writer.close();
+                               writer = null;
+                               os.close();
+                               os = null;
+                       }
+                       finally {
+                               Util.nothrowClose(writer);
+                               Util.nothrowClose(os);
+                       }
+
+               } catch (SAXException e) {
+                       // TODO Auto-generated catch block
+                       e.printStackTrace();
+               } catch (IOException e) {
+                       // TODO Auto-generated catch block
+                       e.printStackTrace();
+               }
+       }
+
+}
diff --git a/src/de/j32/pimstuff/conduit/Exporter.java b/src/de/j32/pimstuff/conduit/Exporter.java
new file mode 100644 (file)
index 0000000..9fc72a0
--- /dev/null
@@ -0,0 +1,9 @@
+package de.j32.pimstuff.conduit;
+
+import java.io.Closeable;
+
+import de.j32.pimstuff.data.EntryConsumer;
+
+public interface Exporter
+       extends Closeable, EntryConsumer
+{}
diff --git a/src/de/j32/pimstuff/conduit/FritzAddressbookReader.java b/src/de/j32/pimstuff/conduit/FritzAddressbookReader.java
new file mode 100644 (file)
index 0000000..a00d294
--- /dev/null
@@ -0,0 +1,67 @@
+package de.j32.pimstuff.conduit;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.xml.sax.SAXException;
+
+import de.j32.pimstuff.data.Entry;
+import de.j32.pimstuff.data.EntryConsumer;
+import de.j32.util.XmlUtil;
+
+public class FritzAddressbookReader
+       implements Importer
+{
+       Document xml_;
+       
+       public FritzAddressbookReader(InputStream is)
+               throws SAXException, IOException
+       {
+               xml_ = XmlUtil.parse(is);
+       }
+       
+       @Override
+       public void sendTo(EntryConsumer consumer)
+       {
+               for (Element node : XmlUtil.iterateElements(xml_.getElementsByTagName("contact"))) {
+                       /* subnodes:
+                        *   category (unused, always 0)
+                        *   person/realName
+                        *   person/imageURL
+                        *   telephony/number (@prio, @type, @vanity)
+                        *   services/email
+                        *   mod_time
+                        */
+                       Entry entry = new Entry();
+
+                       try {
+                               entry.name(node.getElementsByTagName("realName").item(0).getTextContent());
+
+                               for (Element phone : XmlUtil.iterateElements(node.getElementsByTagName("number"))) {
+                                       entry.attribute("phone", phone.getAttribute("type"), phone.getTextContent());
+                               }
+                               
+                               try {
+                                       entry.attribute("email", "", 
+                                                       node.getElementsByTagName("email").item(0).getTextContent());
+                               }
+                               catch (NullPointerException e) {} // ignore missing optional email
+                       }
+                       catch (NullPointerException e) {
+                               // Ignore incomplete entries
+                               entry = null;
+                       }
+
+                       if (entry != null)
+                               consumer.consume(entry);
+               }
+       }
+
+       @Override
+       public void close()
+               throws IOException
+       {}
+       
+}
diff --git a/src/de/j32/pimstuff/conduit/FritzAddressbookWriter.java b/src/de/j32/pimstuff/conduit/FritzAddressbookWriter.java
new file mode 100644 (file)
index 0000000..361caf8
--- /dev/null
@@ -0,0 +1,94 @@
+package de.j32.pimstuff.conduit;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+
+import org.xml.sax.SAXException;
+
+import de.j32.pimstuff.data.Attribute;
+import de.j32.pimstuff.data.Entry;
+import de.j32.util.SimpleXmlGenerator;
+import de.j32.util.Util;
+
+public class FritzAddressbookWriter
+       implements Exporter
+{
+       OutputStream os_;
+       SimpleXmlGenerator gen_;
+       
+       public FritzAddressbookWriter(OutputStream os)
+       {
+               try {
+                       os_ = os;
+                       gen_ = new SimpleXmlGenerator(os_, "iso-8859-1");
+                       gen_.startDocument(); gen_.nl();
+                       gen_.start("phonebooks"); gen_.nl();
+                       gen_.start("phonebook"); gen_.nl();
+               }
+               catch (SAXException e)
+               {
+                       throw new AssertionError("Invalid XML/SAX document generated.");
+               } catch (UnsupportedEncodingException e) {
+                       throw new AssertionError("Unsopported encoding iso-8859-1 ??");
+               }
+       }
+
+       @Override
+       public void consume(Entry entry)
+       {
+               try {
+                       gen_.start("contact"); gen_.nl();
+
+                               gen_.start("category"); gen_.text("0"); gen_.end(); gen_.nl();
+                       
+                               gen_.start("person"); gen_.nl();
+                                       gen_.start("realName"); gen_.text(entry.name()); gen_.end(); gen_.nl();
+                                       gen_.empty("imageURL"); gen_.nl();
+                               gen_.end(); gen_.nl();
+                               
+                               gen_.start("telephony"); gen_.nl();
+                                       for (Attribute number : entry.attributes("phone")) {
+                                               gen_.start("number",
+                                                               gen_.attribute("prio",   number.index > 0 ? "0" : "1")
+                                                                       .attribute("type",   number.rel)
+                                                                       .attribute("vanity", ""));
+                                                   gen_.text(number.value);    gen_.end(); gen_.nl();
+                                       }
+                               gen_.end(); gen_.nl();
+                               
+                               Attribute email  = Util.first(entry.attributes("email"));
+                               if (email != null) {
+                                       gen_.start("services"); gen_.nl();
+                                               gen_.start("email", gen_.attribute("classifier","private"));
+                                                       gen_.text(email.value); gen_.end(); gen_.nl();
+                                       gen_.end(); gen_.nl();
+                               }
+                               else {
+                                       gen_.empty("services"); gen_.nl();
+                               }
+                               
+                       gen_.end(); gen_.nl();
+               }
+               catch (SAXException e)
+               {
+                       throw new AssertionError("Invalid XML/SAX document generated.");
+               }
+       }
+
+       @Override
+       public void close()
+               throws IOException
+       {
+               try {
+                       gen_.end(); gen_.nl();
+                       gen_.end();
+                       gen_.endDocument();
+               }
+               catch (SAXException e)
+               {
+                       throw new AssertionError("Invalid XML/SAX document generated.");
+               }
+       }
+       
+}
diff --git a/src/de/j32/pimstuff/conduit/Importer.java b/src/de/j32/pimstuff/conduit/Importer.java
new file mode 100644 (file)
index 0000000..9f7b4b2
--- /dev/null
@@ -0,0 +1,9 @@
+package de.j32.pimstuff.conduit;
+
+import java.io.Closeable;
+
+import de.j32.pimstuff.data.EntryProducer;
+
+public interface Importer
+       extends Closeable, EntryProducer
+{}
diff --git a/src/de/j32/pimstuff/data/Addressbook.java b/src/de/j32/pimstuff/data/Addressbook.java
new file mode 100644 (file)
index 0000000..8c1d4b5
--- /dev/null
@@ -0,0 +1,31 @@
+package de.j32.pimstuff.data;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+
+public class Addressbook
+       implements EntryConsumer, EntryProducer
+{
+       LinkedList<Entry> data_ = new LinkedList<Entry>();
+       
+       public void add(Entry entry)
+       {
+               data_.add(entry);
+       }
+       
+       public Iterator<Entry> entries()
+       {
+               return data_.iterator();
+       }
+       
+       public void consume(Entry entry)
+       {
+               add(entry);
+       }
+       
+       public void sendTo(EntryConsumer consumer)
+       {
+               for (Entry entry : data_)
+                       consumer.consume(entry);
+       }
+}
diff --git a/src/de/j32/pimstuff/data/Attribute.java b/src/de/j32/pimstuff/data/Attribute.java
new file mode 100644 (file)
index 0000000..3889ebb
--- /dev/null
@@ -0,0 +1,17 @@
+package de.j32.pimstuff.data;
+
+public class Attribute
+{
+       public String type;
+       public String rel;
+       public String value;
+       public int index;
+       
+       public Attribute(String type_, String rel_, String value_, int index_)
+       {
+               type = type_;
+               rel = rel_;
+               value = value_;
+               index = index_;
+       }
+}
diff --git a/src/de/j32/pimstuff/data/Entry.java b/src/de/j32/pimstuff/data/Entry.java
new file mode 100644 (file)
index 0000000..909cd3b
--- /dev/null
@@ -0,0 +1,75 @@
+package de.j32.pimstuff.data;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+
+import de.j32.util.Filter;
+import de.j32.util.FilteredIterator;
+
+public class Entry
+{
+       long id_ = 0;
+       String name_ = "";
+       ArrayList<Attribute> attributes_ = new ArrayList<Attribute>();
+
+       public void name(String name)
+       {
+               name_ = name;
+       }
+       
+       public String name()
+       {
+               return name_;
+       }
+       
+       public void id(long id)
+       {
+               id_ = id;
+       }
+       
+       public long id()
+       {
+               return id_;
+       }
+       
+       public void attribute(String type, String rel, String value)
+       {
+               attributes_.add(new Attribute(type, rel, value,attributes_.size()));
+       }
+
+       public Iterable<Attribute> attributes()
+       {
+               return attributes_;
+       }
+       
+       public Iterable<Attribute> attributes(final String type)
+       {
+               return new Iterable<Attribute>() {
+                       public Iterator<Attribute> iterator() {
+                               return new FilteredIterator<Attribute>(
+                                               attributes_.iterator(),
+                                               new Filter<Attribute>() {
+                                                       public boolean match(Attribute element) {
+                                                               return element.type == type;
+                                                       }
+                                               });
+                       }
+               };
+       }
+       
+       public Iterable<Attribute> attributes(final String type, final String rel)
+       {
+               return new Iterable<Attribute>() {
+                       public Iterator<Attribute> iterator() {
+                               return new FilteredIterator<Attribute>(
+                                               attributes_.iterator(),
+                                               new Filter<Attribute>() {
+                                                       public boolean match(Attribute element) {
+                                                               return element.type == type && element.rel == rel;
+                                                       }
+                                               });
+                       }
+               };
+       }
+
+}
diff --git a/src/de/j32/pimstuff/data/EntryConsumer.java b/src/de/j32/pimstuff/data/EntryConsumer.java
new file mode 100644 (file)
index 0000000..98f3e07
--- /dev/null
@@ -0,0 +1,6 @@
+package de.j32.pimstuff.data;
+
+public interface EntryConsumer
+{
+       public void consume(Entry entry);       
+}
diff --git a/src/de/j32/pimstuff/data/EntryProducer.java b/src/de/j32/pimstuff/data/EntryProducer.java
new file mode 100644 (file)
index 0000000..9b79f97
--- /dev/null
@@ -0,0 +1,6 @@
+package de.j32.pimstuff.data;
+
+public interface EntryProducer
+{
+       public void sendTo(EntryConsumer consumer);
+}
diff --git a/src/de/j32/util/Filter.java b/src/de/j32/util/Filter.java
new file mode 100644 (file)
index 0000000..07f4359
--- /dev/null
@@ -0,0 +1,6 @@
+package de.j32.util;
+
+public interface Filter<E>
+{
+       public boolean match(E element);
+}
diff --git a/src/de/j32/util/FilteredIterator.java b/src/de/j32/util/FilteredIterator.java
new file mode 100644 (file)
index 0000000..26ad1f0
--- /dev/null
@@ -0,0 +1,54 @@
+package de.j32.util;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+
+public class FilteredIterator<E>
+       implements Iterator<E>
+{
+       Iterator<E> base_;
+       Filter<E> filter_;
+       E next_;
+       boolean hasNext_ = true;
+
+       public FilteredIterator(Iterator<E> base, Filter<E> filter)
+       {
+               base_ = base;
+               filter_ = filter;
+               advance();
+       }
+       
+       public E next()
+       {
+               if (hasNext_) {
+                       E rv = next_;
+                       advance();
+                       return rv;
+               }
+               else 
+                       throw new NoSuchElementException();
+       }
+       
+       public boolean hasNext()
+       {
+               return hasNext_;
+       }
+       
+       public void remove()
+       {
+               throw new UnsupportedOperationException();
+       }
+       
+       void advance()
+       {
+               while (base_.hasNext()) {
+                       next_ = base_.next();
+                       if (filter_.match(next_))
+                               return;
+               }
+               hasNext_ = false;
+               next_ = null;
+       }
+
+}
diff --git a/src/de/j32/util/SimpleXmlGenerator.java b/src/de/j32/util/SimpleXmlGenerator.java
new file mode 100644 (file)
index 0000000..cc8356d
--- /dev/null
@@ -0,0 +1,127 @@
+package de.j32.util;
+
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
+import java.util.Stack;
+
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.transform.TransformerFactoryConfigurationError;
+import javax.xml.transform.sax.SAXTransformerFactory;
+import javax.xml.transform.sax.TransformerHandler;
+import javax.xml.transform.stream.StreamResult;
+
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.AttributesImpl;
+
+public class SimpleXmlGenerator
+{
+       TransformerHandler handler_;
+       Stack<String> openElements_ = new Stack<String>();
+       
+       public SimpleXmlGenerator(OutputStream os, String encoding)
+               throws UnsupportedEncodingException
+       {
+               SAXTransformerFactory factory = (SAXTransformerFactory) SAXTransformerFactory.newInstance();
+               // factory.setAttribute("indent-number", new Integer(2));
+               try {
+                       handler_ = factory.newTransformerHandler();
+               }
+               catch (TransformerConfigurationException e) {
+                       throw new RuntimeException("XML/SAX transformer configuration error");
+               }
+               catch (TransformerFactoryConfigurationError e) {
+                       throw new RuntimeException("XML/SAX transformer factory configuration error");
+               }
+               Transformer tf = handler_.getTransformer();
+               tf.setOutputProperty(OutputKeys.ENCODING,encoding);
+               // if (doctype != null)
+               //        tf.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM,doctype);
+               // tf.setOutputProperty(OutputKeys.INDENT,indent ? "yes" : "no");
+               handler_.setResult(new StreamResult(new OutputStreamWriter(os, encoding)));
+       }
+       
+       public void startDocument()
+               throws SAXException
+       {
+               handler_.startDocument();
+       }
+
+       public void start(String name)
+               throws SAXException
+       {
+               handler_.startElement("","",name,null);
+               openElements_.add(name);
+       }
+       
+       public static class Attributes
+       {
+               AttributesImpl attributes_ = new AttributesImpl();
+               
+               public Attributes attribute(String name, String value)
+               {
+                       attributes_.addAttribute("","",name,"CDATA",value);
+                       return this;
+               }
+               
+               AttributesImpl getAttributes()
+               {
+                       return attributes_;
+               }
+               
+               Attributes() {}
+       }
+
+       public Attributes attribute(String name, String value)
+       {
+               Attributes a = new Attributes();
+               return a.attribute(name, value);
+       }
+       
+       public void start(String name, Attributes attrs)
+               throws SAXException
+       {
+               handler_.startElement("","",name, attrs.getAttributes());
+               openElements_.push(name);
+       }
+       
+       public void end()
+               throws SAXException
+       {
+               handler_.endElement("","",openElements_.pop());
+       }
+       
+       public void empty(String name)
+               throws SAXException
+       {
+               start(name);
+               end();
+       }
+       
+       public void empty(String name, Attributes attrs)
+               throws SAXException
+       {
+               start(name, attrs);
+               end();
+       }
+       
+       public void text(String text)
+               throws SAXException
+       {
+               handler_.characters(text.toCharArray(), 0, text.length());
+       }
+       
+       public void nl()
+               throws SAXException
+       {
+               text("\n");
+       }
+       
+       public void endDocument()
+               throws SAXException
+       {
+               handler_.endDocument();
+       }
+}
diff --git a/src/de/j32/util/Util.java b/src/de/j32/util/Util.java
new file mode 100644 (file)
index 0000000..95a3f78
--- /dev/null
@@ -0,0 +1,40 @@
+package de.j32.util;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Iterator;
+
+public class Util {
+
+       public static <E> E nonnull(E ob) {
+               if (ob == null)
+                       throw new NullPointerException();
+               return ob;
+       }
+
+       public static void transfer(InputStream is, OutputStream os)
+                       throws IOException {
+               byte[] buffer = new byte[16384];
+               int len = -1;
+               while ((len = is.read(buffer)) != -1)
+                       os.write(buffer, 0, len);
+       }
+
+       public static void nothrowClose(Closeable c) {
+               if (c != null) {
+                       try {
+                               c.close();
+                       } catch (IOException e) {}
+               }
+       }
+       
+       public static <E> E first(Iterable<E> i)
+       {
+               Iterator<E> it = i.iterator();
+               if (it.hasNext())
+                       return it.next();
+               return null;
+       }
+}
diff --git a/src/de/j32/util/XmlUtil.java b/src/de/j32/util/XmlUtil.java
new file mode 100644 (file)
index 0000000..2ec9086
--- /dev/null
@@ -0,0 +1,101 @@
+package de.j32.util;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.StringReader;
+import java.util.Iterator;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.EntityResolver;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+public class XmlUtil
+{
+       public static Document parse(InputStream is)
+               throws SAXException, IOException
+       {
+               try {
+                       DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+                       factory.setValidating(false);
+                       factory.setExpandEntityReferences(false);
+                       DocumentBuilder builder = factory.newDocumentBuilder();
+                       builder.setEntityResolver(new EntityResolver() {
+                               public InputSource resolveEntity(String publicId, String systemId)
+                               throws SAXException, IOException
+                               {
+                                       return new InputSource(new StringReader(""));
+                               }});            
+                       return builder.parse(is);
+               }
+               catch (ParserConfigurationException e) {
+                       throw new RuntimeException("SAX/DOM parser configuration error");
+               }
+       }
+       
+       public static Iterable<Element> iterateElements(final NodeList nodes)
+       {
+               return new Iterable<Element>() {
+                       public Iterator<Element> iterator() {
+                               return new NodeListIterator<Element>(nodes, Element.class);
+                       }
+               };
+       }
+
+       static class NodeListIterator<E extends Node>
+               implements Iterator<E>
+       {
+               Class<?> nodeType_;
+               NodeList nodes_;
+               int i_ = 0;
+               E next_;
+               
+               public NodeListIterator(NodeList nodes, Class<?> nodeType)
+               {
+                       nodes_ = nodes;
+                       nodeType_ = nodeType;
+                       advance();
+               }
+               
+               @Override
+               public boolean hasNext()
+               {
+                       return next_ != null;
+               }
+
+               @Override
+               public E next()
+               {
+                       E rv = next_;
+                       advance();
+                       return rv;
+               }
+
+               @Override
+               public void remove()
+               {
+                       throw new UnsupportedOperationException();
+               }
+               
+               @SuppressWarnings("unchecked")
+               void advance()
+               {
+                       while (i_ < nodes_.getLength()) {
+                               Node n = nodes_.item(i_);
+                               ++ i_;
+                               if (nodeType_.isInstance(n)) {
+                                       next_ = (E) n;
+                                       return;
+                               }
+                       }
+                       next_ = null;
+               }
+       }       
+}