package com.developi.sbt;
/**
* NSF-Cached Credential Store Snippet for the IBM Social Business Toolkit
* Version 1.0, (C) 2014 Serdar Basegmez
*
* This class is a derivative work of MemoryStore class from IBM Social
* Business Toolkit SDK project, licensed under Apache License V2.0.
*
* Social Business Toolkit SDK (http://ibmsbt.openntf.org)
* (c) Copyright IBM Corp. 2010, 2013
*
* Also uses MIMEBean utility methods from OpenNTF Domino API project,
* created by Nathan T Freeman, Jesse Gallagher with Jesse Gallagher,
* Tim Tripcony, Paul Withers, Declan Lynch, Rene Winklemeyer.
*
* OpenNTF Domino API (http://openntf.org/main.nsf/project.xsp?r=project/OpenNTF%20Domino%20API)
* Licensed under Apache License V2.0
*
*/
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Externalizable;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import lotus.domino.Database;
import lotus.domino.Document;
import lotus.domino.MIMEEntity;
import lotus.domino.MIMEHeader;
import lotus.domino.NotesException;
import lotus.domino.Session;
import lotus.domino.Stream;
import lotus.domino.View;
import com.ibm.commons.util.StringUtil;
import com.ibm.sbt.security.credential.store.BaseStore;
import com.ibm.sbt.security.credential.store.CredentialStoreException;
import com.ibm.xsp.extlib.util.ExtLibUtil;
public class CachedCredStore extends BaseStore {
/**
* DESCRIPTION:
*
* IBM Social Business Toolkit supports RDBMS-based persistent credential store
* for Endpoints. However it doesn't support NSF-based stores. So when you develop
* SBT applications on XPages, it would be pretty annoying that every time the
* classloader cleared (e.g.HTTP restart, Java class changes, etc.), you lose all
* credentials and tokens.
*
* This class can be used as an alternative to Memory store to persist endpoint
* information in a notes document. It simply uses a MIME stream in a rich text
* item, thanks to the brilliant idea of Tim Tripcony.
*
* Warning: This implementation is not using any security. All sensitive data would
* be accessible with a NotesClient. So use it for development and testing for now!
*
* Next version I will convert it to sessionAsSigner but still, it should be supported
* by Reader/Author fields and even with encryption.
*
* USAGE:
*
* 1. Copy the Java class into your NSF project.
*
* 2. Create a view named or aliased as "tokenstores".
* Selection for the view : SELECT form="tokenstore"
* First column, ascending sorted with "ServerName" field
*
* 3. Define the credential store in your Faces-Config file:
*
* <managed-bean-name>CredStore</managed-bean-name>
* <managed-bean-class>com.developi.sbt.CachedCredStore</managed-bean-class>
* <managed-bean-scope>application</managed-bean-scope>
* </managed-bean>
*
* 4. Use CredStore in your endpoint declarations
*/
private Map<String,byte[]> map = new HashMap<String,byte[]>();
public CachedCredStore() {
loadStore();
}
public Object load(String service, String type, String user) throws CredentialStoreException {
String application = findApplicationName();
String key = createKey(application, service, type, user);
return deSerialize(map.get(key));
}
public void store(String service, String type, String user, Object credentials) throws CredentialStoreException {
String application = findApplicationName();
String key = createKey(application, service, type, user);
map.put(key, serialize(credentials) );
saveStore();
}
public void remove(String service, String type, String user) throws CredentialStoreException {
String application = findApplicationName();
String key = createKey(application, service, type, user);
map.remove(key);
saveStore();
}
/**
* Create a key for the internal map.
*/
protected String createKey(String application, String service, String type, String user) throws CredentialStoreException {
StringBuilder b = new StringBuilder(128);
b.append(StringUtil.getNonNullString(application));
b.append('|');
b.append(StringUtil.getNonNullString(service));
b.append('|');
b.append(StringUtil.getNonNullString(type));
b.append('|');
b.append(StringUtil.getNonNullString(user));
return b.toString();
}
@SuppressWarnings("unchecked")
protected void loadStore() {
Document doc=getStoreDoc();
if(doc==null) return;
try {
Object obj=restoreState(doc, "CredStore");
if(obj instanceof Map) {
Iterator it = ((Map)obj).entrySet().iterator();
while (it.hasNext()) {
Map.Entry pairs = (Map.Entry)it.next();
if(pairs.getValue() instanceof byte[]) {
map.put((String)pairs.getKey(), (byte[])pairs.getValue());
}
it.remove(); // avoids a ConcurrentModificationException
}
}
} catch (Throwable t) {
System.out.println("Error loading tokenstore: "+t.getMessage());
t.printStackTrace(System.err);
} finally {
if(doc!=null) {
try {
doc.recycle();
} catch (NotesException e) { }
}
}
System.out.println("Token store loaded...");
}
protected void saveStore() {
Document doc=getStoreDoc();
if(doc==null) return;
try {
saveState((HashMap<String,byte[]>)map, doc, "CredStore");
doc.save();
} catch(Throwable t) {
System.out.println("Error saving tokenstore: "+t.getMessage());
t.printStackTrace(System.err);
} finally {
if(doc!=null) {
try {
doc.recycle();
} catch (NotesException e) { }
}
}
}
protected Document getStoreDoc() {
Database database=ExtLibUtil.getCurrentDatabase();
Document doc=null;
View view=null;
try {
view=database.getView("tokenstores");
if(view==null) {
System.out.println("Unable to find 'tokenstores' view...");
return null;
}
String serverName=database.getServer();
doc=view.getDocumentByKey(serverName, true);
if(doc==null) {
doc=database.createDocument();
doc.replaceItemValue("Form", "tokenstore");
doc.replaceItemValue("ServerName", serverName);
doc.computeWithForm(false, false);
doc.save();
}
} catch (NotesException e) {
e.printStackTrace();
} finally {
if(view!=null) {
try {
view.recycle();
} catch (NotesException e) { }
}
}
return doc;
}
// MIMEBean methods
/**
* Restore state. Imported from org.openntf.domino
*
* @param doc
* the doc
* @param itemName
* the item name
* @return the serializable
* @throws Throwable
* the throwable
*/
@SuppressWarnings("unchecked")
public static Object restoreState(Document doc, String itemName) throws Throwable {
Session session=doc.getParentDatabase().getParent();
boolean convertMime = session.isConvertMime();
session.setConvertMime(false);
Object result = null;
MIMEEntity entity = doc.getMIMEEntity(itemName);
if(null==entity) return null;
Stream mimeStream = session.createStream();
entity.getContentAsBytes(mimeStream);
ByteArrayOutputStream streamOut = new ByteArrayOutputStream();
mimeStream.getContents(streamOut);
mimeStream.recycle();
byte[] stateBytes = streamOut.toByteArray();
ByteArrayInputStream byteStream = new ByteArrayInputStream(stateBytes);
ObjectInputStream objectStream;
if (entity.getHeaders().toLowerCase().contains("content-encoding: gzip")) {
GZIPInputStream zipStream = new GZIPInputStream(byteStream);
objectStream = new ObjectInputStream(zipStream);
} else {
objectStream = new ObjectInputStream(byteStream);
}
// There are three potential storage forms: Externalizable, Serializable, and StateHolder, distinguished by type or header
if(entity.getContentSubType().equals("x-java-externalized-object")) {
Class<Externalizable> externalizableClass = (Class<Externalizable>)Class.forName(entity.getNthHeader("X-Java-Class").getHeaderVal());
Externalizable restored = externalizableClass.newInstance();
restored.readExternal(objectStream);
result = restored;
} else {
Object restored = (Serializable) objectStream.readObject();
// But wait! It might be a StateHolder object or Collection!
MIMEHeader storageScheme = entity.getNthHeader("X-Storage-Scheme");
MIMEHeader originalJavaClass = entity.getNthHeader("X-Original-Java-Class");
if(storageScheme != null && storageScheme.getHeaderVal().equals("StateHolder")) {
Class<?> facesContextClass = Class.forName("javax.faces.context.FacesContext");
Method getCurrentInstance = facesContextClass.getMethod("getCurrentInstance");
Class<?> stateHoldingClass = (Class<?>)Class.forName(originalJavaClass.getHeaderVal());
Method restoreStateMethod = stateHoldingClass.getMethod("restoreState", facesContextClass, Object.class);
result = stateHoldingClass.newInstance();
restoreStateMethod.invoke(result, getCurrentInstance.invoke(null), restored);
} else {
result = restored;
}
}
if(entity!=null) {
entity.recycle();
}
session.setConvertMime(convertMime);
return result;
}
/**
* Save state. Imported from org.openntf.domino
*
* @param object
* the object
* @param doc
* the doc
* @param itemName
* the item name
* @throws Throwable
* the throwable
*/
private static void saveState(Serializable object, Document doc, String itemName) throws Throwable {
saveState(object, doc, itemName, true, null);
}
/**
* Save state. Imported from org.openntf.domino
*
* @param object
* the object
* @param doc
* the doc
* @param itemName
* the item name
* @param compress
* the compress
* @throws Throwable
* the throwable
*/
private static void saveState(Serializable object, Document doc, String itemName, boolean compress, Map<String, String> headers) throws Throwable {
Session session=doc.getParentDatabase().getParent();
boolean convertMime = session.isConvertMime();
session.setConvertMime(false);
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
ObjectOutputStream objectStream = compress ? new ObjectOutputStream(new GZIPOutputStream(byteStream)) : new ObjectOutputStream(
byteStream);
String contentType = null;
// Prefer externalization if available
if(object instanceof Externalizable) {
((Externalizable)object).writeExternal(objectStream);
contentType = "application/x-java-externalized-object";
} else {
objectStream.writeObject(object);
contentType = "application/x-java-serialized-object";
}
objectStream.flush();
objectStream.close();
Stream mimeStream = session.createStream();
MIMEEntity previousState = doc.getMIMEEntity(itemName);
MIMEEntity entity = previousState == null ? doc.createMIMEEntity(itemName) : previousState;
ByteArrayInputStream byteIn = new ByteArrayInputStream(byteStream.toByteArray());
mimeStream.setContents(byteIn);
entity.setContentFromBytes(mimeStream, contentType, MIMEEntity.ENC_NONE);
MIMEHeader contentEncoding = entity.getNthHeader("Content-Encoding");
if (compress) {
if (contentEncoding == null) {
contentEncoding = entity.createHeader("Content-Encoding");
}
contentEncoding.setHeaderVal("gzip");
contentEncoding.recycle();
} else {
if (contentEncoding != null) {
contentEncoding.remove();
contentEncoding.recycle();
}
}
MIMEHeader javaClass = entity.getNthHeader("X-Java-Class");
if (javaClass == null) {
javaClass = entity.createHeader("X-Java-Class");
}
javaClass.setHeaderVal(object.getClass().getName());
javaClass.recycle();
if(headers != null) {
for(Map.Entry<String, String> entry : headers.entrySet()) {
MIMEHeader paramHeader = entity.getNthHeader(entry.getKey());
if(paramHeader == null) {
paramHeader = entity.createHeader(entry.getKey());
}
paramHeader.setHeaderVal(entry.getValue());
paramHeader.recycle();
}
}
entity.recycle();
mimeStream.recycle();
session.setConvertMime(convertMime);
}
}