JsonCms.java

/***************************************************************************
   Copyright 2013 Emily Estes

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
***************************************************************************/
package net.metanotion.contentstore.json;


import java.io.IOException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

import net.metanotion.util.Dispatcher;
import net.metanotion.util.Service;
import net.metanotion.util.Unknown;
import net.metanotion.web.FileUpload;
import net.metanotion.web.HttpValues;
import net.metanotion.web.RequestObject;
import net.metanotion.web.concrete.HttpUtil;
import net.metanotion.web.concrete.JsonUtil;
import net.metanotion.web.concrete.ObjectPrefixDispatcher;
import net.metanotion.web.concrete.URIPrefixDispatcher;
import net.metanotion.web.concrete.WebInterfaceDispatcher;

import net.metanotion.contentstore.Collection;
import net.metanotion.contentstore.ContentStore;
import net.metanotion.contentstore.Entry;
import net.metanotion.contentstore.Header;
import net.metanotion.contentstore.StoreObject;
import net.metanotion.contentstore.StoreUtils;

/** This class provides a JSON oriented restful HTTP API to provide access to a
	{@link net.metanotion.contentstore.ContentStore}.
*/
public final class JsonCms {
	private static final String ENTRY_PREFIX = "entries/";
	private static final String COLLECTION_PREFIX = "collections/";
	private static final String API_PREFIX = "";
	private static final String SUCCESS = "{ \"success\": true }";
	private static final String CONTENT_TYPE = "Content-Type";
	private static final String PLAIN_TEXT = "text/plain";

	public static final String COLLECTION_ID = "collectionId";
	public static final String ENTRY_ID = "entryId";

	public static final String OFFSET = "offset";
	public static final String COUNT = "count";
	public static final String TITLE = "title";
	/** This constant is the default name of the field for file upload forms used by the js/iframeFormManager.js library. */
	public static final String FILE_RESPONSE_ID = "IFRAMERESPONSEID";
	public static final String ENTRY = "entry";
	public static final String RO = "ro";

	/** This interface is used to enumerate the destinations an entry can be moved to in the api. */
	public interface EntryDestinations {
		/** Given an entry, enumerate destinations the entry can be sent to.
			@param e The entry to be moved.
			@return A list of collections the entry can be moved to.
		*/
		public Iterable<Header> destinations(Entry e);
	}

	private static final List<Header> EMPTY = Collections.emptyList();
	private static final EntryDestinations DEFAULT = new EntryDestinations() {
		@Override public Iterable<Header> destinations(final Entry e) { return EMPTY; }
	};

	/** The session object for an instance of the CMS. This session exposes implementations of the API. */
	public final class Session {
		private final class JP implements ApiStore {
			@Override public Object api() {
				return JsonUtil.makeWebApi(ApiStore.class, prefix + API_PREFIX);
			}

			@Override public Object createCollection(final String title) {
				return JsonUtil.makeWebApi(ApiCollection.class, getCollectionPrefix(store.makeCollection(title).oid()));
			}

			@Override public Object getCollection(final String title) {
				return JsonUtil.makeWebApi(ApiCollection.class, getCollectionPrefix(store.getCollection(title).oid()));
			}

			@Override public HttpValues get(final long oid) { return StoreUtils.get(store.getEntry(oid)); }

			@Override public HeaderList destinations(final long oid) {
				final LinkedList<HeaderItem> items = new LinkedList<>();
				int ct = 0;
				for(final Header h: selector.destinations(store.getEntry(oid))) {
					items.add(new HeaderItem(h.Title, h.id,
						JsonUtil.makeWebApi(ApiCollection.class, getCollectionPrefix(h.id)), ct));
					ct++;
				}
				return new HeaderList(items);
			}

			@Override public Object move(final long oid, final long collectionId) {
				store.getEntry(oid).move(store.getCollection(collectionId));
				return SUCCESS;
			}

