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 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 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(); + } +}