NSF-Cached Credential Store for IBM Social Business Toolkit


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

}
All code submitted to OpenNTF XSnippets, whether submitted as a "Snippet" or in the body of a Comment, is provided under the Apache License Version 2.0. See Terms of Use for full details.
1 comment(s)Login first to comment...
jeniffer homes
(at 03:28 on 19.12.2015)
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.