			@Override public Object lookupEntry(final long oid) {
				return JsonUtil.makeWebApi(ApiEntry.class, getEntryPrefix(oid));
			}
		}

		private final class C implements ApiCollection {
			@Override public Object api(final long oid) {
				return JsonUtil.makeWebApi(ApiCollection.class, getCollectionPrefix(oid));
			}

			@Override public HeaderList list(final long oid, final String offset, final int count) {
				return listObject(store.getCollection(oid), ApiEntry.class, prefix + ENTRY_PREFIX, offset, count);
			}

			@Override public Object remove(final long oid) {
				store.getCollection(oid).delete();
				return SUCCESS;
			}

			@Override public Object updateTitle(final long oid, final String title) {
				store.getCollection(oid).setTitle(title);
				return SUCCESS;
			}

			@Override public Object append(final long oid, final String title) {
				final Entry e = store.getCollection(oid).append(title);
				return JsonUtil.makeWebApi(ApiEntry.class, getEntryPrefix(e.oid()));
			}

			@Override public Object create(final long oid, final FileUpload entry, final RequestObject ro) {
				final String filename = entry.getClientFilename();
				store.getCollection(oid).append(filename).update(entry, filename, ro.getHeader(CONTENT_TYPE));
				return SUCCESS;
			}

			@Override public Object createText(final long oid, final String title, final String entry) {
				final Entry e = store.getCollection(oid).append(title).update(entry, PLAIN_TEXT);
				return JsonUtil.makeWebApi(ApiEntry.class, getEntryPrefix(e.oid()));
			}

			@Override public Object attach(final long oid, final long entry) {
				store.getCollection(oid).attach(store.getEntry(entry));
				return SUCCESS;
			}

			@Override public Object detach(final long oid, final long entry) {
				store.getCollection(oid).detach(store.getEntry(entry));
				return SUCCESS;
			}
		}

		private final class E implements ApiEntry {
			@Override public HttpValues get(final long oid) { return StoreUtils.get(store.getEntry(oid)); }

			@Override public Object api(final long oid) {
				return JsonUtil.makeWebApi(ApiEntry.class, getEntryPrefix(oid));
			}

			@Override public HeaderList list(final long oid, final String offset, final int count) {
				return listObject(store.getEntry(oid), ApiCollection.class, prefix + COLLECTION_PREFIX, offset, count);
			}

			@Override public Object remove(final long oid) {
				store.getEntry(oid).delete();
				return SUCCESS;
			}

			@Override public Object updateTitle(final long oid, final String title) {
				store.getEntry(oid).setTitle(title);
				return SUCCESS;
			}

			@Override public Object append(final long oid, final String title) {
				final Collection c = store.getEntry(oid).append(title);
				return JsonUtil.makeWebApi(ApiCollection.class, getCollectionPrefix(c.oid()));
			}

			@Override public Object update(final long oid, final FileUpload entry, final RequestObject ro) {
				store.getEntry(oid).update(entry, entry.getClientFilename(), ro.getHeader(CONTENT_TYPE));
				return SUCCESS;
			}

			@Override public Object updateText(final long oid, final String title, final String entry) {
				store.getEntry(oid).setTitle(title).update(entry, PLAIN_TEXT);
				return SUCCESS;
			}

			@Override public Object move(final long oid, final long collectionId) {
				store.getEntry(oid).move(store.getCollection(collectionId));
				return SUCCESS;
			}

