BasicTenantExport.java

/***************************************************************************
   Copyright 2015 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.multitenant;


import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicReference;

import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.metanotion.contentstore.Collection;
import net.metanotion.contentstore.ContentStore;
import net.metanotion.contentstore.Header;
import net.metanotion.contentstore.StoreUtils;
import net.metanotion.web.HttpValues;
import net.metanotion.web.concrete.HttpUtil;

/** <p>This class is a reasonable default implementation of {@link TenantExport}. It does require your web application
instances to incorporate an {@link net.metanotion.contentstore.ContentStore} to provide storage management for the
export files produced, if this is not acceptable, you may need to consider writing your own implementation. This
implementation also relies on your application to provide an implementation of the {@link ExportImport} interface to
delegate the actual work of exporting and importing files into an instance. {@link ExportImport} is a candidate for
being factored out of the multitenant package and into a more generic package so that other tooling support can be
developed around it in the future.</p> */
public final class BasicTenantExport implements TenantExport {
	private static final Logger logger = LoggerFactory.getLogger(BasicTenantExport.class);

	private static final String INVALID_EXPORT_ID = "Invalid export id.";

	private static final class ConcurrentBoundedBuffer<V> {
		private final V[] array;

		private int start = 0;
		private int end = 0;

		public ConcurrentBoundedBuffer(final int size) {
			this.array = (V[]) new Object[size];
		}

		public synchronized void clear() {
			this.start = 0;
			this.end = 0;
		}

		public synchronized void add(final V value) {
			this.array[this.end] = value;
			this.end = (end + 1) % this.array.length;
			if (this.end == this.start) {
				this.start = (start + 1) % this.array.length;
			}
		}

		public synchronized List<V> list() {
			final ArrayList<V> ret = new ArrayList<>();
			if(this.start == this.end) {
				return ret;
			} else if(start < end) {
				for(int i=start; i < end; i++) {
					ret.add(this.array[i]);
				}
			} else {
				for(int i=start; i < this.array.length; i++) {
					ret.add(this.array[i]);
				}
				for(int i=0; i < end; i++) {
					ret.add(this.array[i]);
				}
			}
			return ret;
		}
	}

	private final ContentStore store;
	private final String backupCollection;
	private final ExecutorService queue;
	private final ExportImport tenant;
	private final AtomicReference<ConcurrentBoundedBuffer<String>> currentJob = new AtomicReference<>();

	/** Create a new instance of the TenantExport service.
		@param store The content store implementation providing durable storage for export files.
		@param backupCollection The name of the collection to use in the content stare for file storage.
		@param queue The executor service to submit export, import, and reset instance jobs to. Calls to the
			{@link ExportImport} service will be submitted to this service to allow for non-blocking behavior under the
			assumption that these operations might block interactive responses made to this implementation.
		@param tenant The application specific implementation of the {@link ExportImport} service to delegate the
			actual creation and use of export files to and to resent the application instances.
	*/
	public BasicTenantExport(final ContentStore store,
			final String backupCollection,
			final ExecutorService queue,
			final ExportImport tenant) {
		this.store = store;
		this.backupCollection = backupCollection;
		this.queue = queue;
		this.tenant = tenant;
	}

	@Override public Status status() {
		final ConcurrentBoundedBuffer<String> job = currentJob.get();
		logger.debug("Job status {}", job);
		if(job == null) {
			final List<String> e = Collections.emptyList();
			return new Status(false, e);
		} else {
			final List<String> e = job.list();
			logger.debug("List: {}", e);
			return new Status(true, e);
		}
	}

	@Override public Status initiateExport() {
		final ConcurrentBoundedBuffer<String> messageList = new ConcurrentBoundedBuffer<>(10);
		messageList.add("msg_exportStarted");
		if(currentJob.compareAndSet(null, messageList)) {
			queue.submit(new Runnable() {
				@Override public void run() {
					try {
						final Collection media = store.getCollection(backupCollection);
						final String filename = tenant.getBackupFilename();
						final String mimeType = tenant.getBackupMimeType();
						final File temp = File.createTempFile("BasicTenantExport.", ".backup");
						try (final OutputStream out = new FileOutputStream(temp)) {
							tenant.exportTo(out);
						}
						final net.metanotion.contentstore.Entry e = media.append(filename);
						try (final InputStream in = new FileInputStream(temp)) {
							e.update(in, filename, mimeType);
						}
						temp.delete();
						messageList.add("msg_exportComplete");
					} catch (final RuntimeException | IOException ex) {
						logger.error("{}", ex);
					}
					currentJob.set(null);
				}
			});
		}
		return status();
	}

	@Override public Status resetInstance() {
		final ConcurrentBoundedBuffer<String> messageList = new ConcurrentBoundedBuffer<>(10);
		messageList.add("msg_resetStarted");
		if(currentJob.compareAndSet(null, messageList)) {
			queue.submit(new Runnable() {
				@Override public void run() {
					try {
						tenant.resetInstance();
						messageList.add("msg_resetComplete");
					} catch (final Exception ex) {
						logger.error("{}", ex);
					}
					currentJob.set(null);
				}
			});
		}
		return status();
	}

	private static String getStoreTime() {
		final DateTimeFormatter dateFormat = DateTimeFormat.forPattern("YYYY.MM.dd-HH.mm");
		return dateFormat.print(new DateTime());
	}

	@Override public void storeExternalExport(final String filename, final String mimeType, final InputStream in) {
		final Collection media = store.getCollection(backupCollection);
		final net.metanotion.contentstore.Entry e = media.append("File uploaded '" + getStoreTime() + "' - " + filename);
		e.update(in, filename, mimeType);
	}

	@Override public List<TenantExport.Entry> listExports(int offset, final int count) {
		final ArrayList<TenantExport.Entry> list = new ArrayList<>();
		final Collection media = store.getCollection(backupCollection);
		for(final Header h: media.header(count, offset)) {
			final TenantExport.Entry e = new TenantExport.Entry();
			e.description = h.Title;
			e.fileId = h.id;
			e.offset = offset;
			offset++;
			list.add(e);
		}
		return list;
	}

	@Override public void deleteExport(final long fileId) {
		final net.metanotion.contentstore.Entry e = store.getEntry(fileId);
		if(e.getCollection().oid() != store.getCollection(backupCollection).oid()) {
			throw new RuntimeException(INVALID_EXPORT_ID);
		}
		e.delete();
	}

	@Override public HttpValues getExport(final long fileId) {
		final net.metanotion.contentstore.Entry e = store.getEntry(fileId);
		if(e.getCollection().oid() != store.getCollection(backupCollection).oid()) {
			return HttpUtil.new404("Invalid Export");
		}
		return StoreUtils.get(e);
	}

	@Override public Status initiateImport(final long fileId) {
		final ConcurrentBoundedBuffer<String> messageList = new ConcurrentBoundedBuffer<>(10);
		messageList.add("msg_importStarted");
		if(currentJob.compareAndSet(null, messageList)) {
			final net.metanotion.contentstore.Entry e = store.getEntry(fileId);
			if(e.getCollection().oid() != store.getCollection(backupCollection).oid()) {
				throw new RuntimeException(INVALID_EXPORT_ID);
			}
			queue.submit(new Runnable() {
				@Override public void run() {
					try (final InputStream in = e.readFile()) {
						tenant.importFrom(in);
						messageList.add("msg_importComplete");
					} catch (final Exception ex) {
						logger.error("{}", ex);
					}
					currentJob.set(null);
				}
			});
		}
		return status();
	}
}