From 0acc9e978a93e1127f548f3e1085adf5b56d6dfe Mon Sep 17 00:00:00 2001 From: mposolda Date: Fri, 30 Aug 2013 17:38:54 +0200 Subject: [PATCH] Added first version of NoSQL api and MongoDBImpl implementation --- pom.xml | 11 +- services/pom.xml | 8 + .../models/nosql/adapters/NoSQLRealm.java | 49 +++++ .../models/nosql/adapters/NoSQLUser.java | 46 ++++ .../nosql/api/AttributedNoSQLObject.java | 17 ++ .../services/models/nosql/api/NoSQL.java | 26 +++ .../models/nosql/api/NoSQLCollection.java | 21 ++ .../services/models/nosql/api/NoSQLField.java | 22 ++ .../services/models/nosql/api/NoSQLId.java | 18 ++ .../models/nosql/api/NoSQLObject.java | 9 + .../models/nosql/impl/MongoDBImpl.java | 203 ++++++++++++++++++ .../models/nosql/impl/ObjectInfo.java | 53 +++++ .../services/models/nosql/impl/Test.java | 53 +++++ 13 files changed, 533 insertions(+), 3 deletions(-) create mode 100644 services/src/main/java/org/keycloak/services/models/nosql/adapters/NoSQLRealm.java create mode 100644 services/src/main/java/org/keycloak/services/models/nosql/adapters/NoSQLUser.java create mode 100644 services/src/main/java/org/keycloak/services/models/nosql/api/AttributedNoSQLObject.java create mode 100644 services/src/main/java/org/keycloak/services/models/nosql/api/NoSQL.java create mode 100644 services/src/main/java/org/keycloak/services/models/nosql/api/NoSQLCollection.java create mode 100644 services/src/main/java/org/keycloak/services/models/nosql/api/NoSQLField.java create mode 100644 services/src/main/java/org/keycloak/services/models/nosql/api/NoSQLId.java create mode 100644 services/src/main/java/org/keycloak/services/models/nosql/api/NoSQLObject.java create mode 100644 services/src/main/java/org/keycloak/services/models/nosql/impl/MongoDBImpl.java create mode 100644 services/src/main/java/org/keycloak/services/models/nosql/impl/ObjectInfo.java create mode 100644 services/src/main/java/org/keycloak/services/models/nosql/impl/Test.java diff --git a/pom.xml b/pom.xml index d08e744eeb..99a90cd930 100755 --- a/pom.xml +++ b/pom.xml @@ -236,9 +236,9 @@ com.icegreen greenmail 1.3.1b - + - + org.seleniumhq.selenium selenium-java @@ -248,7 +248,12 @@ org.seleniumhq.selenium selenium-chrome-driver 2.35.0 - + + + org.mongodb + mongo-java-driver + 2.11.2 + diff --git a/services/pom.xml b/services/pom.xml index 4d8075f8cc..7c38f45478 100755 --- a/services/pom.xml +++ b/services/pom.xml @@ -144,6 +144,14 @@ jackson-xc provided + + org.picketlink + picketlink-common + + + org.mongodb + mongo-java-driver + junit junit diff --git a/services/src/main/java/org/keycloak/services/models/nosql/adapters/NoSQLRealm.java b/services/src/main/java/org/keycloak/services/models/nosql/adapters/NoSQLRealm.java new file mode 100644 index 0000000000..c061f8b8c3 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/models/nosql/adapters/NoSQLRealm.java @@ -0,0 +1,49 @@ +package org.keycloak.services.models.nosql.adapters; + +import org.keycloak.services.models.nosql.api.NoSQLCollection; +import org.keycloak.services.models.nosql.api.NoSQLField; +import org.keycloak.services.models.nosql.api.NoSQLId; +import org.keycloak.services.models.nosql.api.NoSQLObject; + +/** + * @author Marek Posolda + */ +@NoSQLCollection(collectionName = "realms") +public class NoSQLRealm implements NoSQLObject { + + private String oid; + private String prop1; + private Integer prop2; + + @NoSQLId + public String getOid() { + return oid; + } + + public void setOid(String oid) { + this.oid = oid; + } + + @NoSQLField(fieldName = "property1") + public String getProp1() { + return prop1; + } + + public void setProp1(String prop1) { + this.prop1 = prop1; + } + + @NoSQLField(fieldName = "property2") + public Integer getProp2() { + return prop2; + } + + public void setProp2(Integer prop2) { + this.prop2 = prop2; + } + + @Override + public String toString() { + return "NoSQLRealm [ oid=" + oid + ", prop1=" + prop1 + ", prop2=" + prop2 + "]"; + } +} diff --git a/services/src/main/java/org/keycloak/services/models/nosql/adapters/NoSQLUser.java b/services/src/main/java/org/keycloak/services/models/nosql/adapters/NoSQLUser.java new file mode 100644 index 0000000000..5c316fff4b --- /dev/null +++ b/services/src/main/java/org/keycloak/services/models/nosql/adapters/NoSQLUser.java @@ -0,0 +1,46 @@ +package org.keycloak.services.models.nosql.adapters; + +import org.keycloak.services.models.nosql.api.NoSQLCollection; +import org.keycloak.services.models.nosql.api.NoSQLField; +import org.keycloak.services.models.nosql.api.NoSQLId; + +/** + * @author Marek Posolda + */ +@NoSQLCollection(collectionName = "users") +public class NoSQLUser { + + @NoSQLId + private String oid; + + private String username; + + private String realmId; + + @NoSQLId + public String getOid() { + return oid; + } + + public void setOid(String oid) { + this.oid = oid; + } + + @NoSQLField + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + @NoSQLField(fieldName = "realm_id") + public String getRealmId() { + return realmId; + } + + public void setRealmId(String realmId) { + this.realmId = realmId; + } +} diff --git a/services/src/main/java/org/keycloak/services/models/nosql/api/AttributedNoSQLObject.java b/services/src/main/java/org/keycloak/services/models/nosql/api/AttributedNoSQLObject.java new file mode 100644 index 0000000000..f750e82a8c --- /dev/null +++ b/services/src/main/java/org/keycloak/services/models/nosql/api/AttributedNoSQLObject.java @@ -0,0 +1,17 @@ +package org.keycloak.services.models.nosql.api; + +import java.util.Map; + +/** + * @author Marek Posolda + */ +public interface AttributedNoSQLObject extends NoSQLObject { + + void setAttribute(String name, String value); + + void removeAttribute(String name); + + String getAttribute(String name); + + Map getAttributes(); +} diff --git a/services/src/main/java/org/keycloak/services/models/nosql/api/NoSQL.java b/services/src/main/java/org/keycloak/services/models/nosql/api/NoSQL.java new file mode 100644 index 0000000000..dc16bb6ec1 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/models/nosql/api/NoSQL.java @@ -0,0 +1,26 @@ +package org.keycloak.services.models.nosql.api; + +import java.util.List; +import java.util.Map; + +/** + * @author Marek Posolda + */ +public interface NoSQL { + + /** + * Insert object if it's oid is null. Otherwise update + */ + void saveObject(NoSQLObject object); + + T loadObject(Class type, String oid); + + List loadObjects(Class type, Map queryAttributes); + + // Object must have filled oid + void removeObject(NoSQLObject object); + + void removeObject(Class type, String oid); + + void removeObjects(Class type, Map queryAttributes); +} diff --git a/services/src/main/java/org/keycloak/services/models/nosql/api/NoSQLCollection.java b/services/src/main/java/org/keycloak/services/models/nosql/api/NoSQLCollection.java new file mode 100644 index 0000000000..ff41188736 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/models/nosql/api/NoSQLCollection.java @@ -0,0 +1,21 @@ +package org.keycloak.services.models.nosql.api; + +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * @author Marek Posolda + */ +@Target({TYPE}) +@Documented +@Retention(RUNTIME) +@Inherited +public @interface NoSQLCollection { + + String collectionName(); +} diff --git a/services/src/main/java/org/keycloak/services/models/nosql/api/NoSQLField.java b/services/src/main/java/org/keycloak/services/models/nosql/api/NoSQLField.java new file mode 100644 index 0000000000..c3e0586d2f --- /dev/null +++ b/services/src/main/java/org/keycloak/services/models/nosql/api/NoSQLField.java @@ -0,0 +1,22 @@ +package org.keycloak.services.models.nosql.api; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * @author Marek Posolda + */ +@Target({METHOD, FIELD}) +@Documented +@Retention(RUNTIME) +public @interface NoSQLField { + + String fieldName() default ""; + + // TODO: add lazy loading? +} diff --git a/services/src/main/java/org/keycloak/services/models/nosql/api/NoSQLId.java b/services/src/main/java/org/keycloak/services/models/nosql/api/NoSQLId.java new file mode 100644 index 0000000000..0cbca8542f --- /dev/null +++ b/services/src/main/java/org/keycloak/services/models/nosql/api/NoSQLId.java @@ -0,0 +1,18 @@ +package org.keycloak.services.models.nosql.api; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * @author Marek Posolda + */ +@Target({METHOD, FIELD}) +@Documented +@Retention(RUNTIME) +public @interface NoSQLId { +} diff --git a/services/src/main/java/org/keycloak/services/models/nosql/api/NoSQLObject.java b/services/src/main/java/org/keycloak/services/models/nosql/api/NoSQLObject.java new file mode 100644 index 0000000000..1f430b6f5a --- /dev/null +++ b/services/src/main/java/org/keycloak/services/models/nosql/api/NoSQLObject.java @@ -0,0 +1,9 @@ +package org.keycloak.services.models.nosql.api; + +/** + * Just marker interface + * + * @author Marek Posolda + */ +public interface NoSQLObject { +} diff --git a/services/src/main/java/org/keycloak/services/models/nosql/impl/MongoDBImpl.java b/services/src/main/java/org/keycloak/services/models/nosql/impl/MongoDBImpl.java new file mode 100644 index 0000000000..bbdd84685c --- /dev/null +++ b/services/src/main/java/org/keycloak/services/models/nosql/impl/MongoDBImpl.java @@ -0,0 +1,203 @@ +package org.keycloak.services.models.nosql.impl; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import com.mongodb.BasicDBObject; +import com.mongodb.DB; +import com.mongodb.DBCollection; +import com.mongodb.DBCursor; +import com.mongodb.DBObject; +import org.bson.types.ObjectId; +import org.jboss.resteasy.logging.Logger; +import org.keycloak.services.managers.RealmManager; +import org.keycloak.services.models.nosql.api.AttributedNoSQLObject; +import org.keycloak.services.models.nosql.api.NoSQL; +import org.keycloak.services.models.nosql.api.NoSQLCollection; +import org.keycloak.services.models.nosql.api.NoSQLField; +import org.keycloak.services.models.nosql.api.NoSQLId; +import org.keycloak.services.models.nosql.api.NoSQLObject; +import org.picketlink.common.properties.Property; +import org.picketlink.common.properties.query.AnnotatedPropertyCriteria; +import org.picketlink.common.properties.query.PropertyQueries; + +/** + * @author Marek Posolda + */ +public class MongoDBImpl implements NoSQL { + + private final DB database; + // private static final Logger logger = Logger.getLogger(MongoDBImpl.class); + + public MongoDBImpl(DB database) { + this.database = database; + } + + private ConcurrentMap, ObjectInfo> objectInfoCache = + new ConcurrentHashMap, ObjectInfo>(); + + + @Override + public void saveObject(NoSQLObject object) { + Class clazz = object.getClass(); + + // Find annotations for ID, for all the properties and for the name of the collection. + ObjectInfo objectInfo = getObjectInfo(clazz); + + // Create instance of BasicDBObject and add all declared properties to it (properties with null value probably should be skipped) + BasicDBObject dbObject = new BasicDBObject(); + List> props = objectInfo.getProperties(); + for (Property property : props) { + String propName = property.getName(); + Object propValue = property.getValue(object); + + + dbObject.append(propName, propValue); + + // Adding attributes + if (object instanceof AttributedNoSQLObject) { + AttributedNoSQLObject attributedObject = (AttributedNoSQLObject)object; + Map attributes = attributedObject.getAttributes(); + for (Map.Entry attribute : attributes.entrySet()) { + dbObject.append(attribute.getKey(), attribute.getValue()); + } + } + } + + DBCollection dbCollection = database.getCollection(objectInfo.getDbCollectionName()); + + // Decide if we should insert or update (based on presence of oid property in original object) + Property oidProperty = objectInfo.getOidProperty(); + String currentId = oidProperty.getValue(object); + if (currentId == null) { + dbCollection.insert(dbObject); + + // Add oid to value of given object + oidProperty.setValue(object, dbObject.getString("_id")); + } else { + BasicDBObject setCommand = new BasicDBObject("$set", dbObject); + BasicDBObject query = new BasicDBObject("_id", new ObjectId(currentId)); + dbCollection.update(query, setCommand); + } + } + + @Override + public T loadObject(Class type, String oid) { + ObjectInfo objectInfo = getObjectInfo(type); + DBCollection dbCollection = database.getCollection(objectInfo.getDbCollectionName()); + + BasicDBObject idQuery = new BasicDBObject("_id", new ObjectId(oid)); + DBObject dbObject = dbCollection.findOne(idQuery); + + return convertObject(type, dbObject); + } + + @Override + public List loadObjects(Class type, Map queryAttributes) { + ObjectInfo objectInfo = getObjectInfo(type); + DBCollection dbCollection = database.getCollection(objectInfo.getDbCollectionName()); + + BasicDBObject query = new BasicDBObject(); + for (Map.Entry queryAttr : queryAttributes.entrySet()) { + query.append(queryAttr.getKey(), queryAttr.getValue()); + } + DBCursor cursor = dbCollection.find(query); + + return convertCursor(type, cursor); + } + + @Override + public void removeObject(NoSQLObject object) { + //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public void removeObject(Class type, String oid) { + //To change body of implemented methods use File | Settings | File Templates. + } + + @Override + public void removeObjects(Class type, Map queryAttributes) { + //To change body of implemented methods use File | Settings | File Templates. + } + + private ObjectInfo getObjectInfo(Class objectClass) { + ObjectInfo objectInfo = (ObjectInfo)objectInfoCache.get(objectClass); + if (objectInfo == null) { + Property idProperty = PropertyQueries.createQuery(objectClass).addCriteria(new AnnotatedPropertyCriteria(NoSQLId.class)).getFirstResult(); + if (idProperty == null) { + throw new IllegalStateException("Class " + objectClass + " doesn't have property with declared annotation " + NoSQLId.class); + } + + List> properties = PropertyQueries.createQuery(objectClass).addCriteria(new AnnotatedPropertyCriteria(NoSQLField.class)).getResultList(); + + NoSQLCollection classAnnotation = objectClass.getAnnotation(NoSQLCollection.class); + if (classAnnotation == null) { + throw new IllegalStateException("Class " + objectClass + " doesn't have annotation " + NoSQLCollection.class); + } + + String dbCollectionName = classAnnotation.collectionName(); + objectInfo = new ObjectInfo((Class)objectClass, dbCollectionName, idProperty, properties); + + ObjectInfo existing = objectInfoCache.putIfAbsent((Class)objectClass, objectInfo); + if (existing != null) { + objectInfo = existing; + } + } + + return objectInfo; + } + + + private T convertObject(Class type, DBObject dbObject) { + ObjectInfo objectInfo = getObjectInfo(type); + + T object; + try { + object = type.newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + + for (String key : dbObject.keySet()) { + Object value = dbObject.get(key); + Property property; + + if ("_id".equals(key)) { + // Current property is "id" + Property idProperty = objectInfo.getOidProperty(); + idProperty.setValue(object, value.toString()); + + } else if ((property = objectInfo.getPropertyByName(key)) != null) { + // It's declared property with @DBField annotation + property.setValue(object, value); + + } else if (object instanceof AttributedNoSQLObject) { + // It's attributed object and property is not declared, so we will call setAttribute + ((AttributedNoSQLObject)object).setAttribute(key, value.toString()); + + } else { + // Show warning if it's unknown + // TODO: logging + // logger.warn("Property with key " + key + " not known for type " + type); + System.err.println("Property with key " + key + " not known for type " + type); + } + } + + return object; + } + + private List convertCursor(Class type, DBCursor cursor) { + List result = new ArrayList(); + + for (DBObject dbObject : cursor) { + T converted = convertObject(type, dbObject); + result.add(converted); + } + + return result; + } +} diff --git a/services/src/main/java/org/keycloak/services/models/nosql/impl/ObjectInfo.java b/services/src/main/java/org/keycloak/services/models/nosql/impl/ObjectInfo.java new file mode 100644 index 0000000000..867ac12b62 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/models/nosql/impl/ObjectInfo.java @@ -0,0 +1,53 @@ +package org.keycloak.services.models.nosql.impl; + +import java.util.List; + +import org.keycloak.services.models.nosql.api.NoSQLObject; +import org.picketlink.common.properties.Property; + +/** + * @author Marek Posolda + */ +class ObjectInfo { + + private final Class objectClass; + + private final String dbCollectionName; + + private final Property oidProperty; + + private final List> properties; + + public ObjectInfo(Class objectClass, String dbCollectionName, Property oidProperty, List> properties) { + this.objectClass = objectClass; + this.dbCollectionName = dbCollectionName; + this.oidProperty = oidProperty; + this.properties = properties; + } + + public Class getObjectClass() { + return objectClass; + } + + public String getDbCollectionName() { + return dbCollectionName; + } + + public Property getOidProperty() { + return oidProperty; + } + + public List> getProperties() { + return properties; + } + + public Property getPropertyByName(String propertyName) { + for (Property property : properties) { + if (propertyName.equals(property.getName())) { + return property; + } + } + + return null; + } +} diff --git a/services/src/main/java/org/keycloak/services/models/nosql/impl/Test.java b/services/src/main/java/org/keycloak/services/models/nosql/impl/Test.java new file mode 100644 index 0000000000..375418ab41 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/models/nosql/impl/Test.java @@ -0,0 +1,53 @@ +package org.keycloak.services.models.nosql.impl; + +import java.net.UnknownHostException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.mongodb.DB; +import com.mongodb.MongoClient; +import org.keycloak.services.models.nosql.adapters.NoSQLRealm; + +/** + * TODO: delete + * + * @author Marek Posolda + */ +public class Test { + + public static void main(String[] args) throws UnknownHostException { + MongoClient mongoClient = new MongoClient( "localhost" , 27017 ); + DB javaDB = mongoClient.getDB("java"); + + MongoDBImpl test = new MongoDBImpl(javaDB); + NoSQLRealm realm = new NoSQLRealm(); + realm.setOid("522085fc31dab908ec31c0cb"); + realm.setProp1("something1"); + realm.setProp2(12); + test.saveObject(realm); + System.out.println(realm.getOid()); + + realm = test.loadObject(NoSQLRealm.class, "522085fc31dab908ec31c0cb"); + System.out.println("Loaded realm: " + realm); + + Map query = new HashMap(); + query.put("prop1", "sm"); + List queryResults = test.loadObjects(NoSQLRealm.class, query); + System.out.println("results1: " + queryResults); + + query.put("prop1", "something2"); + queryResults = test.loadObjects(NoSQLRealm.class, query); + System.out.println("results2: " + queryResults); + + query.put("prop2", 12); + queryResults = test.loadObjects(NoSQLRealm.class, query); + System.out.println("results3: " + queryResults); + + query.put("prop1", "something1"); + queryResults = test.loadObjects(NoSQLRealm.class, query); + System.out.println("results4: " + queryResults); + + mongoClient.close(); + } +}