			@Override public HttpValues attach(final long oid,
					final String responseTag,
					final FileUpload entry,
					final RequestObject ro) {
				final String attachmentStream = "Entries/" + Long.toString(oid) + "/attachments";
				Collection c;
				try {
					c = store.getCollection(attachmentStream);
				} catch (final Exception ex) {
					c = store.getEntry(oid).append(attachmentStream);
				}
				final String filename = entry.getClientFilename();
				final Entry newEntry = c.append(filename).update(entry, filename, ro.getHeader(CONTENT_TYPE));
				return HttpUtil.newHtmlString("<!DOCTYPE html><html><head><meta charset=\"UTF-8\">" +
					"<script>window.parent.iframeLoadDequeue('" + responseTag + "', { api: " +
					JsonUtil.makeWebApi(ApiEntry.class, getEntryPrefix(newEntry.oid())).toString() +
					", file: " + Long.toString(newEntry.oid()) + " });</script>" +
					"</head><body></body></html>");
			}
		}

		@Service public final ApiCollection collections = new C();
		@Service public final ApiEntry entries = new E();
		@Service public final ApiStore cms = new JP();

		private final ContentStore store;
		private final EntryDestinations selector;
		private Session(final ContentStore store, final EntryDestinations selector) {
			this.store = store;
			this.selector = selector;
		}
	}

	private final String prefix;

	/** Create an instance of the HTTP contentstore API at the given URI prefix.
		@param prefix The URI prefix this content store is located at.
	*/
	public JsonCms(final String prefix) { this.prefix = prefix; }

	/** Provide a dispatcher suitable for processing HTTP requests against this content store.
		@return A dispatcher.
		@throws IOException if there was an issue generating the dispatcher.
	*/
	public Dispatcher<? extends Object,RequestObject> dispatcher() throws IOException {
		return new URIPrefixDispatcher()
			.addDispatcher(ENTRY_PREFIX,
				new ObjectPrefixDispatcher<Unknown>(
					new WebInterfaceDispatcher<>(ApiEntry.class),
					ENTRY_ID))
			.addDispatcher(COLLECTION_PREFIX,
				new ObjectPrefixDispatcher<Unknown>(
					new WebInterfaceDispatcher<>(ApiCollection.class),
					COLLECTION_ID))
			.addDispatcher(API_PREFIX, new WebInterfaceDispatcher<>(ApiStore.class));
	}

	/** Create a new session/service instance backed by the provided content store.
		Note: you can expose this session object's services easily via annotating the instance variable with the
		{@link net.metanotion.util.Extends} annotation and the {@link net.metanotion.util.UnknownMagic} helper.
		@param store The content store instance backing this session.
		@return A session object providing implementations of the messages dispatched by the library's provided
			dispatcher.
	*/
	public Session newSession(final ContentStore store) { return new Session(store, DEFAULT); }

	/** Create a new session/service instance backed by the provided content store.
		Note: you can expose this session object's services easily via annotating the instance variable with the
		{@link net.metanotion.util.Extends} annotation and the {@link net.metanotion.util.UnknownMagic} helper.
		@param store The content store instance backing this session.
		@param selector The EntryDestinations implementation to use for this session.
		@return A session object providing implementations of the messages dispatched by the library's provided
			dispatcher.
	*/
	public Session newSession(final ContentStore store, final EntryDestinations selector) {
		return new Session(store, selector);
	}

	private static final String SLASH = "/";
	private String getCollectionPrefix(final long oid) { return prefix + COLLECTION_PREFIX + Long.toString(oid) + SLASH; }
	private String getEntryPrefix(final long oid) { return prefix + ENTRY_PREFIX + Long.toString(oid) + SLASH; }

	private static HeaderList listObject(final StoreObject o,
			final Class klazz,
			final String prefix,
			final String rawOffset,
			final int count) {
		int offset = 0;
		try { offset = Integer.parseInt(rawOffset); } catch (final NumberFormatException nfe) { }
		final LinkedList<HeaderItem> items = new LinkedList<>();
		int ct = offset;
		final Iterable<Header> list = o.header(count, offset);
		for(final Header h: list) {
			items.add(new HeaderItem(h.Title, h.id, JsonUtil.makeWebApi(klazz, prefix + Long.toString(h.id) + SLASH), ct));
			ct++;
		}
		return new HeaderList(items);
	}
}