From 68e276c3842ec822ea7a7c88d9e6e7d604974d55 Mon Sep 17 00:00:00 2001
From: "Endi S. Dewata" <edewata@redhat.com>
Date: Thu, 12 Sep 2013 13:24:32 -0400
Subject: [PATCH] Added LDAPDatabase.

A new LDAPDatabase class was added as a base class for LDAP-based
databases. A new DBRecord class was added to provide the default
implementation for record classes. New annotation classes were added
to specify the object class and attribute mappings.
---
 .../src/com/netscape/certsrv/dbs/IDBSSession.java  |   2 +-
 .../src/com/netscape/cmscore/dbs/DBAttribute.java  |  33 ++++
 .../com/netscape/cmscore/dbs/DBObjectClasses.java  |  30 ++++
 .../src/com/netscape/cmscore/dbs/DBRecord.java     |  98 ++++++++++++
 .../src/com/netscape/cmscore/dbs/Database.java     |  10 +-
 .../src/com/netscape/cmscore/dbs/LDAPDatabase.java | 167 +++++++++++++++++++++
 6 files changed, 336 insertions(+), 4 deletions(-)
 create mode 100644 base/common/src/com/netscape/cmscore/dbs/DBAttribute.java
 create mode 100644 base/common/src/com/netscape/cmscore/dbs/DBObjectClasses.java
 create mode 100644 base/common/src/com/netscape/cmscore/dbs/DBRecord.java
 create mode 100644 base/common/src/com/netscape/cmscore/dbs/LDAPDatabase.java

diff --git a/base/common/src/com/netscape/certsrv/dbs/IDBSSession.java b/base/common/src/com/netscape/certsrv/dbs/IDBSSession.java
index a4c05f40a0df2e0a70cefb3610b7c5cb9a3c5d4a..1a1f58d60b6c7fb92d1d226e17b91c2a497fd27b 100644
--- a/base/common/src/com/netscape/certsrv/dbs/IDBSSession.java
+++ b/base/common/src/com/netscape/certsrv/dbs/IDBSSession.java
@@ -31,7 +31,7 @@ import com.netscape.certsrv.base.ISubsystem;
  *
  * @version $Revision$, $Date$
  */
