diff --git a/README.md b/README.md
index aa207e5..23f1acf 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,13 @@
-# graph-ldap-sync
\ No newline at end of file
+# graph-ldap-sync
+
+LDAP user import utility Apache Shindig with the Neo4j Websocket Backend (https://github.com/iisys-hof/shindig-websocket-client)
+
+Works with generic connectors and an XML-defined mapping of attributes.
+
+License: Apache License, Version 2.0 http://www.apache.org/licenses/LICENSE-2.0
+
+### Usage
+1. Build a using maven build with package goal
+2. A runnable binary is generated in the target-Folder
+3. Edit ldapConfig.xml to match your setup
+4. Run, optionally with a configuration file parameter
\ No newline at end of file
diff --git a/ldapConfig.xml b/ldapConfig.xml
new file mode 100644
index 0000000..3ad426e
--- /dev/null
+++ b/ldapConfig.xml
@@ -0,0 +1,175 @@
+
+
+ ldap://127.0.0.1:389/
+ uid=admin,ou=admins,dc=schub,dc=de
+ secret
+ ou=users,dc=schub,dc=de
+ DAILY
+ true
+ true
+ true
+
+ person
+ inetOrgPerson
+
+
+ ou=users,dc=schub,dc=de
+
+
+ shindig-graph
+ true
+ false
+ false
+ false
+
+ admin
+ http://127.0.0.1:8080/shindig/
+ id,name,displayName,organizations,thumbnailUrl,emails,phoneNumbers
+ /home/user/pictures/
+ http://127.0.0.1:8080/pictures/
+
+
+
+
+ uid
+ id
+ FROM_LDAP
+ COPY
+
+
+ cn
+ name.formatted
+ FROM_LDAP
+ COPY
+
+
+ cn
+ displayName
+ FROM_LDAP
+ COPY
+
+
+ givenName
+ name.givenName
+ FROM_LDAP
+ COPY
+
+
+ sn
+ name.familyName
+ FROM_LDAP
+ COPY
+
+
+
+ jpegPhoto
+ thumbnail
+ FROM_LDAP
+ COPY
+
+
+ mail
+ emails
+ FROM_LDAP
+ COPY
+
+
+ telephoneNumber
+ phoneNumbers
+ FROM_LDAP
+ COPY
+
+
+ roomNumber
+ org_location
+ FROM_LDAP
+ COPY
+
+
+ physicalDeliveryOfficeName
+ org_site
+ FROM_LDAP
+ COPY
+
+
+ title
+ job_title
+ FROM_LDAP
+ COPY
+
+
+
+ manager
+ managerId
+ FROM_LDAP
+ COPY
+
+
+ secretary
+ secretary
+ FROM_LDAP
+ COPY
+
+
+ departmentNumber
+ department
+ FROM_LDAP
+ COPY
+
+
+ ou
+ orgUnit
+ FROM_LDAP
+ COPY
+
+
+
+
+ uid
+ id
+ BOTH
+ COPY_ON_CREATE
+
+
+ cn
+ name.formatted
+ BOTH
+ COPY_ON_CREATE
+
+
+ cn
+ displayName
+ BOTH
+ COPY_ON_CREATE
+
+
+ givenName
+ name.givenName
+ BOTH
+ COPY_ON_CREATE
+
+
+ sn
+ name.familyName
+ BOTH
+ COPY_ON_CREATE
+
+
+
+
+
+
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..135ce3a
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,31 @@
+
+ 4.0.0
+ de.hofuniversity.iisys
+ graph-ldap-sync
+ 0.0.5
+
+
+
+ maven-assembly-plugin
+
+
+ jar-with-dependencies
+
+
+
+ de.hofuniversity.iisys.ldapsync.LdapSync
+
+
+
+
+
+ package
+
+ single
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/java/de/hofuniversity/iisys/ldapsync/ILdapConnector.java b/src/main/java/de/hofuniversity/iisys/ldapsync/ILdapConnector.java
new file mode 100644
index 0000000..83676d2
--- /dev/null
+++ b/src/main/java/de/hofuniversity/iisys/ldapsync/ILdapConnector.java
@@ -0,0 +1,115 @@
+package de.hofuniversity.iisys.ldapsync;
+
+import javax.naming.NamingEnumeration;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.ModificationItem;
+
+/**
+ * Interface for LDAP connectors that manage domains and other prefixes and
+ * suffixes and offer manipulation functionality. All names are considered UIDs.
+ *
+ * @author fholzschuher2
+ *
+ */
+public interface ILdapConnector
+{
+ /**
+ * Establishes a connection to the specified LDAP directory service.
+ *
+ * @throws Exception
+ * if creating the connection fails
+ */
+ public void connect() throws Exception;
+
+ /**
+ * Disconnects any established connection to the LDAP directory service.
+ *
+ * @throws Exception
+ * if disconnecting fails
+ */
+ public void disconnect() throws Exception;
+
+ /**
+ * @return whether a connection is currently established
+ */
+ public boolean isConnected();
+
+ /**
+ * Queries the LDAP directory service for the given name of a context or
+ * object which may not be null.
+ *
+ * @param name
+ * name (UID) of the context or object to search
+ * @return query result
+ * @throws Exception
+ * if the query is flawed or fails
+ */
+ @SuppressWarnings("rawtypes")
+ public NamingEnumeration nameQuery(String name) throws Exception;
+
+ /**
+ * Queries the LDAP directory service for all contexts and objects that
+ * match the given filter expression which may not be null or empty.
+ *
+ * @param filter
+ * filter expression to use
+ * @return query result
+ * @throws Exception
+ * if the query is flawed or fails
+ */
+ @SuppressWarnings("rawtypes")
+ public NamingEnumeration filterQuery(String filter) throws Exception;
+
+ /**
+ * Queries the LDAP directory service for the given name of a context or
+ * object and filters the results with the given expression. None of the
+ * parameters may be null.
+ *
+ * @param name
+ * name (UID) of the context or object to search
+ * @param filter
+ * filter expression to use
+ * @return query result
+ * @throws Exception
+ * if the query is flawed or fails
+ */
+ @SuppressWarnings("rawtypes")
+ public NamingEnumeration query(String name, String filter) throws Exception;
+
+ /**
+ * Carries out the given modifications on the specified directory entry.
+ * None of the parameters may be null.
+ *
+ * @param name
+ * name (UID) of the entity to modify
+ * @param mods
+ * attribute modifications
+ * @throws Exception
+ * if parameters are flawed or the operation fails
+ */
+ public void update(String name, ModificationItem[] mods) throws Exception;
+
+ /**
+ * Creates the entity as defined by the directory context and the given
+ * name. No parameter may be null or empty.
+ *
+ * @param name
+ * name (UID) of the entity to create
+ * @param object
+ * entity to store with initial attributes to set
+ * @throws Exception
+ * if the creation fails
+ */
+ public void create(String name, DirContext object) throws Exception;
+
+ /**
+ * Removes an entry from the LDAP directory as defined by the given name.
+ * Should be used with caution.
+ *
+ * @param name
+ * name (UID) of the entity to remove
+ * @throws Exception
+ * if the removal fails
+ */
+ public void remove(String name) throws Exception;
+}
diff --git a/src/main/java/de/hofuniversity/iisys/ldapsync/ISyncEndpointFactory.java b/src/main/java/de/hofuniversity/iisys/ldapsync/ISyncEndpointFactory.java
new file mode 100644
index 0000000..3f1123b
--- /dev/null
+++ b/src/main/java/de/hofuniversity/iisys/ldapsync/ISyncEndpointFactory.java
@@ -0,0 +1,24 @@
+package de.hofuniversity.iisys.ldapsync;
+
+import java.util.List;
+
+import de.hofuniversity.iisys.ldapsync.endpoints.ISyncEndpoint;
+
+/**
+ * Factory interface for creating ISyncEndpoint objects that can be used for
+ * synchronization.
+ *
+ * @author fholzschuher2
+ *
+ */
+public interface ISyncEndpointFactory
+{
+ /**
+ * Creates all configured end points and links them to an LDAP connector.
+ * This method should only be called once, unless duplicate end points are
+ * desired.
+ *
+ * @return newly created end points for synchronization
+ */
+ public List createEndpoints();
+}
diff --git a/src/main/java/de/hofuniversity/iisys/ldapsync/LdapBuffer.java b/src/main/java/de/hofuniversity/iisys/ldapsync/LdapBuffer.java
new file mode 100644
index 0000000..0d5cfaa
--- /dev/null
+++ b/src/main/java/de/hofuniversity/iisys/ldapsync/LdapBuffer.java
@@ -0,0 +1,726 @@
+package de.hofuniversity.iisys.ldapsync;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import javax.naming.NamingEnumeration;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.BasicAttribute;
+import javax.naming.directory.BasicAttributes;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.ModificationItem;
+import javax.naming.directory.SearchResult;
+
+import de.hofuniversity.iisys.ldapsync.model.ILdapUser;
+import de.hofuniversity.iisys.ldapsync.model.ILdapUserFactory;
+import de.hofuniversity.iisys.ldapsync.model.SimpleLdapUser;
+
+/**
+ * Class that holds a copy of the current LDAP data that can be modified by
+ * several end point implementations and then synchronized back in a single
+ * operation.
+ *
+ * @author fholzschuher2
+ *
+ */
+public class LdapBuffer
+{
+ private final ILdapConnector fLdap;
+ private final ILdapUserFactory fUserFactory;
+
+ private final Map> fModifications;
+ private final Map fLdapUsers, fNewUsers, fAllUsers;
+ private final Set fDeletedUsers;
+
+ /**
+ * Creates an empty buffer that can hold changes to be written to an LDAP
+ * directory service. Throws a NullPointerException if any argument is null.
+ *
+ * @param ldap
+ * LDAP connector to use for writing changes
+ * @param factory
+ * factory to use for user creation
+ */
+ public LdapBuffer(ILdapConnector ldap, ILdapUserFactory factory)
+ {
+ if (ldap == null)
+ {
+ throw new NullPointerException("ldap connector was null");
+ }
+ if (factory == null)
+ {
+ throw new NullPointerException("user factory was null");
+ }
+
+ fLdap = ldap;
+ fUserFactory = factory;
+
+ fModifications = new HashMap>();
+ fLdapUsers = new HashMap();
+ fNewUsers = new HashMap();
+ fAllUsers = new HashMap();
+ fDeletedUsers = new HashSet();
+ }
+
+ /**
+ * Sets a fresh set of data from an LDAP directory service as the buffer's
+ * state. As a consequence all stored changes are discarded. If the given
+ * object is null, the buffer will be blank.
+ *
+ * @param ldapContents
+ * @throws Exception
+ * if handling results causes an Exception
+ */
+ @SuppressWarnings("rawtypes")
+ public void setData(NamingEnumeration ldapContents) throws Exception
+ {
+ fNewUsers.clear();
+ fDeletedUsers.clear();
+ fModifications.clear();
+ fLdapUsers.clear();
+
+ /*
+ * read and copy all users and their attributes from the result to
+ * prevent further unnecessary access
+ */
+ if (ldapContents != null)
+ {
+ SearchResult result = null;
+ NamingEnumeration extends Attribute> atts = null;
+ NamingEnumeration> vals = null;
+ Attributes localAtts = null;
+ String name = null;
+ String ouString = null;
+ Attribute a = null;
+ Attribute localA = null;
+
+ while (ldapContents.hasMore())
+ {
+ result = (SearchResult) ldapContents.next();
+ localAtts = new BasicAttributes();
+
+ // comes in format "uid=name"
+ // in a subtree search, it's "uid=name,ou=..."
+ name = result.getName();
+ ouString = null;
+ if(name.indexOf(',') > 0)
+ {
+ String[] split = name.split(",");
+ name = split[0];
+ ouString = split[1];
+ }
+ name = name.split("=")[1];
+
+ // copy all to prevent additional lookups
+ atts = result.getAttributes().getAll();
+ while (atts.hasMore())
+ {
+ a = atts.next();
+ localA = new BasicAttribute(a.getID());
+
+ // copy all values
+ vals = a.getAll();
+ while (vals.hasMore())
+ {
+ localA.add(vals.next());
+ }
+
+ localAtts.put(localA);
+ }
+
+ //add organizational unit hierarchy
+ if(ouString != null)
+ {
+ localA = new BasicAttribute("orgUnitString");
+ localA.add(ouString);
+ localAtts.put(localA);
+ }
+
+ // add to map
+ fLdapUsers.put(name, new SimpleLdapUser(name, localAtts));
+ }
+
+ fAllUsers.putAll(fLdapUsers);
+ }
+ }
+
+ /**
+ * Checks if there already is a user with the given UID in the current LDAP
+ * query result or among the newly created users in the buffer. Name may not
+ * be null.
+ *
+ * @param name
+ * UID of the user in question
+ * @return whether the user already exists in the buffer
+ */
+ public boolean hasUser(String name)
+ {
+ boolean has = fAllUsers.containsKey(name);
+
+ return has;
+ }
+
+ /**
+ * Retrieves the user with the given name from the existing LDAP users or
+ * the newly created users and returns null if there is no such user.
+ *
+ * @param name
+ * name of the user
+ * @return user or null
+ */
+ public ILdapUser getUser(String name)
+ {
+ ILdapUser user = fAllUsers.get(name);
+
+ return user;
+ }
+
+ /**
+ * Returns a map of all currently available users including existing LDAP
+ * users as well as newly created users. Users that have already been
+ * deleted from the buffer are not included.
+ *
+ * @return map of all available users
+ */
+ public Map getAllUsers()
+ {
+ return fAllUsers;
+ }
+
+ /**
+ * Returns the current value of an attribute of a person or null if there is
+ * no value. The value returned by this method includes all modifications
+ * that are stored but haven't been written yet, unlike direct object
+ * access.
+ *
+ * @param name
+ * name of the person
+ * @param att
+ * name of the attribute
+ * @return projected value of the attribute after the next update
+ */
+ public Attribute getCurrentAttribute(String name, String att)
+ {
+ Attribute attribute = null;
+
+ // determine whether it's a new or an existing user
+ ILdapUser user = fLdapUsers.get(name);
+ if (user != null)
+ {
+ // check for queued modifications
+ ModificationItem mod = getModification(name, att);
+
+ if (mod != null)
+ {
+ int op = mod.getModificationOp();
+
+ switch (op)
+ {
+ case DirContext.REPLACE_ATTRIBUTE:
+ attribute = mod.getAttribute();
+ break;
+
+ case DirContext.REMOVE_ATTRIBUTE:
+ // remains null, won't exist anymore
+ break;
+
+ case DirContext.ADD_ATTRIBUTE:
+ // collect values
+ attribute = new BasicAttribute(att);
+
+ try
+ {
+ // old values
+ NamingEnumeration> vals = user.getAttribute(att)
+ .getAll();
+ while (vals.hasMore())
+ {
+ attribute.add(vals.next());
+ }
+
+ // new values
+ vals = mod.getAttribute().getAll();
+ while (vals.hasMore())
+ {
+ attribute.add(vals.next());
+ }
+ } catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+
+ break;
+ }
+ } else
+ {
+ // still the original value
+ attribute = user.getAttribute(att);
+ }
+ } else
+ {
+ user = fNewUsers.get(name);
+
+ if (user != null)
+ {
+ attribute = user.getAttribute(att);
+ }
+ }
+
+ return attribute;
+ }
+
+ private ModificationItem getModification(String name, String att)
+ {
+ ModificationItem mod = null;
+
+ Map mods = fModifications.get(name);
+ if (mods != null)
+ {
+ mod = mods.get(att);
+ }
+
+ return mod;
+ }
+
+ private void setModification(String name, String att, ModificationItem mod)
+ {
+ Map mods = fModifications.get(name);
+ if (mods == null)
+ {
+ mods = new HashMap();
+ fModifications.put(name, mods);
+ }
+ mods.put(att, mod);
+ }
+
+ private void removeModification(String name, String att)
+ {
+ Map mods = fModifications.get(name);
+ if (mods != null)
+ {
+ mods.remove(att);
+ }
+ }
+
+ /**
+ * Adds a list of values to an attribute with multiple values without
+ * checking for duplicates. If the attribute does not yet exist, it is
+ * created. The calling class should filter out duplicates if that is the
+ * required behavior. Parameters may not be null or empty.
+ *
+ * @param name
+ * name of the entity the attribute belongs to
+ * @param att
+ * name of the attribute
+ * @param vals
+ * list of values to add
+ */
+ public void addToAttribute(String name, String att, List vals)
+ {
+ // existing users
+ ILdapUser user = fLdapUsers.get(name);
+ Attribute attr = null;
+
+ if (user != null)
+ {
+ // existing changes
+ ModificationItem mod = getModification(name, att);
+ Attribute oldAtt = null;
+
+ if (mod != null)
+ {
+ switch (mod.getModificationOp())
+ {
+ case DirContext.ADD_ATTRIBUTE:
+ // aggregate with existing values to add
+ attr = mod.getAttribute();
+ for (Object val : vals)
+ {
+ attr.add(val);
+ }
+ mod = new ModificationItem(DirContext.ADD_ATTRIBUTE,
+ attr);
+
+ break;
+
+ case DirContext.REPLACE_ATTRIBUTE:
+ /*
+ * aggregate with existing values which will replace old
+ * ones
+ */
+ attr = mod.getAttribute();
+ for (Object val : vals)
+ {
+ attr.add(val);
+ }
+
+ oldAtt = user.getAttribute(att);
+ if (!areEqualLists(oldAtt, attr))
+ {
+ mod = new ModificationItem(
+ DirContext.REPLACE_ATTRIBUTE, attr);
+ }
+
+ break;
+
+ case DirContext.REMOVE_ATTRIBUTE:
+ // replace with new values
+ attr = new BasicAttribute(att);
+ for (Object val : vals)
+ {
+ attr.add(val);
+ }
+
+ oldAtt = user.getAttribute(att);
+ if (!areEqualLists(oldAtt, attr))
+ {
+ mod = new ModificationItem(
+ DirContext.REPLACE_ATTRIBUTE, attr);
+ }
+
+ break;
+ }
+ } else
+ {
+ attr = new BasicAttribute(att);
+ for (Object val : vals)
+ {
+ attr.add(val);
+ }
+ mod = new ModificationItem(DirContext.ADD_ATTRIBUTE, attr);
+ }
+
+ setModification(name, att, mod);
+ } else
+ {
+ // users that haven't been created yet
+ user = fNewUsers.get(name);
+ if (user != null)
+ {
+ attr = user.getAttribute(att);
+
+ if (attr == null)
+ {
+ attr = new BasicAttribute(att);
+ user.addAttribute(attr);
+ }
+
+ for (Object val : vals)
+ {
+ attr.add(val);
+ }
+ }
+ }
+ }
+
+ private boolean areEqualLists(Attribute a1, Attribute a2)
+ {
+ boolean match = true;
+
+ try
+ {
+ NamingEnumeration> ne1 = a1.getAll();
+ NamingEnumeration> ne2 = a2.getAll();
+
+ // compare all a1 values to a2
+ while (ne1.hasMore())
+ {
+ if (!ne2.hasMore() || !ne1.next().equals(ne2.next()))
+ {
+ match = false;
+ break;
+ }
+ }
+
+ // if a2 has more values than a1, they're not equal
+ if (ne2.hasMore())
+ {
+ match = false;
+ }
+ } catch (Exception e)
+ {
+ e.printStackTrace();
+ match = false;
+ }
+
+ return match;
+ }
+
+ /**
+ * Sets an attribute of an entity, overwriting any potential previous
+ * values. Parameters may not be null or empty.
+ *
+ * @param name
+ * name of the entity the attribute belongs
+ * @param att
+ * name of the attribute
+ * @param val
+ * value to set for the attribute
+ */
+ public void setAttribute(String name, String att, Object val)
+ {
+ // existing users
+ ILdapUser user = fLdapUsers.get(name);
+
+ if (user != null)
+ {
+ // check if the value matches the original value
+ Object orgVal = null;
+ Attribute a = user.getAttribute(att);
+ if (a != null)
+ {
+ try
+ {
+ orgVal = a.get();
+ } catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ if (val.equals(orgVal))
+ {
+ // no modifications necessary
+ removeModification(name, att);
+ } else
+ {
+ // attribute set to new value, other modifications irrelevant
+ Attribute attr = new BasicAttribute(att, val);
+ ModificationItem mod = new ModificationItem(
+ DirContext.REPLACE_ATTRIBUTE, attr);
+ setModification(name, att, mod);
+ }
+ }
+ // users that haven't been created yet
+ else
+ {
+ user = fNewUsers.get(name);
+ if (user != null)
+ {
+ user.setAttribute(att, val);
+ }
+ }
+ }
+
+ /**
+ * Sets an attribute with multiple values of an entity, overwriting any
+ * potential previous values. Parameters may not be null or empty.
+ *
+ * @param name
+ * name of the entity the attribute belongs
+ * @param att
+ * name of the attribute
+ * @param vals
+ * new values
+ */
+ public void setAttribute(String name, String att, List vals)
+ {
+ // existing users
+ ILdapUser user = fLdapUsers.get(name);
+
+ if (user != null)
+ {
+ // check if values match the original values
+ Attribute a = user.getAttribute(att);
+ boolean match = false;
+
+ if (a != null)
+ {
+ try
+ {
+ match = true;
+ NamingEnumeration> oldVals = a.getAll();
+
+ for (Object val : vals)
+ {
+ if (!oldVals.hasMore() || !oldVals.next().equals(val))
+ {
+ match = false;
+ break;
+ }
+ }
+ } catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+
+ if (match)
+ {
+ // no modifications necessary
+ removeModification(name, att);
+ } else
+ {
+ // attribute set to new value, other modifications irrelevant
+ Attribute attr = new BasicAttribute(att);
+ for (Object val : vals)
+ {
+ attr.add(val);
+ }
+ ModificationItem mod = new ModificationItem(
+ DirContext.REPLACE_ATTRIBUTE, attr);
+ setModification(name, att, mod);
+ }
+ }
+ // users that haven't been created yet
+ else
+ {
+ user = fNewUsers.get(name);
+
+ if (user != null)
+ {
+ Attribute attr = new BasicAttribute(att);
+ for (Object val : vals)
+ {
+ attr.add(val);
+ }
+ user.addAttribute(attr);
+ }
+ }
+ }
+
+ /**
+ * Removes an attribute from an entity. If the attribute does not exist, the
+ * call is ignored. Parameters should not be null or empty.
+ *
+ * @param name
+ * name of the entity the attribute belongs to
+ * @param att
+ * name of the attribute
+ */
+ public void removeAttribute(String name, String att)
+ {
+ // existing users
+ ILdapUser user = fLdapUsers.get(name);
+
+ if (user != null)
+ {
+ // attribute removed, other modifications irrelevant
+ removeModification(name, att);
+
+ // check whether the attribute was there in the first place
+ Attribute orgVal = user.getAttribute(att);
+ if (orgVal != null)
+ {
+ Attribute attr = new BasicAttribute(att);
+ ModificationItem mod = new ModificationItem(
+ DirContext.REMOVE_ATTRIBUTE, attr);
+ setModification(name, att, mod);
+ }
+ } else
+ {
+ // users that haven't been created yet
+ user = fNewUsers.get(name);
+ if (user != null)
+ {
+ user.removeAttribute(att);
+ }
+ }
+ }
+
+ /**
+ * Creates a new user with the given name and configured initial data. If
+ * there already is a user with the given name, a RuntimeException is
+ * thrown. Name may not be null or empty.
+ *
+ * @param name
+ * UID for the new user
+ * @return newly created user
+ */
+ public ILdapUser createUser(String name)
+ {
+ // check for existing users
+ if (hasUser(name))
+ {
+ throw new RuntimeException("user \"" + name + "\" already exists");
+ }
+
+ ILdapUser user = fUserFactory.createUser(name);
+ fNewUsers.put(name, user);
+ fAllUsers.put(name, user);
+
+ return user;
+ }
+
+ /**
+ * Queues the deletion of the user with the given name and removes it from
+ * the cached result set. The call is ignored if there is no such user.
+ *
+ * @param name
+ * UID of the user to delete
+ */
+ public void deleteUser(String name)
+ {
+ // check if user exists and isn't already being deleted
+ if (fLdapUsers.containsKey(name) && !fDeletedUsers.contains(name))
+ {
+ fDeletedUsers.add(name);
+ fLdapUsers.remove(name);
+ }
+
+ // delete from new users as well?
+ fNewUsers.remove(name);
+
+ fAllUsers.remove(name);
+ }
+
+ /**
+ * @return whether there are changes in the buffer that can be written
+ */
+ public boolean hasChanges()
+ {
+ return !fNewUsers.isEmpty() || !fDeletedUsers.isEmpty()
+ || !fModifications.isEmpty();
+ }
+
+ /**
+ * Writes all changes in the buffer to the already connected LDAP directory
+ * service. The connection is not closed after writing. Changes are written
+ * in this order: user deletions, user creations, attribute updates.
+ *
+ * @throws Exception
+ * if an Exception occurs during writing
+ */
+ public void writeToLdap() throws Exception
+ {
+ // delete users
+ for (String name : fDeletedUsers)
+ {
+ fLdap.remove(name);
+ }
+ fDeletedUsers.clear();
+
+ // create new users
+ for (Entry userE : fNewUsers.entrySet())
+ {
+ fLdap.create(userE.getKey(), userE.getValue());
+ }
+ fNewUsers.clear();
+
+ // update attributes of existing users
+ String name = null;
+ List modList = null;
+ ModificationItem[] modArr = null;
+ for (Entry> entry : fModifications
+ .entrySet())
+ {
+ name = entry.getKey();
+ modList = new ArrayList();
+
+ for (Entry mod : entry.getValue()
+ .entrySet())
+ {
+ modList.add(mod.getValue());
+ }
+
+ modArr = new ModificationItem[modList.size()];
+ fLdap.update(name, modList.toArray(modArr));
+ }
+ fModifications.clear();
+ }
+}
diff --git a/src/main/java/de/hofuniversity/iisys/ldapsync/LdapSync.java b/src/main/java/de/hofuniversity/iisys/ldapsync/LdapSync.java
new file mode 100644
index 0000000..79a6a91
--- /dev/null
+++ b/src/main/java/de/hofuniversity/iisys/ldapsync/LdapSync.java
@@ -0,0 +1,102 @@
+package de.hofuniversity.iisys.ldapsync;
+
+import de.hofuniversity.iisys.ldapsync.config.SyncConfig;
+import de.hofuniversity.iisys.ldapsync.config.XMLConfigReader;
+import de.hofuniversity.iisys.ldapsync.model.ILdapUserFactory;
+import de.hofuniversity.iisys.ldapsync.model.LdapUserFactory;
+
+/**
+ * Startup class for the LDAP synchronization program for Apache Shindig and
+ * Apache Rave. It reads the configuration and determines which properties to
+ * synchronize in which direction.
+ *
+ * @author fholzschuher2
+ *
+ */
+public class LdapSync
+{
+ private final String fConfigPath;
+
+ /**
+ * Creates a new LDAP synchronization instance using the configuration
+ * specified under the given path. Throws a NullPointerException if the
+ * given path is null or empty.
+ *
+ * @param configPath
+ * path to configuration file
+ */
+ public LdapSync(String configPath)
+ {
+ if (configPath == null || configPath.isEmpty())
+ {
+ throw new NullPointerException("no configuration path given");
+ }
+
+ fConfigPath = configPath;
+ }
+
+ /**
+ * Reads the configuration, tests the LDAP connection and if successful
+ * starts a thread to regularly perform updates.
+ *
+ * @throws Exception
+ * if any part of the startup process fails
+ */
+ public void start() throws Exception
+ {
+ // read configuration
+ XMLConfigReader cReader = new XMLConfigReader(fConfigPath);
+ SyncConfig config = cReader.readConfig();
+
+ // create connector
+ ILdapConnector conn = new SimpleLdapConnector(config);
+
+ // test connection
+ conn.connect();
+ conn.disconnect();
+
+ // user factory
+ ILdapUserFactory userFactory = new LdapUserFactory(config);
+
+ // create buffer
+ LdapBuffer buffer = new LdapBuffer(conn, userFactory);
+
+ // create end point factory
+ ISyncEndpointFactory endPointFactory = new SyncEndpointFactory(config,
+ buffer);
+
+ // start scheduler
+ SyncScheduler scheduler = new SyncScheduler(config, conn,
+ endPointFactory, buffer);
+ Thread schedThread = new Thread(scheduler);
+ schedThread.start();
+
+ // TODO: deliver external interface to trigger synchronization
+ }
+
+ /**
+ * Simple startup routine that creates an LDAP synchronizer witch the
+ * configuration file at the path specified or with default parameters and
+ * starts it.
+ *
+ * @param args
+ * first parameter can be the path to a configuration file
+ */
+ public static void main(String[] args)
+ {
+ String path = "ldapConfig.xml";
+
+ if (args != null && args.length > 0 && !args[0].isEmpty())
+ {
+ path = args[0];
+ }
+
+ try
+ {
+ new LdapSync(path).start();
+ } catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/src/main/java/de/hofuniversity/iisys/ldapsync/SimpleLdapConnector.java b/src/main/java/de/hofuniversity/iisys/ldapsync/SimpleLdapConnector.java
new file mode 100644
index 0000000..e156a76
--- /dev/null
+++ b/src/main/java/de/hofuniversity/iisys/ldapsync/SimpleLdapConnector.java
@@ -0,0 +1,166 @@
+package de.hofuniversity.iisys.ldapsync;
+
+import java.util.Hashtable;
+
+import javax.naming.Context;
+import javax.naming.NamingEnumeration;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.InitialDirContext;
+import javax.naming.directory.ModificationItem;
+import javax.naming.directory.SearchControls;
+
+import de.hofuniversity.iisys.ldapsync.config.SyncConfig;
+
+/**
+ * Simple plain text LDAP connector providing an interface to query an LDAP
+ * directory service. Supports anonymous and user-password authentication. All
+ * names are considered UIDs.
+ *
+ * @author fholzschuher2
+ *
+ */
+public class SimpleLdapConnector implements ILdapConnector
+{
+ private final String fUrl, fUser, fUserContext;
+ private final char[] fPassword;
+
+ private final SearchControls fCtrl;
+
+ private final boolean fReadOnly;
+
+ private DirContext fContext;
+ private boolean fConnected;
+
+ /**
+ * Creates a simple LDAP connector using the given configuration. Throws a
+ * NullPointerException if the given configuration is null or the specified
+ * URL is null or empty.
+ *
+ * @param config
+ * configuration to use
+ */
+ public SimpleLdapConnector(SyncConfig config)
+ {
+ fUrl = config.getUrl();
+
+ if (fUrl == null || fUrl.isEmpty())
+ {
+ throw new NullPointerException("no URL given");
+ }
+
+ if (config.getContext() == null)
+ {
+ fUserContext = "";
+ } else
+ {
+ fUserContext = "," + config.getContext();
+ }
+
+ fUser = config.getUser();
+ fPassword = config.getPassword();
+ fReadOnly = config.getReadOnly();
+
+ fCtrl = new SearchControls();
+
+ if(config.getSubtreeSearch())
+ {
+ fCtrl.setSearchScope(SearchControls.SUBTREE_SCOPE);
+ }
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ public void connect() throws Exception
+ {
+ if (!fConnected)
+ {
+ Hashtable env = new Hashtable();
+ env.put(Context.INITIAL_CONTEXT_FACTORY,
+ "com.sun.jndi.ldap.LdapCtxFactory");
+ env.put(Context.PROVIDER_URL, fUrl);
+
+ if (fUser != null && fPassword != null)
+ {
+ env.put(Context.SECURITY_AUTHENTICATION, "simple");
+ env.put(Context.SECURITY_PRINCIPAL, fUser);
+ env.put(Context.SECURITY_CREDENTIALS, new String(fPassword));
+ }
+
+ fContext = new InitialDirContext(env);
+
+ fConnected = true;
+ }
+ }
+
+ public void disconnect() throws Exception
+ {
+ if (fConnected)
+ {
+ fConnected = false;
+ fContext.close();
+ }
+ }
+
+ public boolean isConnected()
+ {
+ return fConnected;
+ }
+
+ @SuppressWarnings("rawtypes")
+ public NamingEnumeration nameQuery(String name) throws Exception
+ {
+ return query(name, "uid=*");
+ }
+
+ @SuppressWarnings("rawtypes")
+ public NamingEnumeration filterQuery(String filter) throws Exception
+ {
+ return query("", filter);
+ }
+
+ @SuppressWarnings("rawtypes")
+ public NamingEnumeration query(String name, String filter) throws Exception
+ {
+ if (!name.isEmpty())
+ {
+ name = "uid=" + name + fUserContext;
+ } else
+ {
+ name = fUserContext.substring(1);
+ }
+
+ return fContext.search(name, filter, fCtrl);
+ }
+
+ public void update(String name, ModificationItem[] mods) throws Exception
+ {
+ if (!fReadOnly && !name.isEmpty())
+ {
+ name = "uid=" + name + fUserContext;
+
+ fContext.modifyAttributes(name, mods);
+ }
+ }
+
+ public void create(String name, DirContext object) throws Exception
+ {
+ if (!fReadOnly)
+ {
+ if (!name.isEmpty())
+ {
+ name = "uid=" + name + fUserContext;
+ }
+
+ fContext.bind(name, object);
+ }
+ }
+
+ public void remove(String name) throws Exception
+ {
+ if (!name.isEmpty())
+ {
+ name = "uid=" + name + fUserContext;
+ }
+
+ fContext.destroySubcontext(name);
+ }
+}
diff --git a/src/main/java/de/hofuniversity/iisys/ldapsync/SyncEndpointFactory.java b/src/main/java/de/hofuniversity/iisys/ldapsync/SyncEndpointFactory.java
new file mode 100644
index 0000000..d229e76
--- /dev/null
+++ b/src/main/java/de/hofuniversity/iisys/ldapsync/SyncEndpointFactory.java
@@ -0,0 +1,92 @@
+package de.hofuniversity.iisys.ldapsync;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import de.hofuniversity.iisys.ldapsync.config.SyncConfig;
+import de.hofuniversity.iisys.ldapsync.config.SyncEndpointConfig;
+import de.hofuniversity.iisys.ldapsync.endpoints.ISyncEndpoint;
+import de.hofuniversity.iisys.ldapsync.endpoints.RaveEndpoint;
+import de.hofuniversity.iisys.ldapsync.endpoints.ShindigGraphEndpoint;
+import de.hofuniversity.iisys.ldapsync.endpoints.TestEndpoint;
+
+/**
+ * Factory creating ISyncEndpoint objects from configuration parameters that can
+ * be used for synchronization.
+ *
+ * @author fholzschuher2
+ *
+ */
+public class SyncEndpointFactory implements ISyncEndpointFactory
+{
+ private final List fConfigs;
+ private final LdapBuffer fLdap;
+
+ /**
+ * Creates an end point factory, using configurations from the given
+ * configuration object and linking them to the given LDAP buffer.
+ *
+ * @param config
+ * configuration to use
+ * @param ldap
+ * LDAP buffer to use
+ */
+ public SyncEndpointFactory(SyncConfig config, LdapBuffer ldap)
+ {
+ if (config == null)
+ {
+ throw new NullPointerException("configuration was null");
+ }
+ if (config.getEndpoints() == null)
+ {
+ throw new NullPointerException("list of endpoint configurations "
+ + "was null");
+ }
+ if (ldap == null)
+ {
+ throw new NullPointerException("ldap buffer was null");
+ }
+
+ fConfigs = config.getEndpoints();
+ fLdap = ldap;
+ }
+
+ public List createEndpoints()
+ {
+ List endPoints = new ArrayList();
+
+ ISyncEndpoint ep = null;
+ for (SyncEndpointConfig config : fConfigs)
+ {
+ ep = getEndpoint(config);
+ if (ep != null)
+ {
+ endPoints.add(ep);
+ }
+ }
+
+ return endPoints;
+ }
+
+ private ISyncEndpoint getEndpoint(SyncEndpointConfig config)
+ {
+ final String type = config.getType();
+ ISyncEndpoint ep = null;
+
+ if ("shindig-graph".equalsIgnoreCase(type))
+ {
+ ep = new ShindigGraphEndpoint(config, fLdap);
+ } else if ("rave".equalsIgnoreCase(type))
+ {
+ ep = new RaveEndpoint(config, fLdap);
+ } else if ("test".equalsIgnoreCase(type))
+ {
+ ep = new TestEndpoint(fLdap, config);
+ } else
+ {
+ System.err.println("unknown endpoint type: " + type);
+ }
+
+ return ep;
+ }
+}
diff --git a/src/main/java/de/hofuniversity/iisys/ldapsync/SyncScheduler.java b/src/main/java/de/hofuniversity/iisys/ldapsync/SyncScheduler.java
new file mode 100644
index 0000000..09692b9
--- /dev/null
+++ b/src/main/java/de/hofuniversity/iisys/ldapsync/SyncScheduler.java
@@ -0,0 +1,283 @@
+package de.hofuniversity.iisys.ldapsync;
+
+import java.util.GregorianCalendar;
+import java.util.List;
+
+import javax.naming.NamingEnumeration;
+
+import de.hofuniversity.iisys.ldapsync.config.SyncConfig;
+import de.hofuniversity.iisys.ldapsync.endpoints.ISyncEndpoint;
+
+/**
+ * Scheduler that initiates the synchronization process on a regular basis as
+ * configured or when forced. It opens a connection before synchronizing and
+ * closes it afterwards.
+ *
+ * @author fholzschuher2
+ *
+ */
+public class SyncScheduler implements Runnable
+{
+ private static final long TIME_THRESHOLD = 60000;
+
+ private final Object fTrigger;
+
+ private final SyncConfig fConfig;
+ private final ILdapConnector fLdap;
+ private final ISyncEndpointFactory fFactory;
+ private final LdapBuffer fBuffer;
+
+ private long fWaitTime, fNextSync;
+
+ private List fEndPoints;
+
+ private boolean fRunning;
+ private boolean fForceSync;
+
+ /**
+ * Creates a synchronization scheduler that synchronizes according to the
+ * given configuration, controlling the given connector, handling the end
+ * points from the given factory. Throws a NullPointerException if any
+ * parameter is null.
+ *
+ * @param config
+ * configuration object to use
+ * @param ldap
+ * LDAP connector to control
+ * @param factory
+ * factory that delivers end points
+ * @param buffer
+ * buffer whose changes to write
+ */
+ public SyncScheduler(SyncConfig config, ILdapConnector ldap,
+ ISyncEndpointFactory factory, LdapBuffer buffer)
+ {
+ if (config == null)
+ {
+ throw new NullPointerException("configuration was null");
+ }
+ if (ldap == null)
+ {
+ throw new NullPointerException("ldap connector was null");
+ }
+ if (factory == null)
+ {
+ throw new NullPointerException("end point factory was null");
+ }
+ if (buffer == null)
+ {
+ throw new NullPointerException("ldap buffer was null");
+ }
+
+ fTrigger = new Object();
+ fConfig = config;
+ fLdap = ldap;
+ fFactory = factory;
+ fBuffer = buffer;
+
+ fForceSync = fConfig.getSyncOnStart();
+ fEndPoints = fFactory.createEndpoints();
+ }
+
+ public void run()
+ {
+ fRunning = true;
+
+ while (fRunning)
+ {
+ // execute synchronizations if forced or time is correct enough
+ if (fForceSync || System.currentTimeMillis() >= fNextSync
+ || System.currentTimeMillis() - fNextSync < TIME_THRESHOLD)
+ {
+ fForceSync = false;
+
+ try
+ {
+ sync();
+ } catch (Exception e)
+ {
+ e.printStackTrace();
+
+ // stop?
+ // fRunning = false;
+ }
+ }
+
+ // stop if there was an interrupt
+ if (!fRunning)
+ {
+ return;
+ }
+
+ // compute time to next cycle
+ computeTime();
+
+ try
+ {
+ synchronized (fTrigger)
+ {
+ fTrigger.wait(fWaitTime);
+ }
+ } catch (InterruptedException e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @SuppressWarnings("rawtypes")
+ private void sync() throws Exception
+ {
+ // get current data from LDAP
+ System.out.println("connecting to LDAP");
+ fLdap.connect();
+
+ // refresh data
+ System.out.print("getting users from LDAP");
+ long time = System.currentTimeMillis();
+ NamingEnumeration data = fLdap.query("", "uid=*");
+ fBuffer.setData(data);
+ time = System.currentTimeMillis() - time;
+ System.out.println(" (" + time + " ms)");
+
+ // refresh all end points
+ System.out.println("synchronizing with end points");
+ int count = 1;
+ for (ISyncEndpoint endPoint : fEndPoints)
+ {
+ try
+ {
+ System.out.print("end point " + count + " ...");
+ time = System.currentTimeMillis();
+ endPoint.sync();
+ time = System.currentTimeMillis() - time;
+ System.out.println(" done (" + time + " ms)");
+ } catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+
+ // write changes in the buffer if there are any
+ if (fBuffer.hasChanges())
+ {
+ System.out.print("writing changes to LDAP ...");
+ time = System.currentTimeMillis();
+ fBuffer.writeToLdap();
+ time = System.currentTimeMillis() - time;
+ System.out.println(" done (" + time + " ms)");
+ } else
+ {
+ System.out.println("no changes");
+ }
+
+ // discard LDAP connection
+ System.out.println("disconnecting from LDAP");
+ fLdap.disconnect();
+ }
+
+ private void computeTime()
+ {
+ GregorianCalendar cal = new GregorianCalendar();
+ String timeString = fConfig.getTime();
+ int hour = cal.get(GregorianCalendar.HOUR_OF_DAY);
+ int minute = cal.get(GregorianCalendar.MINUTE);
+ int day = cal.get(GregorianCalendar.DAY_OF_WEEK);
+ int week = cal.get(GregorianCalendar.WEEK_OF_YEAR);
+ int month = cal.get(GregorianCalendar.MONTH);
+
+ if (timeString != null && !timeString.isEmpty())
+ {
+ String[] time = fConfig.getTime().split(":");
+ hour = Integer.parseInt(time[0]);
+ minute = Integer.parseInt(time[1]);
+ }
+
+ if (fConfig.getDay() > 0)
+ {
+ day = fConfig.getDay();
+ }
+
+ // TODO: short months, invalid times and days
+
+ switch (fConfig.getInterval())
+ {
+ // next hour, specified or same minute
+ case HOURLY:
+ cal.set(GregorianCalendar.MINUTE, minute);
+ cal.set(GregorianCalendar.HOUR_OF_DAY, hour + 1);
+ fNextSync = cal.getTimeInMillis();
+ fWaitTime = fNextSync - System.currentTimeMillis();
+ break;
+
+ // next day, specified or same time
+ case DAILY:
+ cal.set(GregorianCalendar.MINUTE, minute);
+ cal.set(GregorianCalendar.HOUR_OF_DAY, hour);
+ cal.set(GregorianCalendar.DAY_OF_YEAR, day + 1);
+ fNextSync = cal.getTimeInMillis();
+ fWaitTime = fNextSync - System.currentTimeMillis();
+ break;
+
+ // next week, specified or same time and day of week
+ case WEEKLY:
+ cal.set(GregorianCalendar.MINUTE, minute);
+ cal.set(GregorianCalendar.HOUR_OF_DAY, hour);
+ cal.set(GregorianCalendar.DAY_OF_WEEK, day);
+ cal.set(GregorianCalendar.WEEK_OF_YEAR, week + 1);
+ fNextSync = cal.getTimeInMillis();
+ fWaitTime = fNextSync - System.currentTimeMillis();
+ break;
+
+ // next month, specified or same time and day of month
+ case MONTHLY:
+ cal.set(GregorianCalendar.MINUTE, minute);
+ cal.set(GregorianCalendar.HOUR_OF_DAY, hour);
+ cal.set(GregorianCalendar.DAY_OF_MONTH, day);
+ cal.set(GregorianCalendar.MONTH, month + 1);
+ fNextSync = cal.getTimeInMillis();
+ fWaitTime = fNextSync - System.currentTimeMillis();
+ break;
+
+ // synchronization can only be triggered manually
+ case MANUAL:
+ fWaitTime = Long.MAX_VALUE;
+ fNextSync = Long.MAX_VALUE;
+ break;
+ }
+ }
+
+ /**
+ * @return whether the scheduler is currently active
+ */
+ public boolean isRunning()
+ {
+ return fRunning;
+ }
+
+ /**
+ * Stops the scheduler if it is running.
+ */
+ public void stop()
+ {
+ fRunning = false;
+
+ synchronized (fTrigger)
+ {
+ fTrigger.notify();
+ }
+ }
+
+ /**
+ * Causes the scheduler to perform an unscheduled synchronization.
+ */
+ public void forceSync()
+ {
+ fForceSync = true;
+
+ synchronized (fTrigger)
+ {
+ fTrigger.notify();
+ }
+ }
+}
diff --git a/src/main/java/de/hofuniversity/iisys/ldapsync/config/CycleTypes.java b/src/main/java/de/hofuniversity/iisys/ldapsync/config/CycleTypes.java
new file mode 100644
index 0000000..91bac0b
--- /dev/null
+++ b/src/main/java/de/hofuniversity/iisys/ldapsync/config/CycleTypes.java
@@ -0,0 +1,12 @@
+package de.hofuniversity.iisys.ldapsync.config;
+
+/**
+ * Enumeration of cycle types for regular sync operations
+ *
+ * @author fholzschuher2
+ *
+ */
+public enum CycleTypes
+{
+ HOURLY, DAILY, WEEKLY, MONTHLY, MANUAL;
+}
diff --git a/src/main/java/de/hofuniversity/iisys/ldapsync/config/SyncConfig.java b/src/main/java/de/hofuniversity/iisys/ldapsync/config/SyncConfig.java
new file mode 100644
index 0000000..c3e0024
--- /dev/null
+++ b/src/main/java/de/hofuniversity/iisys/ldapsync/config/SyncConfig.java
@@ -0,0 +1,253 @@
+package de.hofuniversity.iisys.ldapsync.config;
+
+import java.util.List;
+
+/**
+ * Class containing configuration information for the LDAP synchronization and
+ * manipulation process.
+ *
+ * @author fholzschuher2
+ *
+ */
+public class SyncConfig
+{
+ // access
+ private String fUrl;
+ private String fContext;
+ private String fUser;
+ private char[] fPassword;
+ private boolean fReadOnly = true;
+ private boolean fSubtreeSearch = true;
+
+ // synchronization
+ private boolean fSyncOnStart;
+ private CycleTypes fInterval;
+ private String fTime;
+ private int fDay;
+
+ // initial values
+ private List fInitialClasses;
+ private List fInitialOus;
+
+ // end points
+ private List fEndpoints;
+
+ /**
+ * @return LDAP URL to connect to
+ */
+ public String getUrl()
+ {
+ return fUrl;
+ }
+
+ /**
+ * @return context under which users are stored
+ */
+ public String getContext()
+ {
+ return fContext;
+ }
+
+ /**
+ * @return user name to authenticate with or null
+ */
+ public String getUser()
+ {
+ return fUser;
+ }
+
+ /**
+ * @return password to authenticate with or null
+ */
+ public char[] getPassword()
+ {
+ return fPassword;
+ }
+
+ /**
+ * @return whether to sync with LDAP on startup
+ */
+ public boolean getSyncOnStart()
+ {
+ return fSyncOnStart;
+ }
+
+ /**
+ * @return general synchronization interval
+ */
+ public CycleTypes getInterval()
+ {
+ return fInterval;
+ }
+
+ /**
+ * @return configuration for end points to synchronize
+ */
+ public List getEndpoints()
+ {
+ return fEndpoints;
+ }
+
+ /**
+ * @param url
+ * LDAP URL to connect to
+ */
+ public void setUrl(String url)
+ {
+ fUrl = url;
+ }
+
+ /**
+ * @param context
+ * context under which users are stored
+ */
+ public void setContext(String context)
+ {
+ fContext = context;
+ }
+
+ /**
+ * @param user
+ * user name to authenticate with or null
+ */
+ public void setUser(String user)
+ {
+ fUser = user;
+ }
+
+ /**
+ * @param password
+ * password to authenticate with
+ */
+ public void setPassword(char[] password)
+ {
+ fPassword = password;
+ }
+
+ /**
+ * @param whether
+ * to sync with LDAP on startup
+ */
+ public void setSyncOnStart(boolean syncOnStart)
+ {
+ fSyncOnStart = syncOnStart;
+ }
+
+ /**
+ * @param interval
+ * general synchronization interval
+ */
+ public void setInterval(CycleTypes interval)
+ {
+ fInterval = interval;
+ }
+
+ /**
+ * @return configuration for end points to synchronize
+ */
+ public void setEndpoints(List endpoints)
+ {
+ fEndpoints = endpoints;
+ }
+
+ /**
+ * @return time of the day at which to sync as HH:MM
+ */
+ public String getTime()
+ {
+ return fTime;
+ }
+
+ /**
+ * @return day of the week (Sunday is 1) or month
+ */
+ public int getDay()
+ {
+ return fDay;
+ }
+
+ /**
+ * @param time
+ * time of the day at which to sync as HH:MM
+ */
+ public void setTime(String time)
+ {
+ fTime = time;
+ }
+
+ /**
+ * @param day
+ * day of the week (Sunday is 1) or month
+ */
+ public void setDay(int day)
+ {
+ fDay = day;
+ }
+
+ /**
+ * @return whether the LDAP service should not be written to (default:true)
+ */
+ public boolean getReadOnly()
+ {
+ return fReadOnly;
+ }
+
+ /**
+ * @param fReadOnly
+ * whether the LDAP service should not be written to
+ */
+ public void setReadOnly(boolean readOnly)
+ {
+ fReadOnly = readOnly;
+ }
+
+ /**
+ * @return whether an ldap subtree search for users should be performed
+ */
+ public boolean getSubtreeSearch()
+ {
+ return fSubtreeSearch;
+ }
+
+ /**
+ * @param subtreeSearch whether an ldap subtree search for users should be performed
+ */
+ public void setSubtreeSearch(boolean subtreeSearch)
+ {
+ fSubtreeSearch = subtreeSearch;
+ }
+
+ /**
+ * @return initial object classes for user objects
+ */
+ public List getInitialClasses()
+ {
+ return fInitialClasses;
+ }
+
+ /**
+ * @param initialClasses
+ * initial object classes for user objects
+ */
+ public void setInitialClasses(List initialClasses)
+ {
+ fInitialClasses = initialClasses;
+ }
+
+ /**
+ * @return initial organizational units for user objects
+ */
+ public List getInitialOus()
+ {
+ return fInitialOus;
+ }
+
+ /**
+ * @param initialOus
+ * initial organizational units for user objects
+ */
+ public void setInitialOus(List initialOus)
+ {
+ fInitialOus = initialOus;
+ }
+}
diff --git a/src/main/java/de/hofuniversity/iisys/ldapsync/config/SyncDirections.java b/src/main/java/de/hofuniversity/iisys/ldapsync/config/SyncDirections.java
new file mode 100644
index 0000000..f84c15d
--- /dev/null
+++ b/src/main/java/de/hofuniversity/iisys/ldapsync/config/SyncDirections.java
@@ -0,0 +1,6 @@
+package de.hofuniversity.iisys.ldapsync.config;
+
+public enum SyncDirections
+{
+ FROM_LDAP, TO_LDAP, BOTH;
+}
diff --git a/src/main/java/de/hofuniversity/iisys/ldapsync/config/SyncEndpointConfig.java b/src/main/java/de/hofuniversity/iisys/ldapsync/config/SyncEndpointConfig.java
new file mode 100644
index 0000000..707cce2
--- /dev/null
+++ b/src/main/java/de/hofuniversity/iisys/ldapsync/config/SyncEndpointConfig.java
@@ -0,0 +1,140 @@
+package de.hofuniversity.iisys.ldapsync.config;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Generic configuration that defines how to connect to an end point of a
+ * certain type and the associated synchronization rules.
+ *
+ * @author fholzschuher2
+ *
+ */
+public class SyncEndpointConfig
+{
+ private String fType;
+ private boolean fCreateOwnEntries, fDeleteOwnEntries;
+ private boolean fCreateLdapEntries, fDeleteLdapEntries;
+ private Map fProperties;
+ private List fMapping;
+
+ /**
+ * @return name of the end point type
+ */
+ public String getType()
+ {
+ return fType;
+ }
+
+ /**
+ * @return map of configuration properties for the end point
+ */
+ public Map getProperties()
+ {
+ return fProperties;
+ }
+
+ /**
+ * @return rules that determine how to store attributes
+ */
+ public List getMapping()
+ {
+ return fMapping;
+ }
+
+ /**
+ * @param type
+ * name of the end point type
+ */
+ public void setType(String type)
+ {
+ fType = type;
+ }
+
+ /**
+ * @param properties
+ * map of configuration properties for the end point
+ */
+ public void setProperties(Map properties)
+ {
+ fProperties = properties;
+ }
+
+ /**
+ * @param mapping
+ * rules that determine how to store attributes
+ */
+ public void setMapping(List mapping)
+ {
+ fMapping = mapping;
+ }
+
+ /**
+ * @return whether to create end point entries for LDAP entries
+ */
+ public boolean getCreateOwnEntries()
+ {
+ return fCreateOwnEntries;
+ }
+
+ /**
+ * @param createOwnEntries
+ * whether to create end point entries for LDAP entries
+ */
+ public void setCreateOwnEntries(boolean createOwnEntries)
+ {
+ fCreateOwnEntries = createOwnEntries;
+ }
+
+ /**
+ * @return whether to delete end point entries that are not in LDAP
+ */
+ public boolean getDeleteOwnEntries()
+ {
+ return fDeleteOwnEntries;
+ }
+
+ /**
+ * @param deleteOwnEntries
+ * whether to delete end point entries that are not in LDAP
+ */
+ public void setDeleteOwnEntries(boolean deleteOwnEntries)
+ {
+ fDeleteOwnEntries = deleteOwnEntries;
+ }
+
+ /**
+ * @return whether to delete LDAP entries that have no end point equivalent
+ */
+ public boolean getDeleteLdapEntries()
+ {
+ return fDeleteLdapEntries;
+ }
+
+ /**
+ * @param deleteLdapEntries
+ * whether to delete LDAP entries that have no end point
+ * equivalent
+ */
+ public void setDeleteLdapEntries(boolean deleteLdapEntries)
+ {
+ fDeleteLdapEntries = deleteLdapEntries;
+ }
+
+ /**
+ * @return whether to create LDAP entries for end point entries
+ */
+ public boolean getCreateLdapEntries()
+ {
+ return fCreateLdapEntries;
+ }
+
+ /**
+ * @param fCreateLdapEntries
+ * whether to create LDAP entries for end point entries
+ */
+ public void setCreateLdapEntries(boolean createLdapEntries)
+ {
+ fCreateLdapEntries = createLdapEntries;
+ }
+}
diff --git a/src/main/java/de/hofuniversity/iisys/ldapsync/config/SyncOperations.java b/src/main/java/de/hofuniversity/iisys/ldapsync/config/SyncOperations.java
new file mode 100644
index 0000000..6acc01f
--- /dev/null
+++ b/src/main/java/de/hofuniversity/iisys/ldapsync/config/SyncOperations.java
@@ -0,0 +1,6 @@
+package de.hofuniversity.iisys.ldapsync.config;
+
+public enum SyncOperations
+{
+ COPY, COPY_FIRST_ELEMENT, COPY_IF_NEWER, COPY_IF_NULL, ADD_TO_LIST, COPY_ON_CREATE;
+}
diff --git a/src/main/java/de/hofuniversity/iisys/ldapsync/config/SyncRule.java b/src/main/java/de/hofuniversity/iisys/ldapsync/config/SyncRule.java
new file mode 100644
index 0000000..c4a17f5
--- /dev/null
+++ b/src/main/java/de/hofuniversity/iisys/ldapsync/config/SyncRule.java
@@ -0,0 +1,83 @@
+package de.hofuniversity.iisys.ldapsync.config;
+
+/**
+ * Rule class providing a mapping of a LDAP property to the property of a
+ * certain application and rules on how to synchronize them.
+ *
+ * @author fholzschuher2
+ *
+ */
+public class SyncRule
+{
+ private String fLdapProp, fEndPointProp;
+ private SyncDirections fDirection;
+ private SyncOperations fOperation;
+
+ /**
+ * @return name of the property in LDAP
+ */
+ public String getLdapProp()
+ {
+ return fLdapProp;
+ }
+
+ /**
+ * @return name of the property at the end point
+ */
+ public String getEndPointProp()
+ {
+ return fEndPointProp;
+ }
+
+ /**
+ * @return in which direction to synchronize
+ */
+ public SyncDirections getDirection()
+ {
+ return fDirection;
+ }
+
+ /**
+ * @return how to handle existing values
+ */
+ public SyncOperations getOperation()
+ {
+ return fOperation;
+ }
+
+ /**
+ * @param ldapProp
+ * name of the property in LDAP
+ */
+ public void setLdapProp(String ldapProp)
+ {
+ fLdapProp = ldapProp;
+ }
+
+ /**
+ * @param endPointProp
+ * name of the property at the end point
+ */
+ public void setEndPointProp(String endPointProp)
+ {
+ fEndPointProp = endPointProp;
+ }
+
+ /**
+ * @param direction
+ * in which direction to synchronize
+ */
+ public void setDirection(SyncDirections direction)
+ {
+ fDirection = direction;
+ }
+
+ /**
+ * @param operation
+ * how to handle existing values
+ */
+ public void setOperation(SyncOperations operation)
+ {
+ this.fOperation = operation;
+ }
+}
diff --git a/src/main/java/de/hofuniversity/iisys/ldapsync/config/XMLConfigReader.java b/src/main/java/de/hofuniversity/iisys/ldapsync/config/XMLConfigReader.java
new file mode 100644
index 0000000..c12cf1a
--- /dev/null
+++ b/src/main/java/de/hofuniversity/iisys/ldapsync/config/XMLConfigReader.java
@@ -0,0 +1,506 @@
+package de.hofuniversity.iisys.ldapsync.config;
+
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import javax.xml.stream.XMLEventReader;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.events.XMLEvent;
+
+/**
+ * Reader that reads configuration properties from an XML file.
+ *
+ * @author fholzschuher2
+ *
+ */
+public class XMLConfigReader
+{
+ private static final String CONFIG_ROOT = "ldap_config";
+ private static final String INIT_OCS = "object_classes";
+ private static final String INIT_OUS = "org_units";
+ private static final String ENDPOINT_CONF = "endpoint";
+
+ private static final String CLASS = "class";
+ private static final String UNIT = "unit";
+
+ private static final String URL = "url";
+ private static final String CONTEXT = "context";
+ private static final String USER = "user";
+ private static final String PASSWORD = "password";
+ private static final String READ_ONLY = "read_only";
+ private static final String SUBTREE_SEARCH = "subtree_search";
+
+ private static final String START_SYNC = "sync_on_start";
+ private static final String INTERVAL = "interval";
+ private static final String TIME = "time";
+ private static final String DAY = "day";
+
+ private static final String TYPE = "type";
+ private static final String CREATE_OWN = "create_own_entries";
+ private static final String DELETE_OWN = "delete_own_entries";
+ private static final String CREATE_LDAP = "create_ldap_entries";
+ private static final String DELETE_LDAP = "delete_ldap_entries";
+ private static final String ENDPOINT_PROPS = "properties";
+ private static final String MAPPING = "mapping";
+
+ private static final String RULE = "rule";
+ private static final String LDAP_PROP = "ldap_property";
+ private static final String END_POINT_PROP = "end_point_property";
+ private static final String DIRECTION = "direction";
+ private static final String OPERATION = "operation";
+
+ private final String fPath;
+
+ private SyncConfig fConfig;
+
+ /**
+ * Creates an XML configuration reader that reads the file at the location
+ * specified.
+ *
+ * @param path
+ * path to XML file
+ */
+ public XMLConfigReader(String path)
+ {
+ if (path == null || path.isEmpty())
+ {
+ throw new NullPointerException("no path given");
+ }
+
+ fPath = path;
+ }
+
+ /**
+ * Reads the configuration as configured.
+ *
+ * @return configuration object
+ * @throws Exception
+ * if reading fails
+ */
+ public SyncConfig readConfig() throws Exception
+ {
+ fConfig = new SyncConfig();
+ fConfig.setEndpoints(new ArrayList());
+
+ XMLInputFactory inputFactory = XMLInputFactory.newInstance();
+ InputStream in = new FileInputStream(fPath);
+ final XMLEventReader eReader = inputFactory.createXMLEventReader(in);
+
+ XMLEvent event = null;
+ String tag = null;
+ String value = null;
+
+ while (eReader.hasNext())
+ {
+ event = eReader.nextEvent();
+
+ if (event.isStartElement())
+ {
+ tag = event.asStartElement().getName().getLocalPart();
+
+ if (tag.equals(CONFIG_ROOT))
+ {
+ continue;
+ } else if (tag.equals(ENDPOINT_CONF))
+ {
+ readEndpointConf(eReader);
+ } else if (tag.equals(INIT_OCS))
+ {
+ readObjectClasses(eReader);
+ } else if (tag.equals(INIT_OUS))
+ {
+ readOrgUnits(eReader);
+ } else
+ {
+ event = eReader.nextEvent();
+ value = event.asCharacters().toString();
+
+ setConfigProperty(tag, value);
+ }
+ }
+ if (event.isEndElement())
+ {
+ tag = event.asEndElement().getName().getLocalPart();
+
+ if (tag.equals(CONFIG_ROOT))
+ {
+ break;
+ }
+ }
+ }
+
+ return fConfig;
+ }
+
+ private void setConfigProperty(String name, String value)
+ {
+ if (name.equals(URL))
+ {
+ fConfig.setUrl(value);
+ } else if (name.equals(CONTEXT))
+ {
+ fConfig.setContext(value);
+ } else if (name.equals(USER))
+ {
+ fConfig.setUser(value);
+ } else if (name.equals(PASSWORD))
+ {
+ fConfig.setPassword(value.toCharArray());
+ } else if (name.equals(READ_ONLY))
+ {
+ boolean ro = Boolean.parseBoolean(value);
+ fConfig.setReadOnly(ro);
+ } else if (name.equals(SUBTREE_SEARCH))
+ {
+ boolean sts = Boolean.parseBoolean(value);
+ fConfig.setSubtreeSearch(sts);
+ } else if (name.equals(START_SYNC))
+ {
+ boolean sync = Boolean.parseBoolean(value);
+ fConfig.setSyncOnStart(sync);
+ } else if (name.equals(INTERVAL))
+ {
+ CycleTypes cycle = CycleTypes.valueOf(value);
+ fConfig.setInterval(cycle);
+ } else if (name.equals(TIME))
+ {
+ fConfig.setTime(value);
+ } else if (name.equals(DAY))
+ {
+ fConfig.setDay(Integer.parseInt(value));
+ } else
+ {
+ System.out.println("unknown main config property: " + name);
+ }
+ }
+
+ private void readObjectClasses(final XMLEventReader eReader)
+ throws Exception
+ {
+ List classes = new ArrayList();
+
+ XMLEvent event = null;
+ String tag = null;
+ String value = null;
+
+ while (eReader.hasNext())
+ {
+ event = eReader.nextEvent();
+
+ if (event.isStartElement())
+ {
+ tag = event.asStartElement().getName().getLocalPart();
+
+ if (tag.equals(CLASS))
+ {
+ event = eReader.nextEvent();
+ value = event.asCharacters().toString();
+ classes.add(value);
+ }
+ }
+ if (event.isEndElement())
+ {
+ tag = event.asEndElement().getName().getLocalPart();
+
+ if (tag.equals(INIT_OCS))
+ {
+ break;
+ }
+ }
+ }
+
+ fConfig.setInitialClasses(classes);
+ }
+
+ private void readOrgUnits(final XMLEventReader eReader) throws Exception
+ {
+ List orgUnits = new ArrayList();
+
+ XMLEvent event = null;
+ String tag = null;
+ String value = null;
+
+ while (eReader.hasNext())
+ {
+ event = eReader.nextEvent();
+
+ if (event.isStartElement())
+ {
+ tag = event.asStartElement().getName().getLocalPart();
+
+ if (tag.equals(UNIT))
+ {
+ event = eReader.nextEvent();
+ value = event.asCharacters().toString();
+ orgUnits.add(value);
+ }
+ }
+ if (event.isEndElement())
+ {
+ tag = event.asEndElement().getName().getLocalPart();
+
+ if (tag.equals(INIT_OUS))
+ {
+ break;
+ }
+ }
+ }
+
+ fConfig.setInitialOus(orgUnits);
+ }
+
+ private void readEndpointConf(final XMLEventReader eReader)
+ throws Exception
+ {
+ SyncEndpointConfig config = new SyncEndpointConfig();
+
+ XMLEvent event = null;
+ String tag = null;
+ String value = null;
+
+ Map props = null;
+ List rules = null;
+
+ while (eReader.hasNext())
+ {
+ event = eReader.nextEvent();
+
+ if (event.isStartElement())
+ {
+ tag = event.asStartElement().getName().getLocalPart();
+
+ if (tag.equals(ENDPOINT_PROPS))
+ {
+ props = readEndpointProperties(eReader);
+ config.setProperties(props);
+ } else if (tag.equals(MAPPING))
+ {
+ rules = readEndpointRules(eReader);
+ config.setMapping(rules);
+ } else
+ {
+ event = eReader.nextEvent();
+ value = event.asCharacters().toString();
+
+ setEndPointProperty(config, tag, value);
+ }
+ }
+ if (event.isEndElement())
+ {
+ tag = event.asEndElement().getName().getLocalPart();
+
+ if (tag.equals(ENDPOINT_CONF))
+ {
+ break;
+ }
+ }
+ }
+
+ fConfig.getEndpoints().add(config);
+ }
+
+ private Map readEndpointProperties(
+ final XMLEventReader eReader) throws Exception
+ {
+ Map properties = new HashMap();
+
+ XMLEvent event = null;
+ String tag = null;
+ String value = null;
+
+ while (eReader.hasNext())
+ {
+ event = eReader.nextEvent();
+
+ if (event.isStartElement())
+ {
+ tag = event.asStartElement().getName().getLocalPart();
+
+ event = eReader.nextEvent();
+ value = event.asCharacters().toString();
+
+ properties.put(tag, value);
+ }
+ if (event.isEndElement())
+ {
+ tag = event.asEndElement().getName().getLocalPart();
+
+ if (tag.equals(ENDPOINT_PROPS))
+ {
+ break;
+ }
+ }
+ }
+
+ return properties;
+ }
+
+ private List readEndpointRules(final XMLEventReader eReader)
+ throws Exception
+ {
+ List rules = new ArrayList();
+
+ XMLEvent event = null;
+ String tag = null;
+ SyncRule rule = null;
+
+ while (eReader.hasNext())
+ {
+ event = eReader.nextEvent();
+
+ if (event.isStartElement())
+ {
+ tag = event.asStartElement().getName().getLocalPart();
+
+ if (tag.equals(RULE))
+ {
+ rule = readRule(eReader);
+ rules.add(rule);
+ }
+ }
+ if (event.isEndElement())
+ {
+ tag = event.asEndElement().getName().getLocalPart();
+
+ if (tag.equals(MAPPING))
+ {
+ break;
+ }
+ }
+ }
+
+ return rules;
+ }
+
+ private SyncRule readRule(final XMLEventReader eReader) throws Exception
+ {
+ SyncRule rule = new SyncRule();
+
+ XMLEvent event = null;
+ String tag = null;
+ String value = null;
+
+ while (eReader.hasNext())
+ {
+ event = eReader.nextEvent();
+
+ if (event.isStartElement())
+ {
+ tag = event.asStartElement().getName().getLocalPart();
+
+ event = eReader.nextEvent();
+ value = event.asCharacters().toString();
+
+ if (tag.equals(LDAP_PROP))
+ {
+ rule.setLdapProp(value);
+ } else if (tag.equals(END_POINT_PROP))
+ {
+ rule.setEndPointProp(value);
+ } else if (tag.equals(DIRECTION))
+ {
+ rule.setDirection(SyncDirections.valueOf(value));
+ } else if (tag.equals(OPERATION))
+ {
+ rule.setOperation(SyncOperations.valueOf(value));
+ } else
+ {
+ System.out.println("unknown rule property: " + tag);
+ }
+ }
+ if (event.isEndElement())
+ {
+ tag = event.asEndElement().getName().getLocalPart();
+
+ if (tag.equals(RULE))
+ {
+ break;
+ }
+ }
+ }
+
+ return rule;
+ }
+
+ private void setEndPointProperty(SyncEndpointConfig config, String name,
+ String value)
+ {
+ if (name.equals(TYPE))
+ {
+ config.setType(value);
+ } else if (name.equals(CREATE_OWN))
+ {
+ boolean create = Boolean.parseBoolean(value);
+ config.setCreateOwnEntries(create);
+ } else if (name.equals(CREATE_LDAP))
+ {
+ boolean create = Boolean.parseBoolean(value);
+ config.setCreateLdapEntries(create);
+ } else if (name.equals(DELETE_OWN))
+ {
+ boolean delete = Boolean.parseBoolean(value);
+ config.setDeleteOwnEntries(delete);
+ } else if (name.equals(DELETE_LDAP))
+ {
+ boolean delete = Boolean.parseBoolean(value);
+ config.setDeleteLdapEntries(delete);
+ } else
+ {
+ System.out.println("unknown end point property: " + name);
+ }
+ }
+
+ public static void main(String[] args)
+ {
+ XMLConfigReader reader = new XMLConfigReader("ldapConfig.xml");
+
+ try
+ {
+ SyncConfig config = reader.readConfig();
+ System.out.println("url: " + config.getUrl());
+ System.out.println("context: " + config.getContext());
+ System.out.println("read-only: " + config.getReadOnly());
+ System.out.println("sync on start: " + config.getSyncOnStart());
+ System.out.println("cycle: " + config.getInterval());
+
+ for (String clazz : config.getInitialClasses())
+ {
+ System.out.println("initial class: " + clazz);
+ }
+
+ for (String unit : config.getInitialOus())
+ {
+ System.out.println("initial ou: " + unit);
+ }
+
+ for (SyncEndpointConfig sec : config.getEndpoints())
+ {
+ System.out.println(sec.getType());
+
+ for (Entry propE : sec.getProperties()
+ .entrySet())
+ {
+ System.out.println("property: " + propE.getKey() + ": "
+ + propE.getValue());
+ }
+
+ for (SyncRule rule : sec.getMapping())
+ {
+ System.out.println("rule:");
+ System.out.println("ldap: " + rule.getLdapProp());
+ System.out.println("own: " + rule.getEndPointProp());
+ System.out.println("direction: " + rule.getDirection());
+ System.out.println("operation: " + rule.getOperation());
+ }
+ }
+ } catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+}
diff --git a/src/main/java/de/hofuniversity/iisys/ldapsync/endpoints/ASyncEndpoint.java b/src/main/java/de/hofuniversity/iisys/ldapsync/endpoints/ASyncEndpoint.java
new file mode 100644
index 0000000..f154f9c
--- /dev/null
+++ b/src/main/java/de/hofuniversity/iisys/ldapsync/endpoints/ASyncEndpoint.java
@@ -0,0 +1,493 @@
+package de.hofuniversity.iisys.ldapsync.endpoints;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import javax.naming.NamingEnumeration;
+import javax.naming.directory.Attribute;
+
+import de.hofuniversity.iisys.ldapsync.LdapBuffer;
+import de.hofuniversity.iisys.ldapsync.config.SyncEndpointConfig;
+import de.hofuniversity.iisys.ldapsync.config.SyncRule;
+import de.hofuniversity.iisys.ldapsync.model.ILdapUser;
+
+/**
+ * Abstract implementation of an end point providing predefined methods for
+ * common synchronization steps and operations. Here, the implementations are
+ * responsible for monitoring which values have actually changed. The sequence
+ * is: create LDAP users, delete end point users, create end point users, delete
+ * LDAP users and rules in the order they were specified.
+ *
+ * @author fholzschuher2
+ *
+ */
+public abstract class ASyncEndpoint implements ISyncEndpoint
+{
+ private final LdapBuffer fLdap;
+ private final List fRules;
+
+ private final boolean fCreateOwn;
+ private final boolean fCreateLdap;
+ private final boolean fDeleteOwn;
+ private final boolean fDeleteLdap;
+
+ private final Set fCreatedUsers;
+
+ private Set fOwnUsers;
+ private Map fLdapUsers;
+
+ /**
+ * Creates an abstract end point, executing a configurable standard
+ * synchronization procedure. Throws a NullPointerException if any
+ * parameters or the set of rules are null.
+ *
+ * @param ldap
+ * LDAP connector to use
+ * @param config
+ * configuration object to use
+ */
+ public ASyncEndpoint(LdapBuffer ldap, SyncEndpointConfig config)
+ {
+ if (ldap == null)
+ {
+ throw new NullPointerException("ldap buffer was null");
+ }
+ if (config == null)
+ {
+ throw new NullPointerException("configuration object was null");
+ }
+
+ fLdap = ldap;
+ fRules = config.getMapping();
+
+ fCreateOwn = config.getCreateOwnEntries();
+ fCreateLdap = config.getCreateLdapEntries();
+ fDeleteOwn = config.getDeleteOwnEntries();
+ fDeleteLdap = config.getDeleteLdapEntries();
+
+ fCreatedUsers = new HashSet();
+ }
+
+ // methods to implement
+
+ /**
+ * Gets all values of a certain attribute belonging to a certain user. None
+ * of the parameters may be null. Returns null if there is no such user or
+ * attribute.
+ *
+ * @param name
+ * name of the user the attribute belongs to
+ * @param att
+ * name of the attribute to get
+ * @return list of attribute values or null
+ */
+ protected abstract List getValues(String name, String att);
+
+ /**
+ * Sets an attribute belonging to a certain user to a certain value. None of
+ * the parameters may be null.
+ *
+ * @param name
+ * name of the user the attribute belongs to
+ * @param att
+ * name of the attribute to set
+ * @param val
+ * value to set as the attribute's value
+ */
+ protected abstract void setAttribute(String name, String att, Object val);
+
+ /**
+ * Sets multiple values for an attribute of a certain user. None of the
+ * attributes may be null.
+ *
+ * @param name
+ * name of the user the attribute belongs to
+ * @param att
+ * name of the attribute to set
+ * @param vals
+ * list of values for the attribute
+ */
+ protected abstract void setAttribute(String name, String att,
+ List vals);
+
+ /**
+ * Adds a list of attributes to an attribute of a certain user, without
+ * deleting old values. If the attribute does not exist it should be
+ * created. None of the parameters may be null.
+ *
+ * @param name
+ * name of the user the attribute belongs to
+ * @param att
+ * attribute to add values to
+ * @param vals
+ * values to add to the attribute
+ */
+ protected abstract void
+ addValues(String name, String att, List vals);
+
+ /**
+ * Removes an attribute from a certain user. Calls with non-existent
+ * attributes attributes should be ignored. None of the parameters may be
+ * null.
+ *
+ * @param name
+ * name of the user the attribute belongs to
+ * @param att
+ * name of the attribute to remove
+ */
+ protected abstract void removeAttribute(String name, String att);
+
+ /**
+ * Retrieves a set of all users that exist at the end point.
+ *
+ * @return set of all known users
+ */
+ protected abstract Set getUserNames();
+
+ /**
+ * Creates a new user at the end point based on the given LDAP user.
+ * Parameter may not be null.
+ *
+ * @param user
+ * LDAP user to base the user on
+ */
+ protected abstract void createUser(ILdapUser user);
+
+ /**
+ * Deletes a user at the end point. Parameter may not be null.
+ *
+ * @param name
+ * name of the user to delete
+ */
+ protected abstract void deleteUser(String name);
+
+ /**
+ * Hook to execute before all other operations. Can be left blank.
+ */
+ protected abstract void preHook();
+
+ /**
+ * Hook to execute after all other operations. Can be left blank.
+ */
+ protected abstract void postHook();
+
+ // default synchronization routine
+
+ public void sync()
+ {
+ preHook();
+
+ fOwnUsers = getUserNames();
+ fLdapUsers = fLdap.getAllUsers();
+
+ // create users in LDAP that only exist for the end point
+ if (fCreateLdap)
+ {
+ for (String name : fOwnUsers)
+ {
+ if (!fLdapUsers.containsKey(name))
+ {
+ fLdap.createUser(name);
+ }
+ }
+ }
+
+ // delete users at the end point that don't exist in LDAP
+ if (fDeleteOwn)
+ {
+ for (String name : fOwnUsers)
+ {
+ if (!fLdapUsers.containsKey(name))
+ {
+ deleteUser(name);
+ }
+ }
+ }
+
+ // get a fresh list of users
+ fOwnUsers = getUserNames();
+
+ // create users that only exist in LDAP
+ if (fCreateOwn)
+ {
+ for (Entry userE : fLdapUsers.entrySet())
+ {
+ if (!fOwnUsers.contains(userE.getKey()))
+ {
+ createUser(userE.getValue());
+ fCreatedUsers.add(userE.getKey());
+ }
+ }
+ }
+
+ // get a fresh list of users
+ fOwnUsers = getUserNames();
+
+ // delete users in LDAP that only exist at the end point
+ if (fDeleteLdap)
+ {
+ Set toDelete = new HashSet();
+
+ for (Entry userE : fLdapUsers.entrySet())
+ {
+ if (!fOwnUsers.contains(userE.getKey()))
+ {
+ toDelete.add(userE.getKey());
+ }
+ }
+
+ for (String name : toDelete)
+ {
+ fLdap.deleteUser(name);
+ }
+ }
+
+ handleRules();
+
+ fCreatedUsers.clear();
+
+ postHook();
+ }
+
+ private void handleRules()
+ {
+ for (SyncRule rule : fRules)
+ {
+ for (String user : fOwnUsers)
+ {
+ switch (rule.getDirection())
+ {
+ case TO_LDAP:
+ handleToLdap(user, rule);
+ break;
+
+ case FROM_LDAP:
+ handleFromLdap(user, rule);
+ break;
+
+ case BOTH:
+ handleBoth(user, rule);
+ break;
+ }
+ }
+ }
+ }
+
+ private void handleToLdap(String name, SyncRule rule)
+ {
+ String ldapAtt = rule.getLdapProp();
+ List values = getValues(name, rule.getEndPointProp());
+ if (values == null || values.isEmpty())
+ {
+ // break if not found or not set
+ return;
+ }
+
+ switch (rule.getOperation())
+ {
+ case ADD_TO_LIST:
+ fLdap.addToAttribute(name, ldapAtt, values);
+ break;
+
+ case COPY:
+ fLdap.setAttribute(name, ldapAtt, values);
+ break;
+
+ case COPY_FIRST_ELEMENT:
+ fLdap.setAttribute(name, ldapAtt, values.get(0));
+ break;
+
+ case COPY_IF_NEWER:
+ // TODO: how?
+ break;
+
+ case COPY_IF_NULL:
+ Attribute att = fLdap.getCurrentAttribute(name, ldapAtt);
+ boolean empty = true;
+ try
+ {
+ if (att != null && att.get() != null
+ && !att.get().toString().isEmpty())
+ {
+ empty = false;
+ }
+ } catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ if (empty)
+ {
+ fLdap.setAttribute(name, ldapAtt, values);
+ }
+ break;
+
+ case COPY_ON_CREATE:
+ if (fLdap.getUser(name).isNew())
+ {
+ fLdap.setAttribute(name, ldapAtt, values);
+ }
+ break;
+ }
+ }
+
+ private void handleFromLdap(String name, SyncRule rule)
+ {
+ String ownAtt = rule.getEndPointProp();
+ Attribute att = fLdap.getCurrentAttribute(name, rule.getLdapProp());
+ if (att == null)
+ {
+ // break if not found
+ return;
+ }
+
+ List ldapValues = new ArrayList();
+ try
+ {
+ NamingEnumeration> valEnum = att.getAll();
+ while (valEnum.hasMore())
+ {
+ ldapValues.add(valEnum.next());
+ }
+ } catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+
+ switch (rule.getOperation())
+ {
+ case ADD_TO_LIST:
+ addValues(name, ownAtt, ldapValues);
+ break;
+
+ case COPY:
+ if (ldapValues.size() > 1)
+ {
+ setAttribute(name, ownAtt, ldapValues);
+ } else
+ {
+ setAttribute(name, ownAtt, ldapValues.get(0));
+ }
+ break;
+
+ case COPY_FIRST_ELEMENT:
+ setAttribute(name, ownAtt, ldapValues.get(0));
+ break;
+
+ case COPY_IF_NEWER:
+ // TODO: how?
+ break;
+
+ case COPY_IF_NULL:
+ List ownValues = getValues(name, ownAtt);
+ if (ownValues == null || ownValues.isEmpty())
+ {
+ if (ldapValues.size() > 1)
+ {
+ setAttribute(name, ownAtt, ldapValues);
+ } else
+ {
+ setAttribute(name, ownAtt, ldapValues.get(0));
+ }
+ }
+ break;
+
+ case COPY_ON_CREATE:
+ if (fCreatedUsers.contains(name))
+ {
+ if (ldapValues.size() > 1)
+ {
+ setAttribute(name, ownAtt, ldapValues);
+ } else
+ {
+ setAttribute(name, ownAtt, ldapValues.get(0));
+ }
+ }
+ break;
+ }
+ }
+
+ private void handleBoth(String name, SyncRule rule)
+ {
+ String ldapAtt = rule.getLdapProp();
+ String ownAtt = rule.getEndPointProp();
+ List ownValues = getValues(name, ownAtt);
+ Attribute att = fLdap.getCurrentAttribute(name, ldapAtt);
+
+ List ldapValues = new ArrayList();
+ if (att != null)
+ {
+ try
+ {
+ NamingEnumeration> valEnum = att.getAll();
+ while (valEnum.hasMore())
+ {
+ ldapValues.add(valEnum.next());
+ }
+ } catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+
+ switch (rule.getOperation())
+ {
+ case ADD_TO_LIST:
+ // TODO: merging strategy?
+ break;
+
+ case COPY_IF_NEWER:
+ // TODO: how?
+ break;
+
+ case COPY_IF_NULL:
+ boolean ldapNull = false;
+ boolean ownNull = false;
+
+ if (ldapValues.isEmpty())
+ {
+ ldapNull = true;
+ }
+ if (ownValues == null || ownValues.isEmpty())
+ {
+ ownNull = true;
+ }
+
+ if (ldapNull && !ownNull)
+ {
+ fLdap.setAttribute(name, ldapAtt, ownValues);
+ } else if (ownNull && !ldapNull)
+ {
+ setAttribute(name, ownAtt, ldapValues);
+ }
+ break;
+ case COPY_ON_CREATE:
+ /*
+ * check which side the user was created on and copy from the
+ * other side
+ */
+ if (fCreatedUsers.contains(name))
+ {
+ // user created at end point
+ if (ldapValues.size() > 1)
+ {
+ setAttribute(name, ownAtt, ldapValues);
+ } else
+ {
+ setAttribute(name, ownAtt, ldapValues.get(0));
+ }
+ }
+
+ ILdapUser ldapUser = fLdap.getUser(name);
+ if (ldapUser != null && ldapUser.isNew())
+ {
+ // user created in LDAP
+ fLdap.setAttribute(name, ldapAtt, ownValues);
+ }
+ break;
+ }
+ }
+}
diff --git a/src/main/java/de/hofuniversity/iisys/ldapsync/endpoints/ISyncEndpoint.java b/src/main/java/de/hofuniversity/iisys/ldapsync/endpoints/ISyncEndpoint.java
new file mode 100644
index 0000000..922159c
--- /dev/null
+++ b/src/main/java/de/hofuniversity/iisys/ldapsync/endpoints/ISyncEndpoint.java
@@ -0,0 +1,23 @@
+package de.hofuniversity.iisys.ldapsync.endpoints;
+
+/**
+ * Interface for a synchronization end point providing the functionality to
+ * extract and set people's properties in another application based on LDAP in-
+ * and output.
+ *
+ * @author fholzschuher2
+ *
+ */
+public interface ISyncEndpoint
+{
+ /**
+ * Tells the end point to synchronize with the LDAP service, based on the
+ * current state of the buffer. If any connections are needed, they should
+ * be established when called and discarded afterwards as there can be a
+ * long time span between calls.
+ *
+ * @param ldapContents
+ * result from the latest full query
+ */
+ public void sync();
+}
diff --git a/src/main/java/de/hofuniversity/iisys/ldapsync/endpoints/RaveEndpoint.java b/src/main/java/de/hofuniversity/iisys/ldapsync/endpoints/RaveEndpoint.java
new file mode 100644
index 0000000..23d0f56
--- /dev/null
+++ b/src/main/java/de/hofuniversity/iisys/ldapsync/endpoints/RaveEndpoint.java
@@ -0,0 +1,151 @@
+package de.hofuniversity.iisys.ldapsync.endpoints;
+
+import java.util.List;
+import java.util.Set;
+
+import de.hofuniversity.iisys.ldapsync.LdapBuffer;
+import de.hofuniversity.iisys.ldapsync.config.SyncEndpointConfig;
+import de.hofuniversity.iisys.ldapsync.config.SyncRule;
+import de.hofuniversity.iisys.ldapsync.model.ILdapUser;
+
+/**
+ * End point implementation for Apache Rave's default back-end.
+ *
+ * @author fholzschuher2
+ *
+ */
+public class RaveEndpoint extends ASyncEndpoint
+{
+ // TODO: not working ... why?
+ private static final String USER_API = "api/rpc/users/";
+
+ private static final String HOST = "host";
+ private static final String USER = "user";
+ private static final String PASSWORD = "password";
+
+ private final LdapBuffer fBuffer;
+ private final List fRules;
+
+ private final String fHost, fUser, fPassword;
+
+ /**
+ * Creates an end point connecting to Apache Rave specified by the given
+ * configuration object. Throws a NullPointerException if any parameter or
+ * needed property is null.
+ *
+ * @param config
+ * configuration object to use
+ * @param buffer
+ * LDAP buffer to use
+ */
+ public RaveEndpoint(SyncEndpointConfig config, LdapBuffer buffer)
+ {
+ super(buffer, config);
+
+ if (config == null)
+ {
+ throw new NullPointerException("configuration was null");
+ }
+ if (config.getMapping() == null)
+ {
+ throw new NullPointerException("synchronization rules were null");
+ }
+ if (buffer == null)
+ {
+ throw new NullPointerException("ldap buffer was null");
+ }
+
+ fBuffer = buffer;
+ fRules = config.getMapping();
+
+ fHost = config.getProperties().get(HOST);
+ if (fHost == null || fHost.isEmpty())
+ {
+ throw new NullPointerException("host to connect to was null");
+ }
+
+ fUser = config.getProperties().get(USER);
+ if (fUser == null || fUser.isEmpty())
+ {
+ throw new NullPointerException("user ID to use was null");
+ }
+
+ fPassword = config.getProperties().get(PASSWORD);
+ if (fPassword == null || fPassword.isEmpty())
+ {
+ throw new NullPointerException("password to use was null");
+ }
+ }
+
+ @Override
+ protected void preHook()
+ {
+ // TODO: establish connection
+
+ // TODO Auto-generated method stub
+ }
+
+ @Override
+ protected void postHook()
+ {
+ // TODO Auto-generated method stub
+
+ // TODO: disconnect
+ }
+
+ @Override
+ protected List getValues(String name, String att)
+ {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ protected void setAttribute(String name, String att, Object val)
+ {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ protected void setAttribute(String name, String att, List vals)
+ {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ protected void addValues(String name, String att, List vals)
+ {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ protected void removeAttribute(String name, String att)
+ {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ protected Set getUserNames()
+ {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ protected void createUser(ILdapUser user)
+ {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ protected void deleteUser(String name)
+ {
+ // TODO Auto-generated method stub
+
+ }
+}
diff --git a/src/main/java/de/hofuniversity/iisys/ldapsync/endpoints/ShindigGraphEndpoint.java b/src/main/java/de/hofuniversity/iisys/ldapsync/endpoints/ShindigGraphEndpoint.java
new file mode 100644
index 0000000..74b3dee
--- /dev/null
+++ b/src/main/java/de/hofuniversity/iisys/ldapsync/endpoints/ShindigGraphEndpoint.java
@@ -0,0 +1,598 @@
+package de.hofuniversity.iisys.ldapsync.endpoints;
+
+import java.io.BufferedOutputStream;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import de.hofuniversity.iisys.ldapsync.LdapBuffer;
+import de.hofuniversity.iisys.ldapsync.config.SyncEndpointConfig;
+import de.hofuniversity.iisys.ldapsync.model.ILdapUser;
+import de.hofuniversity.iisys.ldapsync.util.JsonObject;
+
+/**
+ * End point implementation for the Apache Shindig graph back-end.
+ *
+ * @author fholzschuher2
+ *
+ */
+public class ShindigGraphEndpoint extends ASyncEndpoint
+{
+ private static final String ID_ATT = "id";
+
+ private static final String HOST = "host";
+ private static final String USER_ID = "user";
+ private static final String FIELDS = "fields";
+
+
+
+ private static final String PIC_FOLDER = "pic-folder";
+ private static final String PIC_URL = "pic-url";
+
+ //special LDAP attributes and shindig properties
+ private static final String THUMB_ATTR = "thumbnail";
+ private static final String THUMB_PROP = "thumbnailUrl";
+
+ private static final String MAIL_PROP = "emails";
+ private static final String PHONE_PROP = "phoneNumbers";
+
+ private static final String ORG_ATTR = "organization";
+ private static final String ORG_PROP = "organizations";
+ private static final String ORG_NAME_PROP = "name";
+
+ private static final String ORG_LOCATION_ATTR = "org_location";
+ private static final String ORG_LOCATION_PROP = "location";
+
+ private static final String ORG_SITE_ATTR = "org_site";
+ private static final String ORG_SITE_PROP = "site";
+
+ private static final String JOB_TITLE_ATTR = "job_title";
+ private static final String JOB_TITLE_PROP = "title";
+
+ //extended model fields
+ private static final String MANAGER_ID_PROP = "managerId";
+ private static final String SECRETARY_ID_PROP = "secretaryId";
+ private static final String DEPARTMENT_PROP = "department";
+ private static final String DEPARTMENT_HEAD_PROP = "departmentHead";
+ private static final String ORG_UNIT_PROP = "orgUnit";
+
+
+ private static final String ALL_FRAGMENT = "rpc?method=user.getAll";
+ private static final String COUNT_FRAGMENT = "&count=0";
+ private static final String FIELDS_FRAGMENT = "&fields=";
+
+ private static final String CREATE_METHOD = "user.create";
+ private static final String UPDATE_METHOD = "people.update";
+ private static final String DELETE_METHOD = "user.delete";
+
+ private final String fHost, fUserId, fFields;
+ private final String fPicFolder, fPicUrl;
+
+ private final Map fUsers;
+ private final Set fUserNames, fCreatedUsers, fDeletedUsers;
+ private final Set fChangedUsers;
+
+ /**
+ * Creates an end point connecting to the shindig graph back-end specified
+ * by the given configuration object. Throws a NullPointerException if any
+ * parameter or needed property is null.
+ *
+ * @param config
+ * configuration object to use
+ * @param buffer
+ * LDAP buffer to use
+ */
+ public ShindigGraphEndpoint(SyncEndpointConfig config, LdapBuffer buffer)
+ {
+ super(buffer, config);
+
+ if (config == null)
+ {
+ throw new NullPointerException("configuration was null");
+ }
+ if (config.getMapping() == null)
+ {
+ throw new NullPointerException("synchronization rules were null");
+ }
+ if (buffer == null)
+ {
+ throw new NullPointerException("ldap buffer was null");
+ }
+
+ fHost = config.getProperties().get(HOST);
+ if (fHost == null || fHost.isEmpty())
+ {
+ throw new NullPointerException("host to connect to was null");
+ }
+
+ fUserId = config.getProperties().get(USER_ID);
+ if (fUserId == null || fUserId.isEmpty())
+ {
+ throw new NullPointerException("user ID to use was null");
+ }
+
+ fFields = config.getProperties().get(FIELDS);
+ if (fFields == null || fFields.isEmpty())
+ {
+ throw new NullPointerException("fields to fetch were null");
+ }
+
+ fPicFolder = config.getProperties().get(PIC_FOLDER);
+ fPicUrl = config.getProperties().get(PIC_URL);
+
+ fUsers = new HashMap();
+ fUserNames = new HashSet();
+ fCreatedUsers = new HashSet();
+ fDeletedUsers = new HashSet();
+ fChangedUsers = new HashSet();
+ }
+
+ @Override
+ protected void preHook()
+ {
+ // establish connection, read all users
+ String result = "";
+ try
+ {
+ URL shindigUrl = new URL(fHost + ALL_FRAGMENT + COUNT_FRAGMENT
+ + FIELDS_FRAGMENT + fFields);
+ HttpURLConnection connection = (HttpURLConnection) shindigUrl
+ .openConnection();
+
+ BufferedReader reader = new BufferedReader(new InputStreamReader(
+ connection.getInputStream()));
+
+ String line = reader.readLine();
+ while (line != null)
+ {
+ result += line;
+ line = reader.readLine();
+ }
+
+ reader.close();
+ } catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+
+ // build a set of available users
+ int start = result.indexOf("list") + 7;
+ int stop = start - 2;
+ final int lastStop = result.lastIndexOf("}],");
+ String objString = null;
+ JsonObject person = null;
+ String name = null;
+ do
+ {
+ stop = JsonObject.getCloseBracketIndex(result, start);
+
+ if (stop + 1 < result.length())
+ {
+ objString = result.substring(start, stop + 1);
+
+ person = new JsonObject(objString);
+ name = person.getSingleAttribute(ID_ATT);
+ fUserNames.add(name);
+ fUsers.put(name, person);
+ }
+
+ start = stop + 2;
+ } while (stop < lastStop);
+ }
+
+ @Override
+ protected void postHook()
+ {
+ if (!fChangedUsers.isEmpty() || !fDeletedUsers.isEmpty())
+ {
+ writeChanges();
+ }
+
+ // clear
+ fUsers.clear();
+ fUserNames.clear();
+ fChangedUsers.clear();
+ fCreatedUsers.clear();
+ fDeletedUsers.clear();
+ }
+
+ private void writeChanges()
+ {
+ String method = null;
+ final StringBuffer buffer = new StringBuffer("[");
+
+ // collect changes into JSON RPC call batch
+ String id = null;
+ String jsonPerson = null;
+
+ //create new users with only IDs (so that links can be created)
+ for(String userId : fCreatedUsers)
+ {
+ JsonObject user = new JsonObject();
+ user.setSingleAttribute("id", userId);
+ method = CREATE_METHOD;
+ jsonPerson = user.toString();
+
+ buffer.append("{\"method\":\"" + method + "\",\"id\":\"" + userId);
+ buffer.append("\",\"params\":{\"userId\":\"" + userId + "\",");
+ buffer.append("\"person\":" + jsonPerson + "}},");
+ }
+
+ //update changed users (including new)
+ for (JsonObject user : fChangedUsers)
+ {
+ id = user.getSingleAttribute(ID_ATT);
+ jsonPerson = user.toString();
+
+ method = UPDATE_METHOD;
+
+ buffer.append("{\"method\":\"" + method + "\",\"id\":\"" + id);
+ buffer.append("\",\"params\":{\"userId\":\"" + id + "\",");
+ buffer.append("\"person\":" + jsonPerson + "}},");
+ }
+
+ // queue deletion requests
+ for (String name : fDeletedUsers)
+ {
+ buffer.append("{\"method\":\"" + DELETE_METHOD + "\",\"id\":\""
+ + name);
+ buffer.append("\",\"params\":{\"userId\":\"" + name + "\"}},");
+ }
+
+ buffer.setCharAt(buffer.length() - 1, ']');
+
+ // open connection and send batch
+ try
+ {
+ URL shindigUrl = new URL(fHost + "rpc");
+ HttpURLConnection connection = (HttpURLConnection) shindigUrl
+ .openConnection();
+
+ connection.setRequestMethod("POST");
+ connection.setDoInput(true);
+ connection.setDoOutput(true);
+ connection.setUseCaches(false);
+ connection.setRequestProperty("Content-Type", "application/json");
+ connection.setRequestProperty("Content-Length",
+ String.valueOf(buffer.length()));
+
+ OutputStreamWriter writer = new OutputStreamWriter(
+ connection.getOutputStream());
+ writer.write(buffer.toString());
+ writer.flush();
+
+ BufferedReader reader = new BufferedReader(new InputStreamReader(
+ connection.getInputStream()));
+
+ String line = reader.readLine();
+ while (line != null)
+ {
+ line = reader.readLine();
+ }
+
+ reader.close();
+ } catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ protected List getValues(String name, String att)
+ {
+ List values = null;
+
+ JsonObject user = fUsers.get(name);
+ if (user != null)
+ {
+ String value = user.getSingleAttribute(att);
+
+ if (value != null)
+ {
+ values = new ArrayList();
+ values.add(value);
+ } else
+ {
+ List valList = user.getListAttribute(att);
+
+ if (valList != null)
+ {
+ values = new ArrayList(valList);
+ }
+ }
+ }
+
+ return values;
+ }
+
+ @Override
+ protected void setAttribute(String name, String att, Object val)
+ {
+ JsonObject user = fUsers.get(name);
+ if (user != null)
+ {
+ //handle special attributes
+ if(att.equals(THUMB_ATTR))
+ {
+ byte[] picData = (byte[]) val;
+ String url = storePicture(name, picData);
+
+ user.setSingleAttribute(THUMB_PROP, url);
+ fChangedUsers.add(user);
+ }
+ else if(att.equals(MAIL_PROP))
+ {
+ //create fake list if list does not contain address
+ List mails = user.getObjectList(MAIL_PROP);
+
+ if(mails == null)
+ {
+ mails = new ArrayList();
+ }
+
+ if(mails.isEmpty())
+ {
+ JsonObject mail = new JsonObject();
+ mail.setSingleAttribute("value", val.toString());
+ mails.add(mail);
+
+ user.setObjectList(MAIL_PROP, mails);
+ }
+ }
+ else if(att.equals(PHONE_PROP))
+ {
+ //create fake list if list does not contain number
+ List phones = user.getObjectList(PHONE_PROP);
+
+ if(phones == null)
+ {
+ phones = new ArrayList();
+ }
+
+ if(phones.isEmpty())
+ {
+ JsonObject phone = new JsonObject();
+ phone.setSingleAttribute("value", val.toString());
+ phones.add(phone);
+
+ user.setObjectList(PHONE_PROP, phones);
+ }
+ }
+ //TODO: refactor to hashset-based check + set-based name lookup
+ else if(att.equals(ORG_ATTR))
+ {
+ JsonObject org = getPrimaryOrganization(user);
+ org.setSingleAttribute(ORG_NAME_PROP, val.toString());
+ }
+ else if(att.equals(ORG_LOCATION_ATTR))
+ {
+ JsonObject org = getPrimaryOrganization(user);
+ org.setSingleAttribute(ORG_LOCATION_PROP, val.toString());
+ }
+ else if(att.equals(ORG_SITE_ATTR))
+ {
+ JsonObject org = getPrimaryOrganization(user);
+ org.setSingleAttribute(ORG_SITE_PROP, val.toString());
+ }
+ else if(att.equals(JOB_TITLE_ATTR))
+ {
+ JsonObject org = getPrimaryOrganization(user);
+ org.setSingleAttribute(JOB_TITLE_PROP, val.toString());
+ }
+ else if(att.equals(MANAGER_ID_PROP))
+ {
+ //extract uid from the ldap path given
+ //TODO: only works for our case
+ String managerId = val.toString();
+ if(managerId.indexOf(',') > 0)
+ {
+ //get only uid property
+ managerId = managerId.substring(0, managerId.indexOf(','));
+ }
+ if(managerId.indexOf('=') > 0)
+ {
+ //get only uid value
+ managerId = managerId.substring(
+ managerId.indexOf('=') + 1, managerId.length());
+ }
+
+ if(!managerId.isEmpty()
+ && !managerId.equals("-"))
+ {
+ JsonObject org = getPrimaryOrganization(user);
+ org.setSingleAttribute(MANAGER_ID_PROP, managerId);
+ }
+ }
+ else if(att.equals(SECRETARY_ID_PROP))
+ {
+ JsonObject org = getPrimaryOrganization(user);
+ org.setSingleAttribute(SECRETARY_ID_PROP, val.toString());
+ }
+ else if(att.equals(DEPARTMENT_PROP))
+ {
+ JsonObject org = getPrimaryOrganization(user);
+ org.setSingleAttribute(DEPARTMENT_PROP, val.toString());
+ }
+ else if(att.equals(DEPARTMENT_HEAD_PROP))
+ {
+ JsonObject org = getPrimaryOrganization(user);
+ org.setSingleAttribute(DEPARTMENT_HEAD_PROP, val.toString());
+ }
+ else if(att.equals(ORG_UNIT_PROP))
+ {
+ JsonObject org = getPrimaryOrganization(user);
+ org.setSingleAttribute(ORG_UNIT_PROP, val.toString());
+ }
+ //handle simple properties
+ else
+ {
+ String valString = val.toString();
+ String oldVal = user.getSingleAttribute(att);
+
+ if (!valString.equals(oldVal))
+ {
+ user.setSingleAttribute(att, valString);
+ fChangedUsers.add(user);
+ }
+ }
+ }
+ }
+
+ private String storePicture(String name, byte[] data)
+ {
+ File file = new File(fPicFolder + name + ".png");
+ String url = null;
+
+ try
+ {
+ BufferedOutputStream bos = new BufferedOutputStream(
+ new FileOutputStream(file));
+
+ bos.write(data);
+ bos.flush();
+ bos.close();
+
+ url = fPicUrl + name + ".png";
+ }
+ catch(Exception e)
+ {
+ e.printStackTrace();
+ }
+
+ return url;
+ }
+
+ private JsonObject getPrimaryOrganization(JsonObject user)
+ {
+ JsonObject org = null;
+
+ //get first organization from list
+ List organizations = user.getObjectList(ORG_PROP);
+
+ if(organizations == null)
+ {
+ organizations = new ArrayList();
+ user.setObjectList(ORG_PROP, organizations);
+ }
+
+ if(organizations.isEmpty())
+ {
+ org = new JsonObject();
+ organizations.add(org);
+ }
+ else
+ {
+ org = organizations.get(0);
+ }
+
+ return org;
+ }
+
+ @Override
+ protected void setAttribute(String name, String att, List vals)
+ {
+ JsonObject user = fUsers.get(name);
+ if (user != null)
+ {
+ List oldVals = user.getListAttribute(att);
+
+ boolean replace = true;
+ if (oldVals != null)
+ {
+ // check for equality
+ if (vals.size() == oldVals.size())
+ {
+ replace = false;
+
+ }
+ }
+
+ if (replace)
+ {
+ List newVals = new ArrayList();
+ for (Object o : vals)
+ {
+ newVals.add(o.toString());
+ }
+
+ user.setListAttribute(att, newVals);
+ fChangedUsers.add(user);
+ }
+ }
+ }
+
+ @Override
+ protected void addValues(String name, String att, List vals)
+ {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ protected void removeAttribute(String name, String att)
+ {
+ JsonObject user = fUsers.get(name);
+ if (user != null)
+ {
+ boolean changed = user.removeAttribute(att);
+
+ if (changed)
+ {
+ fChangedUsers.add(user);
+ }
+ }
+ }
+
+ @Override
+ protected Set getUserNames()
+ {
+ return new HashSet(fUserNames);
+ }
+
+ @Override
+ protected void createUser(ILdapUser user)
+ {
+ String id = user.getUid();
+
+ fCreatedUsers.add(id);
+ fUserNames.add(id);
+
+ // TODO: provide via COPY_ON_CREATE
+ JsonObject jsonUser = new JsonObject();
+ jsonUser.setSingleAttribute(ID_ATT, id);
+
+ jsonUser.setSingleAttribute("displayName", user
+ .getAttributeValues("cn").get(0).toString());
+ jsonUser.setSingleAttribute("name.formatted",
+ user.getAttributeValues("cn").get(0).toString());
+ jsonUser.setSingleAttribute("name.givenName",
+ user.getAttributeValues("givenName").get(0).toString());
+ jsonUser.setSingleAttribute("name.familyName",
+ user.getAttributeValues("sn").get(0).toString());
+
+ fUsers.put(id, jsonUser);
+ fChangedUsers.add(jsonUser);
+ }
+
+ @Override
+ protected void deleteUser(String name)
+ {
+ JsonObject user = fUsers.remove(name);
+ if (user != null)
+ {
+ fDeletedUsers.add(name);
+ fChangedUsers.remove(user);
+ fCreatedUsers.remove(name);
+ }
+ }
+}
diff --git a/src/main/java/de/hofuniversity/iisys/ldapsync/endpoints/TestEndpoint.java b/src/main/java/de/hofuniversity/iisys/ldapsync/endpoints/TestEndpoint.java
new file mode 100644
index 0000000..ad774ab
--- /dev/null
+++ b/src/main/java/de/hofuniversity/iisys/ldapsync/endpoints/TestEndpoint.java
@@ -0,0 +1,300 @@
+package de.hofuniversity.iisys.ldapsync.endpoints;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import javax.naming.NamingEnumeration;
+import javax.naming.directory.Attribute;
+
+import de.hofuniversity.iisys.ldapsync.LdapBuffer;
+import de.hofuniversity.iisys.ldapsync.config.SyncDirections;
+import de.hofuniversity.iisys.ldapsync.config.SyncEndpointConfig;
+import de.hofuniversity.iisys.ldapsync.config.SyncOperations;
+import de.hofuniversity.iisys.ldapsync.config.SyncRule;
+import de.hofuniversity.iisys.ldapsync.model.ILdapUser;
+
+/**
+ * Fake end point that can be filled with operations for testing purposes
+ *
+ * @author fholzschuher2
+ *
+ */
+public class TestEndpoint implements ISyncEndpoint
+{
+ private final LdapBuffer fLdap;
+ private final List fRules;
+
+ private final boolean fCreateOwn;
+ private final boolean fCreateLdap;
+ private final boolean fDeleteOwn;
+ private final boolean fDeleteLdap;
+
+ private final Map> fUsers;
+
+ /**
+ * Creates a test end point reading and manipulating data in the given
+ * buffer. Throws a NullPointerException if any parameter is null.
+ *
+ * @param ldap
+ * buffer to read from and write to
+ */
+ public TestEndpoint(LdapBuffer ldap, SyncEndpointConfig config)
+ {
+ if (ldap == null)
+ {
+ throw new NullPointerException("ldap buffer was null");
+ }
+ if (config == null)
+ {
+ throw new NullPointerException("configuration object was null");
+ }
+
+ fLdap = ldap;
+ fRules = config.getMapping();
+
+ fCreateOwn = config.getCreateOwnEntries();
+ fCreateLdap = config.getCreateLdapEntries();
+ fDeleteOwn = config.getDeleteOwnEntries();
+ fDeleteLdap = config.getDeleteLdapEntries();
+
+ // create some fake users
+ fUsers = new HashMap>();
+ Map user = null;
+ String[] names = null;
+
+ for (Entry userE : config.getProperties().entrySet())
+ {
+ user = new HashMap();
+ user.put("id", userE.getKey());
+
+ names = userE.getValue().split(" ");
+ user.put("name.formatted", userE.getValue());
+ user.put("name.givenName", names[0]);
+ user.put("name.familyName", names[1]);
+
+ fUsers.put(userE.getKey(), user);
+ }
+ }
+
+ public void sync()
+ {
+ Map users = fLdap.getAllUsers();
+
+ Attribute att = null;
+ NamingEnumeration extends Attribute> atts = null;
+ NamingEnumeration> vals = null;
+
+ // create new fake users
+ if (fCreateOwn)
+ {
+ for (Entry userE : users.entrySet())
+ {
+ if (!fUsers.containsKey(userE.getKey()))
+ {
+ System.out.println("create own user " + userE.getKey());
+
+ try
+ {
+ createLocal(userE.getKey());
+ } catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ // create new LDAP users
+ if (fCreateLdap)
+ ;
+ {
+ for (Entry> userE : fUsers.entrySet())
+ {
+ if (!users.containsKey(userE.getKey()))
+ {
+ System.out.println("create LDAP user " + userE.getKey());
+ fLdap.createUser(userE.getKey());
+
+ try
+ {
+ matchAttributes(userE.getKey());
+ } catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ // delete fake users
+ if (fDeleteOwn)
+ {
+ Set toDelete = new HashSet();
+
+ for (Entry> userE : fUsers.entrySet())
+ {
+ if (!users.containsKey(userE.getKey()))
+ {
+ System.out.println("delete own user " + userE.getKey());
+ toDelete.add(userE.getKey());
+ }
+ }
+
+ for (String name : toDelete)
+ {
+ fUsers.remove(name);
+ }
+ }
+
+ // delete LDAP users
+ if (fDeleteLdap)
+ {
+ Set toDelete = new HashSet();
+
+ for (Entry userE : users.entrySet())
+ {
+ if (!fUsers.containsKey(userE.getKey()))
+ {
+ System.out.println("delete LDAP user " + userE.getKey());
+ toDelete.add(userE.getKey());
+ }
+ }
+
+ for (String name : toDelete)
+ {
+ fLdap.deleteUser(name);
+ }
+ }
+
+ int num = 0;
+
+ // print
+ for (Entry userE : users.entrySet())
+ {
+ try
+ {
+ matchAttributes(userE.getKey());
+ } catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+
+ System.out.println("user " + num + ": " + userE.getKey());
+
+ System.out.print("\t(");
+ atts = userE.getValue().getAttributes().getAll();
+ try
+ {
+ while (atts.hasMore())
+ {
+ att = atts.next();
+ vals = att.getAll();
+
+ System.out.print(att.getID() + ": ");
+ while (vals.hasMore())
+ {
+ System.out.print(vals.next());
+
+ if (vals.hasMore())
+ {
+ System.out.print('|');
+ }
+ }
+
+ if (atts.hasMore())
+ {
+ System.out.print(", ");
+ }
+ }
+ } catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+ System.out.println(')');
+
+ ++num;
+ }
+
+ System.out.println(num + " users");
+ }
+
+ private void createLocal(String name) throws Exception
+ {
+ Map localUser = new HashMap();
+
+ String ldapKey = null;
+ String localKey = null;
+ Object ldapValue = null;
+
+ for (SyncRule rule : fRules)
+ {
+ ldapKey = rule.getLdapProp();
+ localKey = rule.getEndPointProp();
+
+ if (rule.getDirection() == SyncDirections.FROM_LDAP
+ && rule.getOperation() == SyncOperations.COPY_ON_CREATE)
+ {
+ ldapValue = fLdap.getCurrentAttribute(name, ldapKey).get();
+ localUser.put(localKey, ldapValue.toString());
+ }
+ }
+
+ fUsers.put(name, localUser);
+ }
+
+ private void matchAttributes(String name) throws Exception
+ {
+ Map localUser = fUsers.get(name);
+ if (localUser == null)
+ {
+ return;
+ }
+
+ String ldapKey = null;
+ String localKey = null;
+ Object ldapValue = null;
+ Object localValue = null;
+
+ for (SyncRule rule : fRules)
+ {
+ ldapKey = rule.getLdapProp();
+ localKey = rule.getEndPointProp();
+
+ switch (rule.getDirection())
+ {
+ case FROM_LDAP:
+ ldapValue = fLdap.getCurrentAttribute(name, ldapKey).get();
+ switch (rule.getOperation())
+ {
+ case COPY:
+ localUser.put(localKey, ldapValue.toString());
+ break;
+ }
+ break;
+
+ case TO_LDAP:
+ localValue = localUser.get(localKey);
+ switch (rule.getOperation())
+ {
+ case COPY:
+ fLdap.setAttribute(name, ldapKey, localValue);
+ break;
+ }
+ break;
+
+ case BOTH:
+ switch (rule.getOperation())
+ {
+ case ADD_TO_LIST:
+
+ break;
+ }
+ break;
+ }
+ }
+ }
+}
diff --git a/src/main/java/de/hofuniversity/iisys/ldapsync/model/ALdapUser.java b/src/main/java/de/hofuniversity/iisys/ldapsync/model/ALdapUser.java
new file mode 100644
index 0000000..b253939
--- /dev/null
+++ b/src/main/java/de/hofuniversity/iisys/ldapsync/model/ALdapUser.java
@@ -0,0 +1,403 @@
+package de.hofuniversity.iisys.ldapsync.model;
+
+import java.util.Hashtable;
+import java.util.List;
+
+import javax.naming.Binding;
+import javax.naming.Context;
+import javax.naming.Name;
+import javax.naming.NameClassPair;
+import javax.naming.NameNotFoundException;
+import javax.naming.NameParser;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.ModificationItem;
+import javax.naming.directory.SearchControls;
+import javax.naming.directory.SearchResult;
+
+/**
+ * Abstract class giving stub implementation to all unneeded methods of the
+ * DirContext interface and redirecting the rest to ILdapUser methods which
+ * still have to be implemented.
+ *
+ * @author fholzschuher2
+ *
+ */
+public abstract class ALdapUser implements ILdapUser
+{
+ // own methods
+
+ public abstract void addAttribute(Attribute attribute);
+
+ public abstract void setAttribute(String name, Object value);
+
+ public abstract void removeAttribute(String name);
+
+ public abstract String getUid();
+
+ public abstract List getAttributeValues(String name);
+
+ public abstract Attributes getAttributes();
+
+ public abstract Attributes getAttributes(String[] attrIds);
+
+ public abstract Attribute getAttribute(String attr);
+
+ public abstract boolean isNew();
+
+ // needed methods
+
+ public Attributes getAttributes(Name name) throws NamingException
+ {
+ return getAttributes(name.toString());
+ }
+
+ public Attributes getAttributes(String name) throws NamingException
+ {
+ if (!name.equals(""))
+ {
+ throw new NameNotFoundException("this is just a user entity");
+ }
+
+ return getAttributes();
+ }
+
+ public Attributes getAttributes(Name name, String[] attrIds)
+ throws NamingException
+ {
+ return getAttributes(name.toString(), attrIds);
+ }
+
+ public Attributes getAttributes(String name, String[] attrIds)
+ throws NamingException
+ {
+ if (!name.equals(""))
+ {
+ throw new NameNotFoundException("this is just a user entity");
+ }
+
+ return getAttributes(attrIds);
+ }
+
+ // unneeded methods
+
+ public Object addToEnvironment(String propName, Object propVal)
+ throws NamingException
+ {
+ // not needed
+ return null;
+ }
+
+ public void bind(Name name, Object obj) throws NamingException
+ {
+ // not needed
+ }
+
+ public void bind(String name, Object obj) throws NamingException
+ {
+ // not needed
+ }
+
+ public void close() throws NamingException
+ {
+ // not needed
+ }
+
+ public Name composeName(Name name, Name prefix) throws NamingException
+ {
+ // not needed
+ return null;
+ }
+
+ public String composeName(String name, String prefix)
+ throws NamingException
+ {
+ // not needed
+ return null;
+ }
+
+ public Context createSubcontext(Name name) throws NamingException
+ {
+ // not needed
+ return null;
+ }
+
+ public Context createSubcontext(String name) throws NamingException
+ {
+ // not needed
+ return null;
+ }
+
+ public void destroySubcontext(Name name) throws NamingException
+ {
+ // not needed
+ }
+
+ public void destroySubcontext(String name) throws NamingException
+ {
+ // not needed
+ }
+
+ public Hashtable, ?> getEnvironment() throws NamingException
+ {
+ // not needed
+ return null;
+ }
+
+ public String getNameInNamespace() throws NamingException
+ {
+ // not needed
+ return null;
+ }
+
+ public NameParser getNameParser(Name name) throws NamingException
+ {
+ // not needed
+ return null;
+ }
+
+ public NameParser getNameParser(String name) throws NamingException
+ {
+ // not needed
+ return null;
+ }
+
+ public NamingEnumeration list(Name name)
+ throws NamingException
+ {
+ // not needed
+ return null;
+ }
+
+ public NamingEnumeration list(String name)
+ throws NamingException
+ {
+ // not needed
+ return null;
+ }
+
+ public NamingEnumeration listBindings(Name name)
+ throws NamingException
+ {
+ // not needed
+ return null;
+ }
+
+ public NamingEnumeration listBindings(String name)
+ throws NamingException
+ {
+ // not needed
+ return null;
+ }
+
+ public Object lookup(Name name) throws NamingException
+ {
+ // not needed
+ return null;
+ }
+
+ public Object lookup(String name) throws NamingException
+ {
+ // not needed
+ return null;
+ }
+
+ public Object lookupLink(Name name) throws NamingException
+ {
+ // not needed
+ return null;
+ }
+
+ public Object lookupLink(String name) throws NamingException
+ {
+ // not needed
+ return null;
+ }
+
+ public void rebind(Name name, Object obj) throws NamingException
+ {
+ // not needed
+ }
+
+ public void rebind(String name, Object obj) throws NamingException
+ {
+ // not needed
+ }
+
+ public Object removeFromEnvironment(String propName) throws NamingException
+ {
+ // not needed
+ return null;
+ }
+
+ public void rename(Name oldName, Name newName) throws NamingException
+ {
+ // not needed
+ }
+
+ public void rename(String oldName, String newName) throws NamingException
+ {
+ // not needed
+ }
+
+ public void unbind(Name name) throws NamingException
+ {
+ // not needed
+ }
+
+ public void unbind(String name) throws NamingException
+ {
+ // not needed
+ }
+
+ public void bind(Name name, Object obj, Attributes attrs)
+ throws NamingException
+ {
+ // not needed
+ }
+
+ public void bind(String name, Object obj, Attributes attrs)
+ throws NamingException
+ {
+ // not needed
+ }
+
+ public DirContext createSubcontext(Name name, Attributes attrs)
+ throws NamingException
+ {
+ // not needed
+ return null;
+ }
+
+ public DirContext createSubcontext(String name, Attributes attrs)
+ throws NamingException
+ {
+ // not needed
+ return null;
+ }
+
+ public DirContext getSchema(Name name) throws NamingException
+ {
+ // not needed
+ return null;
+ }
+
+ public DirContext getSchema(String name) throws NamingException
+ {
+ // not needed
+ return null;
+ }
+
+ public DirContext getSchemaClassDefinition(Name name)
+ throws NamingException
+ {
+ // not needed
+ return null;
+ }
+
+ public DirContext getSchemaClassDefinition(String name)
+ throws NamingException
+ {
+ // not needed
+ return null;
+ }
+
+ public void modifyAttributes(Name name, ModificationItem[] mods)
+ throws NamingException
+ {
+ // not needed
+
+ }
+
+ public void modifyAttributes(String name, ModificationItem[] mods)
+ throws NamingException
+ {
+ // not needed
+ }
+
+ public void modifyAttributes(Name name, int mod_op, Attributes attrs)
+ throws NamingException
+ {
+ // not needed
+ }
+
+ public void modifyAttributes(String name, int mod_op, Attributes attrs)
+ throws NamingException
+ {
+ // not needed
+ }
+
+ public void rebind(Name name, Object obj, Attributes attrs)
+ throws NamingException
+ {
+ // not needed
+ }
+
+ public void rebind(String name, Object obj, Attributes attrs)
+ throws NamingException
+ {
+ // not needed
+ }
+
+ public NamingEnumeration search(Name name,
+ Attributes matchingAttributes) throws NamingException
+ {
+ // not needed
+ return null;
+ }
+
+ public NamingEnumeration search(String name,
+ Attributes matchingAttributes) throws NamingException
+ {
+ // not needed
+ return null;
+ }
+
+ public NamingEnumeration search(Name name,
+ Attributes matchingAttributes, String[] attributesToReturn)
+ throws NamingException
+ {
+ // not needed
+ return null;
+ }
+
+ public NamingEnumeration search(String name,
+ Attributes matchingAttributes, String[] attributesToReturn)
+ throws NamingException
+ {
+ // not needed
+ return null;
+ }
+
+ public NamingEnumeration search(Name name, String filter,
+ SearchControls cons) throws NamingException
+ {
+ // not needed
+ return null;
+ }
+
+ public NamingEnumeration search(String name, String filter,
+ SearchControls cons) throws NamingException
+ {
+ // not needed
+ return null;
+ }
+
+ public NamingEnumeration search(Name name, String filterExpr,
+ Object[] filterArgs, SearchControls cons) throws NamingException
+ {
+ // not needed
+ return null;
+ }
+
+ public NamingEnumeration search(String name,
+ String filterExpr, Object[] filterArgs, SearchControls cons)
+ throws NamingException
+ {
+ // not needed
+ return null;
+ }
+}
diff --git a/src/main/java/de/hofuniversity/iisys/ldapsync/model/ILdapUser.java b/src/main/java/de/hofuniversity/iisys/ldapsync/model/ILdapUser.java
new file mode 100644
index 0000000..7337a07
--- /dev/null
+++ b/src/main/java/de/hofuniversity/iisys/ldapsync/model/ILdapUser.java
@@ -0,0 +1,88 @@
+package de.hofuniversity.iisys.ldapsync.model;
+
+import java.util.List;
+
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.DirContext;
+
+/**
+ * User class containing information to store in an LDAP directory service. Only
+ * users marked as new via the isNew() method may be manipulated directly with
+ * most implementations. Consequently, all user modifications should be done
+ * through the buffer's methods to keep modifications consistent.
+ *
+ * @author fholzschuher2
+ *
+ */
+public interface ILdapUser extends DirContext
+{
+ /**
+ * Adds an arbitrary attribute to the person. Parameter must not be null.
+ *
+ * @param attribute
+ * attribute to add
+ */
+ public void addAttribute(Attribute attribute);
+
+ /**
+ * Adds or replaces a named attribute. Parameters must not be null.
+ *
+ * @param name
+ * id or name for the attribute
+ * @param value
+ * value of the attribute
+ */
+ public void setAttribute(String name, Object value);
+
+ /**
+ * Removes a named attribute.
+ *
+ * @param name
+ * id or name of the attribute
+ */
+ public void removeAttribute(String name);
+
+ /**
+ * @return user's LDAP UID
+ */
+ public String getUid();
+
+ /**
+ * Returns all values of a certain attribute, as LDAP attributes can have
+ * multiple values the result is a list of objects.
+ *
+ * @param name
+ * name of the attribute to get
+ * @return list of values of the attribute
+ */
+ public List getAttributeValues(String name);
+
+ /**
+ * @return all of the user's attributes
+ */
+ public Attributes getAttributes();
+
+ /**
+ * Returns a set of attributes, whose IDs are defined by the given array.
+ * Attributes that aren't found are not contained in the result. Array of
+ * IDs may not be null.
+ *
+ * @param attrIds
+ * array of attribute IDs
+ * @return collection of requested attributes
+ */
+ public Attributes getAttributes(String[] attrIds);
+
+ /**
+ * @param attr
+ * name of the attribute
+ * @return attribute or null if it doesn't exist
+ */
+ public Attribute getAttribute(String attr);
+
+ /**
+ * @return whether this is a newly created or existing user
+ */
+ public boolean isNew();
+}
diff --git a/src/main/java/de/hofuniversity/iisys/ldapsync/model/ILdapUserFactory.java b/src/main/java/de/hofuniversity/iisys/ldapsync/model/ILdapUserFactory.java
new file mode 100644
index 0000000..06981bf
--- /dev/null
+++ b/src/main/java/de/hofuniversity/iisys/ldapsync/model/ILdapUserFactory.java
@@ -0,0 +1,20 @@
+package de.hofuniversity.iisys.ldapsync.model;
+
+/**
+ * Factory that creates user objects ready to be stored in an LDAP directory.
+ *
+ * @author fholzschuher2
+ *
+ */
+public interface ILdapUserFactory
+{
+ /**
+ * Creates a new user with the given name and some initial values. Name may
+ * not be null or empty.
+ *
+ * @param name
+ * UID for the user
+ * @return newly created user with initial values
+ */
+ public ILdapUser createUser(String name);
+}
diff --git a/src/main/java/de/hofuniversity/iisys/ldapsync/model/LdapUserFactory.java b/src/main/java/de/hofuniversity/iisys/ldapsync/model/LdapUserFactory.java
new file mode 100644
index 0000000..a316d96
--- /dev/null
+++ b/src/main/java/de/hofuniversity/iisys/ldapsync/model/LdapUserFactory.java
@@ -0,0 +1,60 @@
+package de.hofuniversity.iisys.ldapsync.model;
+
+import java.util.List;
+
+import de.hofuniversity.iisys.ldapsync.config.SyncConfig;
+
+/**
+ * Factory that creates user objects ready to be stored in an LDAP directory as
+ * defined by the configuration.
+ *
+ * @author fholzschuher2
+ *
+ */
+public class LdapUserFactory implements ILdapUserFactory
+{
+ private final List fClasses, fOus;
+
+ /**
+ * Creates a user factory, giving users the initial object classes and
+ * organizational units configured. Throws a NullPointerException if
+ * configuration, initial classes or initial organizational units are null
+ * or no organizational units are defined.
+ *
+ * @param config
+ * configuration object to use
+ */
+ public LdapUserFactory(SyncConfig config)
+ {
+ if (config == null)
+ {
+ throw new NullPointerException("configuration object was null");
+ }
+ if (config.getInitialClasses() == null)
+ {
+ throw new NullPointerException("initial classes list was null");
+ }
+
+ fOus = config.getInitialOus();
+ if (fOus == null || fOus.isEmpty())
+ {
+ throw new NullPointerException(
+ "no initial organizational units given");
+ }
+
+ fClasses = config.getInitialClasses();
+ }
+
+ /**
+ * Creates a new user with the given name and the configured initial values.
+ * Name may not be null or empty.
+ *
+ * @param name
+ * UID for the user
+ * @return newly created user with the configured initial values
+ */
+ public ILdapUser createUser(String name)
+ {
+ return new SimpleLdapUser(name, fClasses, fOus);
+ }
+}
diff --git a/src/main/java/de/hofuniversity/iisys/ldapsync/model/SimpleLdapUser.java b/src/main/java/de/hofuniversity/iisys/ldapsync/model/SimpleLdapUser.java
new file mode 100644
index 0000000..165eaf1
--- /dev/null
+++ b/src/main/java/de/hofuniversity/iisys/ldapsync/model/SimpleLdapUser.java
@@ -0,0 +1,185 @@
+package de.hofuniversity.iisys.ldapsync.model;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.naming.NamingEnumeration;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.BasicAttribute;
+import javax.naming.directory.BasicAttributes;
+
+/**
+ * User class containing information to store in an LDAP directory service. Only
+ * users marked as new via the isNew() method can be manipulated directly.
+ * Consequently, all user modifications should be done through the buffer's
+ * methods to keep modifications consistent.
+ *
+ * @author fholzschuher2
+ *
+ */
+public class SimpleLdapUser extends ALdapUser
+{
+ private final String fUid;
+ private final Attributes fAttributes;
+
+ private final boolean fNew;
+
+ /**
+ * Creates a new user which can be automatically stored in an LDAP directory
+ * service. Initially, UID, OCs and OUs are set, further attributes can be
+ * added using the setAttribute()-method. Throws a NullPointerException if
+ * the given UID is null.
+ *
+ * @param uid
+ * LDAP UID for the user
+ * @param ocs
+ * initial object classes for the user
+ * @param ous
+ * initial organizational units for the user
+ */
+ public SimpleLdapUser(String uid, List ocs, List ous)
+ {
+ if (uid == null || uid.isEmpty())
+ {
+ throw new NullPointerException("no uid given");
+ }
+
+ fUid = uid;
+ fAttributes = new BasicAttributes();
+
+ // collect attributes
+ fAttributes.put("uid", fUid);
+
+ // object classes
+ if (ocs != null)
+ {
+ Attribute ocSet = new BasicAttribute("objectclass");
+ for (String oc : ocs)
+ {
+ ocSet.add(oc);
+ }
+ fAttributes.put(ocSet);
+ }
+
+ // organizational units
+ if (ous != null)
+ {
+ Attribute ouSet = new BasicAttribute("ou");
+ for (String ou : ous)
+ {
+ ouSet.add(ou);
+ }
+ fAttributes.put(ouSet);
+ }
+
+ fNew = true;
+ }
+
+ /**
+ * Creates a new user from LDAP query result data. Such instances should not
+ * be manipulated directly but via the buffer.
+ *
+ * @param uid
+ * LDAP UID for the user
+ * @param attributes
+ * collection of attributes
+ */
+ public SimpleLdapUser(String uid, Attributes attributes)
+ {
+ if (uid == null || uid.isEmpty())
+ {
+ throw new NullPointerException("no uid given");
+ }
+ if (attributes == null)
+ {
+ throw new NullPointerException("attributes were null");
+ }
+
+ fUid = uid;
+ fAttributes = attributes;
+
+ fNew = false;
+ }
+
+ // own methods
+
+ public void addAttribute(Attribute attribute)
+ {
+ fAttributes.put(attribute);
+ }
+
+ public void setAttribute(String name, Object value)
+ {
+ fAttributes.put(name, value);
+ }
+
+ public void removeAttribute(String name)
+ {
+ fAttributes.remove(name);
+ }
+
+ public String getUid()
+ {
+ return fUid;
+ }
+
+ public List getAttributeValues(String name)
+ {
+ List list = null;
+ Attribute att = fAttributes.get(name);
+
+ try
+ {
+ if (att != null)
+ {
+ list = new ArrayList();
+
+ NamingEnumeration> elements = att.getAll();
+
+ while (elements.hasMore())
+ {
+ list.add(elements.next());
+ }
+ }
+ } catch (Exception e)
+ {
+ e.printStackTrace();
+ }
+
+ return list;
+ }
+
+ public Attributes getAttributes()
+ {
+ return fAttributes;
+ }
+
+ public Attributes getAttributes(String[] attrIds)
+ {
+ Attributes atts = new BasicAttributes();
+ Attribute a = null;
+
+ for (String id : attrIds)
+ {
+ a = fAttributes.get(id);
+
+ if (a != null)
+ {
+ atts.put(a);
+ }
+ }
+
+ return atts;
+ }
+
+ public Attribute getAttribute(String attr)
+ {
+ return fAttributes.get(attr);
+ }
+
+ public boolean isNew()
+ {
+ return fNew;
+ }
+}
diff --git a/src/main/java/de/hofuniversity/iisys/ldapsync/util/JsonObject.java b/src/main/java/de/hofuniversity/iisys/ldapsync/util/JsonObject.java
new file mode 100644
index 0000000..9dc4e3a
--- /dev/null
+++ b/src/main/java/de/hofuniversity/iisys/ldapsync/util/JsonObject.java
@@ -0,0 +1,635 @@
+package de.hofuniversity.iisys.ldapsync.util;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * Class simplifying the use of JSON objects by converting themselves from and
+ * to JSON as well as providing attribute access. The toString()-method
+ * generates a JSON representation of the object.
+ *
+ * @author fholzschuher2
+ *
+ */
+public class JsonObject
+{
+ private final Map fAttributes;
+ private final Map> fListAttributes;
+ private final Map fSubObjects;
+ private final Map> fObjectLists;
+
+ public static int getCloseBracketIndex(final String json, int start)
+ {
+ final int length = json.length();
+ int depth = 0;
+
+ final char opBracket = json.charAt(start);
+ char clBracket = ']';
+ if (opBracket == '{')
+ {
+ clBracket = '}';
+ }
+
+ int i = start;
+ char c = ' ';
+ for (; i < length; ++i)
+ {
+ c = json.charAt(i);
+
+ if (c == opBracket)
+ {
+ ++depth;
+ } else if (c == clBracket)
+ {
+ --depth;
+ }
+
+ if (depth == 0)
+ {
+ break;
+ }
+ }
+
+ return i;
+ }
+
+ /**
+ * Creates a blank JSON object that can be filled with attributes and
+ * converted to a JSON String.
+ */
+ public JsonObject()
+ {
+ fAttributes = new HashMap();
+ fListAttributes = new HashMap>();
+ fSubObjects = new HashMap();
+ fObjectLists = new HashMap>();
+ }
+
+ /**
+ * Creates a JSON object from a JSON string, setting all attributes and
+ * sub-objects.
+ *
+ * @param json
+ * JSON string
+ */
+ public JsonObject(final String json)
+ {
+ fAttributes = new HashMap();
+ fListAttributes = new HashMap>();
+ fSubObjects = new HashMap();
+ fObjectLists = new HashMap>();
+
+ final int length = json.length();
+ char c = ' ';
+ for (int i = 1; i < length; ++i)
+ {
+ c = json.charAt(i);
+
+ if (c == '"')
+ {
+ i = readAttribute(json, i);
+ }
+
+ // TODO: lists
+ }
+ }
+
+ private int readAttribute(final String json, int start)
+ {
+ int stop = ++start;
+
+ char c = json.charAt(stop);
+ while (c != '"')
+ {
+ ++stop;
+ c = json.charAt(stop);
+ }
+
+ String name = json.substring(start, stop);
+
+ stop += 2;
+ c = json.charAt(stop);
+
+ // singular attributes
+ if (c == '"')
+ {
+ start = ++stop;
+
+ c = json.charAt(stop);
+ while (c != '"')
+ {
+ ++stop;
+ c = json.charAt(stop);
+ }
+ String value = json.substring(start, stop);
+
+ fAttributes.put(name, value);
+ }
+ // objects
+ else if (c == '{')
+ {
+ start = stop;
+ stop = getCloseBracketIndex(json, start) + 1;
+
+ String subString = json.substring(start, stop);
+ JsonObject subObject = new JsonObject(subString);
+
+ fSubObjects.put(name, subObject);
+ }
+ // list attributes
+ else if (c == '[')
+ {
+ start = stop;
+ stop = getCloseBracketIndex(json, start) + 1;
+
+ c = json.charAt(start + 1);
+ if (c == '"')
+ {
+ List values = readList(json, start);
+ fListAttributes.put(name, values);
+ } else
+ {
+ // TODO: object lists?
+ }
+ }
+
+ return stop;
+ }
+
+ private List readList(final String json, int start)
+ {
+ final List list = new ArrayList();
+ int pos = ++start;
+
+ boolean reading = false;
+ char c = json.charAt(pos);
+ while (c != ']')
+ {
+ if (c == '"')
+ {
+ if (reading)
+ {
+ reading = false;
+ list.add(json.substring(start, pos));
+ } else
+ {
+ reading = true;
+ start = pos + 1;
+ }
+ }
+
+ ++pos;
+ c = json.charAt(pos);
+ }
+
+ return list;
+ }
+
+ /**
+ * Retrieves the single value of a named attribute. Returns null if there is
+ * no such attribute or there are multiple values or only objects.
+ *
+ * @param name
+ * name of the attribute
+ * @return value of the attribute or null
+ */
+ public String getSingleAttribute(String name)
+ {
+ String value = null;
+
+ if (name.contains("."))
+ {
+ int dot = name.indexOf('.');
+ String att = name.substring(dot + 1);
+ name = name.substring(0, dot);
+ JsonObject subObject = fSubObjects.get(name);
+
+ if (subObject != null)
+ {
+ value = subObject.getSingleAttribute(att);
+ }
+ } else
+ {
+ value = fAttributes.get(name);
+ }
+
+ return value;
+ }
+
+ /**
+ * Retrieves all values of the attribute with the given name. Returns null
+ * if there is only a singular value or only objects.
+ *
+ * @param name
+ * name of the attribute
+ * @return list of values of the attribute or null
+ */
+ public List getListAttribute(String name)
+ {
+ List values = null;
+
+ if (name.contains("."))
+ {
+ int dot = name.indexOf('.');
+ String att = name.substring(dot + 1);
+ name = name.substring(0, dot);
+ JsonObject subObject = fSubObjects.get(name);
+
+ if (subObject != null)
+ {
+ values = subObject.getListAttribute(att);
+ }
+ } else
+ {
+ values = fListAttributes.get(name);
+ }
+
+ return values;
+ }
+
+ /**
+ * Retrieves a named subordinate object of this JSON object. Returns null if
+ * it does not exist.
+ *
+ * @param name
+ * name of the object
+ * @return a JSON object or null
+ */
+ public JsonObject getSubObject(String name)
+ {
+ JsonObject object = null;
+
+ if (name.contains("."))
+ {
+ int dot = name.indexOf('.');
+ String sub = name.substring(dot + 1);
+ name = name.substring(0, dot);
+ JsonObject subObject = fSubObjects.get(name);
+
+ if (subObject != null)
+ {
+ object = subObject.getSubObject(sub);
+ }
+ } else
+ {
+ object = fSubObjects.get(name);
+ }
+
+ return object;
+ }
+
+ /**
+ * Retrieves all values of the object list with the given name. Returns null
+ * if there is no such list or the attribute is only listed under singular
+ * values or sub objects.
+ *
+ * @param name
+ * name of the attribute
+ * @return list of values or null
+ */
+ public List getObjectList(String name)
+ {
+ List list = null;
+
+ if (name.contains("."))
+ {
+ int dot = name.indexOf('.');
+ String att = name.substring(dot + 1);
+ name = name.substring(0, dot);
+ JsonObject subObject = fSubObjects.get(name);
+
+ if (subObject != null)
+ {
+ list = subObject.getObjectList(att);
+ }
+ } else
+ {
+ list = fObjectLists.get(name);
+ }
+
+ return list;
+ }
+
+ /**
+ * Sets the value of a singular attribute, overwrites any existing values
+ * and removes list and object values with the same name.
+ *
+ * @param name
+ * name of the attribute to set
+ * @param value
+ * value to set the attribute to
+ */
+ public void setSingleAttribute(String name, String value)
+ {
+ if (name.contains("."))
+ {
+ int dot = name.indexOf('.');
+ String att = name.substring(dot + 1);
+ name = name.substring(0, dot);
+ JsonObject subObject = fSubObjects.get(name);
+
+ if (subObject == null)
+ {
+ subObject = new JsonObject();
+ fSubObjects.put(name, subObject);
+ }
+
+ subObject.setSingleAttribute(att, value);
+ } else
+ {
+ removeAttribute(name);
+
+ fAttributes.put(name, value);
+ }
+ }
+
+ /**
+ * Sets an attribute to multiple values, overwriting any existing values and
+ * removing singular values and object values with the same name.
+ *
+ * @param name
+ * name of the attribute to set
+ * @param values
+ * list of values to set the attribute to
+ */
+ public void setListAttribute(String name, List values)
+ {
+ if (name.contains("."))
+ {
+ int dot = name.indexOf('.');
+ String att = name.substring(dot + 1);
+ name = name.substring(0, dot);
+ JsonObject subObject = fSubObjects.get(name);
+
+ if (subObject == null)
+ {
+ subObject = new JsonObject();
+ fSubObjects.put(name, subObject);
+ }
+
+ subObject.setListAttribute(att, values);
+ } else
+ {
+ removeAttribute(name);
+
+ fListAttributes.put(name, values);
+ }
+ }
+
+ /**
+ * Sets the sub object registered under a certain name, overwrites any
+ * existing values and removes list and object values with the same name.
+ *
+ * @param name
+ * name of the sub object to set
+ * @param object
+ * object to set
+ */
+ public void setSubObject(String name, JsonObject object)
+ {
+ if (name.contains("."))
+ {
+ int dot = name.indexOf('.');
+ String sub = name.substring(dot + 1);
+ name = name.substring(0, dot);
+ JsonObject subObject = fSubObjects.get(name);
+
+ if (subObject == null)
+ {
+ subObject = new JsonObject();
+ fSubObjects.put(name, subObject);
+ }
+
+ subObject.setSubObject(sub, object);
+ } else
+ {
+ removeAttribute(name);
+
+ fSubObjects.put(name, object);
+ }
+ }
+
+ /**
+ * Sets the values of an object list, overwrites any existing singular
+ * values and removes list and object values with the same name.
+ *
+ * @param name
+ * name of the object list to set
+ * @param list
+ * list of values to set
+ */
+ public void setObjectList(String name, List list)
+ {
+ if (name.contains("."))
+ {
+ int dot = name.indexOf('.');
+ String att = name.substring(dot + 1);
+ name = name.substring(0, dot);
+ JsonObject subObject = fSubObjects.get(name);
+
+ if (subObject == null)
+ {
+ subObject = new JsonObject();
+ fSubObjects.put(name, subObject);
+ }
+
+ subObject.setObjectList(att, list);
+ } else
+ {
+ removeAttribute(name);
+
+ fObjectLists.put(name, list);
+ }
+ }
+
+ /**
+ * Removes all attributes and sub objects with the given name. Ignores calls
+ * with names of non-existent attributes and objects.
+ *
+ * @param name
+ * name of the attribute to remove
+ * @return whether an attribute was removed
+ */
+ public boolean removeAttribute(String name)
+ {
+ boolean removed = false;
+
+ if (name.contains("."))
+ {
+ int dot = name.indexOf('.');
+ String att = name.substring(dot + 1);
+ name = name.substring(0, dot);
+ JsonObject subObject = fSubObjects.get(name);
+
+ if (subObject != null)
+ {
+ removed |= subObject.removeAttribute(att);
+ }
+ } else
+ {
+ Object oldVal = null;
+
+ oldVal = fAttributes.remove(name);
+ if (oldVal != null)
+ {
+ removed = true;
+ }
+
+ oldVal = fListAttributes.remove(name);
+ if (oldVal != null)
+ {
+ removed = true;
+ }
+
+ oldVal = fObjectLists.remove(name);
+ if (oldVal != null)
+ {
+ removed = true;
+ }
+ oldVal = fSubObjects.remove(name);
+ if (oldVal != null)
+ {
+ removed = true;
+ }
+ }
+
+ return removed;
+ }
+
+ @Override
+ public String toString()
+ {
+ final StringBuffer buffer = new StringBuffer("{");
+
+ // whether there are preceding attributes, so that a comma is needed
+ boolean needsComma = false;
+
+ // singular attributes
+ final Iterator> atts = fAttributes.entrySet()
+ .iterator();
+ if (atts.hasNext())
+ {
+ needsComma = true;
+ }
+
+ Entry att = null;
+ while (atts.hasNext())
+ {
+ att = atts.next();
+ buffer.append("\"" + att.getKey() + "\"");
+ buffer.append(":\"" + att.getValue() + "\"");
+
+ if (atts.hasNext())
+ {
+ buffer.append(',');
+ }
+ }
+
+ // list attributes
+ final Iterator>> listAtts = fListAttributes
+ .entrySet().iterator();
+ if (needsComma && listAtts.hasNext())
+ {
+ buffer.append(',');
+ }
+ if (listAtts.hasNext())
+ {
+ needsComma = true;
+ }
+
+ Entry> attList = null;
+ while (listAtts.hasNext())
+ {
+ attList = listAtts.next();
+ buffer.append("\"" + attList.getKey() + "\":[");
+
+ Iterator attIt = attList.getValue().iterator();
+ while (attIt.hasNext())
+ {
+ buffer.append('"' + attIt.next() + '"');
+
+ if (attIt.hasNext())
+ {
+ buffer.append(',');
+ }
+ }
+
+ buffer.append(']');
+
+ if (atts.hasNext())
+ {
+ buffer.append(',');
+ }
+ }
+
+ // sub objects
+ final Iterator> objects = fSubObjects
+ .entrySet().iterator();
+
+ if (needsComma && objects.hasNext())
+ {
+ buffer.append(',');
+ }
+ if (objects.hasNext())
+ {
+ needsComma = true;
+ }
+
+ Entry obj = null;
+ while (objects.hasNext())
+ {
+ obj = objects.next();
+ buffer.append("\"" + obj.getKey() + "\"");
+ buffer.append(":" + obj.getValue() + "");
+
+ if (objects.hasNext())
+ {
+ buffer.append(',');
+ }
+ }
+
+ // object lists
+ final Iterator>> lists = fObjectLists
+ .entrySet().iterator();
+
+ if (needsComma && lists.hasNext())
+ {
+ buffer.append(',');
+ }
+ if (lists.hasNext())
+ {
+ needsComma = true;
+ }
+
+ Entry> list = null;
+ while (lists.hasNext())
+ {
+ list = lists.next();
+ buffer.append("\"" + list.getKey() + "\":[");
+
+ Iterator objIt = list.getValue().iterator();
+ while (objIt.hasNext())
+ {
+ buffer.append(objIt.next().toString());
+
+ if (objIt.hasNext())
+ {
+ buffer.append(',');
+ }
+ }
+
+ buffer.append("]");
+
+ if (lists.hasNext())
+ {
+ buffer.append(',');
+ }
+ }
+
+ return buffer.append("}").toString();
+ }
+}