-public interface IDBSSession {
+public interface IDBSSession extends AutoCloseable {
 
     /**
      * Returns database subsystem.
diff --git a/base/common/src/com/netscape/cmscore/dbs/DBAttribute.java b/base/common/src/com/netscape/cmscore/dbs/DBAttribute.java
new file mode 100644
index 0000000000000000000000000000000000000000..5cbb31388c0c690d103274d7f1417d941903f5a9
--- /dev/null
+++ b/base/common/src/com/netscape/cmscore/dbs/DBAttribute.java
@@ -0,0 +1,33 @@
+// --- BEGIN COPYRIGHT BLOCK ---
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; version 2 of the License.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+//
+// (C) 2013 Red Hat, Inc.
+// All rights reserved.
+// --- END COPYRIGHT BLOCK ---
+
+package com.netscape.cmscore.dbs;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import com.netscape.certsrv.dbs.IDBAttrMapper;
+
+/**
+ * @author Endi S. Dewata
+ */
+@Retention(RetentionPolicy.RUNTIME)
+public @interface DBAttribute {
+    public String name();
+    public Class<? extends IDBAttrMapper> mapper() default StringMapper.class;
+}
diff --git a/base/common/src/com/netscape/cmscore/dbs/DBObjectClasses.java b/base/common/src/com/netscape/cmscore/dbs/DBObjectClasses.java
new file mode 100644
index 0000000000000000000000000000000000000000..8920a4f97b6ca2f8486cf02ac3b3272e949bce45
--- /dev/null
+++ b/base/common/src/com/netscape/cmscore/dbs/DBObjectClasses.java
@@ -0,0 +1,30 @@
+// --- BEGIN COPYRIGHT BLOCK ---
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; version 2 of the License.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, write to the Free Software Foundation, Inc.,
+// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+//
+// (C) 2013 Red Hat, Inc.
+// All rights reserved.
+// --- END COPYRIGHT BLOCK ---
+
+package com.netscape.cmscore.dbs;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * @author Endi S. Dewata
+ */
+@Retention(RetentionPolicy.RUNTIME)
+public @interface DBObjectClasses {
+    public String[] value();
+}
diff --git a/base/common/src/com/netscape/cmscore/dbs/DBRecord.java b/base/common/src/com/netscape/cmscore/dbs/DBRecord.java
new file mode 100644
index 0000000000000000000000000000000000000000..8d62b00d2244af44307fabccc34079a0a1b88229
--- /dev/null
+++ b/base/common/src/com/netscape/cmscore/dbs/DBRecord.java
@@ -0,0 +1,98 @@
+package com.netscape.cmscore.dbs;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Enumeration;
+import java.util.Vector;
+
+import com.netscape.certsrv.base.EBaseException;
+import com.netscape.certsrv.dbs.IDBObj;
+
+public class DBRecord implements IDBObj {
+
+    private static final long serialVersionUID = 1L;
+
+    @Override
+    public void set(String name, Object value) throws EBaseException {
+        try {
+            // find setter
+            String setter = "set" + Character.toUpperCase(name.charAt(0)) + name.substring(1);
+            for (Method method : getClass().getMethods()) {
+                if (!method.getName().equals(setter)) continue;
+
+                // invoke setter
+                method.invoke(this, value);
+                return;
+            }
+
+            // if setter not available, set field directly
+            Field field = getClass().getField(name);
+            field.set(this, value);
+
+        } catch (InvocationTargetException|NoSuchFieldException|IllegalAccessException e) {
+            throw new EBaseException(e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public Object get(String name) throws EBaseException {
+        try {
+            // find getter
+            String getter = "get" + Character.toUpperCase(name.charAt(0)) + name.substring(1);
+            for (Method method : getClass().getMethods()) {
+                if (!method.getName().equals(getter)) continue;
+
+                // invoke getter
+                return method.invoke(this);
+            }
+
+            // if getter not available, get field directly
+            Field field = getClass().getField(name);
+            return field.get(this);
+
+        } catch (InvocationTargetException|NoSuchFieldException|IllegalAccessException e) {
+            throw new EBaseException(e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public void delete(String name) throws EBaseException {
+        set(name, null);
+    }
+
+    @Override
+    public Enumeration<String> getElements() {
+        return getSerializableAttrNames();
+    }
+
+    @Override
+    public Enumeration<String> getSerializableAttrNames() {
+        Vector<String> list = new Vector<String>();
+
+        // get attributes defined in setters/getters
+        for (Method method : getClass().getMethods()) {
+            DBAttribute dbAttribute = method.getAnnotation(DBAttribute.class);
+            if (dbAttribute == null) continue;
+
+            String name = method.getName();
+            if (!name.matches("^set.+") && !name.matches("^get.+")) continue;
+
+            // get attribute name from method name
+            name = Character.toLowerCase(name.charAt(3)) + name.substring(4);
+            list.add(name);
+        }
+
+        // get attributes defined in fields
+        for (Field field : getClass().getFields()) {
+            DBAttribute dbAttribute = field.getAnnotation(DBAttribute.class);
+            if (dbAttribute == null) continue;
+
+            String name = field.getName();
+            list.add(name);
+        }
+
+        return list.elements();
+    }
+
+}
\ No newline at end of file
diff --git a/base/common/src/com/netscape/cmscore/dbs/Database.java b/base/common/src/com/netscape/cmscore/dbs/Database.java
index 5128298e67e69bb5ee2829e25e4dd2408d5fc94e..3df2d59193975b171f36d9e3d26d1f69cb247f52 100644
--- a/base/common/src/com/netscape/cmscore/dbs/Database.java
+++ b/base/common/src/com/netscape/cmscore/dbs/Database.java
@@ -22,9 +22,10 @@ import java.util.Collection;
 import java.util.LinkedHashMap;
 import java.util.Map;
 
+import com.netscape.certsrv.apps.CMS;
+
 /**
- * This class implements in-memory token database. In the future this
- * will be replaced with LDAP database.
+ * This class implements in-memory database.
  *
  * @author Endi S. Dewata
  */
@@ -32,11 +33,14 @@ public class Database<E> {
 
     public final static int DEFAULT_SIZE = 20;
 
-    public String name ;
+    public String name;
+
     public Map<String, E> records = new LinkedHashMap<String, E>();
 
     public Database(String name) {
         this.name = name;
+
+        CMS.debug("Initializing " + name + " database");
     }
 
     public Collection<E> getRecords() throws Exception {
diff --git a/base/common/src/com/netscape/cmscore/dbs/LDAPDatabase.java b/base/common/src/com/netscape/cmscore/dbs/LDAPDatabase.java
new file mode 100644
index 0000000000000000000000000000000000000000..9e1e5186cf679c4d108641862eb11c86cf599c63
--- /dev/null
+++ b/base/common/src/com/netscape/cmscore/dbs/LDAPDatabase.java
@@ -0,0 +1,167 @@
+package com.netscape.cmscore.dbs;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Enumeration;
+
+import com.netscape.certsrv.apps.CMS;
+import com.netscape.certsrv.base.EBaseException;
+import com.netscape.certsrv.dbs.IDBAttrMapper;
+import com.netscape.certsrv.dbs.IDBObj;
+import com.netscape.certsrv.dbs.IDBRegistry;
+import com.netscape.certsrv.dbs.IDBSSession;
+import com.netscape.certsrv.dbs.IDBSearchResults;
+import com.netscape.certsrv.dbs.IDBSubsystem;
+import com.netscape.certsrv.dbs.Modification;
+import com.netscape.certsrv.dbs.ModificationSet;
+
+/**
+ * This class implements LDAP database.
+ *
+ * @author Endi S. Dewata
+ */
+public abstract class LDAPDatabase<E extends IDBObj> extends Database<E> {
+
+    public IDBSubsystem dbSubsystem;
+    public String baseDN;
+    public Class<E> type;
+
+    public LDAPDatabase(String name, IDBSubsystem dbSubsystem, String baseDN, Class<E> type) throws EBaseException {
+        super(name);
+
+        this.dbSubsystem = dbSubsystem;
+        this.baseDN = baseDN;
+        this.type = type;
+
+        register(type);
+    }
+
+    public void register(Class<E> type) throws EBaseException {
+        try {
+            IDBRegistry dbRegistry = dbSubsystem.getRegistry();
+
+            // register object classes
+            DBObjectClasses dbObjectClasses = type.getAnnotation(DBObjectClasses.class);
+            if (dbObjectClasses == null) {
+                throw new EBaseException("Missing object class mapping in " + type.getName());
+            }
+            dbRegistry.registerObjectClass(type.getName(), dbObjectClasses.value());
+
+            // register attributes defined in setters/getters
+            for (Method method : type.getMethods()) {
+                DBAttribute dbAttribute = method.getAnnotation(DBAttribute.class);
+                if (dbAttribute == null) continue;
+
+                String name = method.getName();
+                if (!name.matches("^set.+") && !name.matches("^get.+")) continue;
+
+                // get attribute name from method name
+                name = Character.toLowerCase(name.charAt(3)) + name.substring(4);
+
+                String attrName = dbAttribute.name();
+                Class<? extends IDBAttrMapper> attrMapper = dbAttribute.mapper();
+
+                Constructor<? extends IDBAttrMapper> constructor = attrMapper.getConstructor(String.class);
+                IDBAttrMapper mapper = constructor.newInstance(attrName);
+
+                dbRegistry.registerAttribute(name, mapper);
+            }
+
+            // register attributes defined in fields
+            for (Field field : type.getFields()) {
+                DBAttribute dbAttribute = field.getAnnotation(DBAttribute.class);
+                if (dbAttribute == null) continue;
+
+                String name = field.getName();
+                String attrName = dbAttribute.name();
+                Class<? extends IDBAttrMapper> attrMapper = dbAttribute.mapper();
+                CMS.debug("  " + name + " : " + attrMapper.getName() + "(\"" + attrName + "\")");
+
+                Constructor<? extends IDBAttrMapper> constructor = attrMapper.getConstructor(String.class);
+                IDBAttrMapper mapper = constructor.newInstance(attrName);
+
+                dbRegistry.registerAttribute(name, mapper);
+            }
+
+        } catch (NoSuchMethodException|InvocationTargetException|IllegalAccessException|InstantiationException|SecurityException e) {
+            throw new EBaseException(e.getMessage(), e);
+        }
+    }
+
+    public abstract String createDN(String id);
+
+    @Override
+    public Collection<E> getRecords() throws Exception {
+        CMS.debug("LDAPDatabase: getRecords()");
+
+        try (IDBSSession session = dbSubsystem.createSession()) {
+            Collection<E> list = new ArrayList<E>();
+
+            CMS.debug("LDAPDatabase: searching " + baseDN);
+            IDBSearchResults results = session.search(baseDN, null);
+
+            while (results.hasMoreElements()) {
+                Object object = results.nextElement();
+                CMS.debug("LDAPDatabase: " + object.getClass());
+            }
+
+            return list;
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public E getRecord(String id) throws Exception {
+        CMS.debug("LDAPDatabase: getRecord(\"" + id + "\")");
+        try (IDBSSession session = dbSubsystem.createSession()) {
+            String dn = createDN(id);
+            CMS.debug("LDAPDatabase: reading " + baseDN);
+            return (E)session.read(dn);
+        }
+    }
+
+    @Override
+    public void addRecord(String id, E record) throws Exception {
+        CMS.debug("LDAPDatabase: addRecord(\"" + id + "\")");
+        try (IDBSSession session = dbSubsystem.createSession()) {
+            String dn = createDN(id);
+
+            CMS.debug("LDAPDatabase: adding " + dn);
+            session.add(dn, record);
+        }
+    }
+
+    @Override
+    public void updateRecord(String id, E record) throws Exception {
+        CMS.debug("LDAPDatabase: updateRecord(\"" + id + "\")");
+        try (IDBSSession session = dbSubsystem.createSession()) {
+            String dn = createDN(id);
+
+            ModificationSet mods = new ModificationSet();
+            for (Enumeration<String> names = record.getSerializableAttrNames(); names.hasMoreElements(); ) {
+                String name = names.nextElement();
+                Object value = record.get(name);
+                mods.add(name, Modification.MOD_REPLACE, value);
+            }
+
+            CMS.debug("LDAPDatabase: modifying " + dn);
+            session.modify(dn, mods);
+        }
+    }
+
+    @Override
+    public void removeRecord(String id) throws Exception {
+        CMS.debug("LDAPDatabase: removeRecord(\"" + id + "\")");
+        try (IDBSSession session = dbSubsystem.createSession()) {
+            String dn = createDN(id);
+
+            CMS.debug("LDAPDatabase: removing " + dn);
+            session.delete(dn);
+        }
+    }
+
+}
-- 
1.8.1.4

