diff --git a/NEW-STRUCTURE.md b/NEW-STRUCTURE.md
new file mode 100644
index 0000000000000000000000000000000000000000..1ff237c8a5012564fe2251201924ea081037f432
--- /dev/null
+++ b/NEW-STRUCTURE.md
@@ -0,0 +1,32 @@
+Goals
+=====
+
+Basically, we would like to have an interface as follows:
+
+* the REST interface should live in a small single class that then instantiates
+  the workers, so we could also setup a SOAP interface if we want
+* most of the argument handling etc. should be the same for all types of
+  export, so reuse it
+* usually, request processing contains of the following steps:
+
+	1. argument parsing & (offline) validation
+	2. argument validation (online), e.g. check whether referred objects
+	   actually exist. Usually this involves fetching at least one object's
+	   metadata in order to determine its format & potentially title for
+	   the results.
+	3. Result header generation
+	4. Result body generation.
+
+    The first three steps should be as fast as possible, i.e. as few repository
+    interaction as possible, so we can reply fast. The fourth step will usually
+    be deferred to the time data is streamed (StreamingOutput's write method)
+
+* There are typical recurring tasks:
+
+	* caching of stylesheets
+	* calculating of Cache headers
+	* processing trees of documents
+	* generating filenames both for the result & for parts of it
+	* generating of TEIcorpus documents
+	* rewriting links
+	*
diff --git a/src/main/java/info/textgrid/services/aggregator/AbstractExporter.java b/src/main/java/info/textgrid/services/aggregator/AbstractExporter.java
new file mode 100644
index 0000000000000000000000000000000000000000..2763873a0ac4f62ebf0738031d65ea39bac8e53e
--- /dev/null
+++ b/src/main/java/info/textgrid/services/aggregator/AbstractExporter.java
@@ -0,0 +1,438 @@
+package info.textgrid.services.aggregator;
+
+import info.textgrid.namespaces.metadata.core._2010.MetadataContainerType;
+import info.textgrid.namespaces.metadata.core._2010.ObjectType;
+import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.AuthFault;
+import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.IoFault;
+import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.MetadataParseFault;
+import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.ObjectNotFoundFault;
+import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.ProtocolNotImplementedFault;
+import info.textgrid.services.aggregator.ITextGridRep.TGOSupplier;
+import info.textgrid.utils.export.filenames.DefaultFilenamePolicy;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.text.MessageFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.ws.rs.core.CacheControl;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Request;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.ResponseBuilder;
+import javax.ws.rs.core.StreamingOutput;
+import javax.xml.datatype.DatatypeConstants;
+import javax.xml.datatype.XMLGregorianCalendar;
+
+import net.sf.saxon.s9api.SaxonApiException;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Stopwatch;
+
+/**
+ * The abstract base class for all aggregators/exporters. This tries to capture
+ * all behavior common to the various format specific exporters, and it tries to
+ * provide sensible defaults so code complexity for the implementers is reduced.
+ *
+ * <p>These are the typical steps:</p>
+ * <ol>
+ * <li>The exporter is instantiated using one of the constructors and configured
+ * for the specific request.</li>
+ * <li>The response (except for the content part) is built using
+ * {@link #createResponse()}.
+ * <p>
+ * This step should include the required steps to decide on the response type
+ * (success/error …), but it should return as fast as possible since the
+ * client/user will not get any feedback before.
+ * </p>
+ * </li>
+ * <li>The actual content should be generated and returned by the
+ * {@link #write(java.io.OutputStream)} method, which is called on-demand when
+ * the client tries to read the body part of the response.</li>
+ * </ol>
+ *
+ * Implementors will typically call at least the following configuration methods:
+ * <ul>
+ * <li>{@link #setMediaType(String)} to set the response's content type
+ * <li>{@link #setFileExtension(String)} to set the response's suggested filename extension
+ * </ul>
+ * Implementors will typically implement / extend the following methods:
+ * <ul>
+ * <li>implement {@link #write(java.io.OutputStream)} to generate the contents
+ * <li>extend {@link #createResponse()} to setup the response any further
+ * </ul>
+ *
+ *
+ * @author vitt
+ */
+public abstract class AbstractExporter implements StreamingOutput {
+
+	private static final Logger logger = Logger
+			.getLogger(AbstractExporter.class.getCanonicalName());
+	private Optional<String> sid = Optional.absent();
+
+	protected Optional<String> getSid() {
+		return sid;
+	}
+
+	public SourceType getSourceType() {
+		return sourceType;
+	}
+
+	public URI[] getRootURIs() {
+		return rootURIs;
+	}
+
+	// detected and extracted
+	public enum SourceType {
+		UNKNOWN, XML, AGGREGATION, BASKET
+	}
+
+	protected enum Disposition {
+		INLINE, ATTACH;
+
+		@Override
+		public String toString() {
+			return super.toString().toLowerCase(Locale.ENGLISH);
+		}
+	}
+
+	protected SourceType sourceType = SourceType.UNKNOWN;
+	protected final ITextGridRep repository;
+	protected final Request request;
+	protected final Stopwatch stopwatch;
+	private TGOSupplier<InputStream> content;
+	protected URI[] rootURIs;
+	private ObjectType[] rootObjects;
+	private Date lastModified;
+	private Boolean notModified = null;
+	private ResponseBuilder responseBuilder;
+	private String title;
+	private String fileExtension;
+	private String mediaType = MediaType.APPLICATION_OCTET_STREAM;
+
+	public String getTitle() {
+		return title;
+	}
+
+	public void setTitle(final String title) {
+		this.title = title;
+	}
+
+	public String getFileExtension() {
+		return fileExtension;
+	}
+
+	public void setFileExtension(final String fileExtension) {
+		this.fileExtension = fileExtension;
+	}
+
+	public Disposition getDisposition() {
+		return disposition;
+	}
+
+	public void setDisposition(final Disposition disposition) {
+		this.disposition = disposition;
+	}
+
+	private Disposition disposition = Disposition.ATTACH;
+
+	public AbstractExporter(final ITextGridRep repository,
+			final Request request, final String uriList) {
+		Preconditions.checkArgument(repository != null, "non-null repository argument required");
+
+		stopwatch = new Stopwatch();
+		stopwatch.start();
+		this.repository = repository;
+		this.request = request;
+		this.rootURIs = extractURIs(uriList);
+		if (rootURIs.length > 1)
+			this.sourceType = SourceType.BASKET;
+	}
+
+	private static URI[] extractURIs(final String uriList) {
+		final String[] uriStrings = uriList.split(",");
+		Preconditions.checkArgument(uriStrings.length > 0, "No URI found in %s", uriList);
+		final URI[] uris = new URI[uriStrings.length];
+		for (int i = 0; i < uriStrings.length; i++) {
+			uris[i] = URI.create(uriStrings[i]);
+		}
+		return uris;
+	}
+
+	/**
+	 * Returns an array with the metadata records of all root objects.
+	 */
+	protected ObjectType[] getRootObjects() throws MetadataParseFault,
+			ObjectNotFoundFault, IoFault, AuthFault {
+		if (rootObjects == null) {
+			if (sourceType == SourceType.BASKET) {
+				final ObjectType[] objects = new ObjectType[rootURIs.length];
+				for (int i = 0; i < rootURIs.length; i++) {
+					final MetadataContainerType container = repository.getCRUDService().readMetadata(sid.orNull(), null, rootURIs[i].toString());
+					objects[i] = container.getObject();
+				}
+				rootObjects = objects;
+				logger.log(Level.INFO, MessageFormat.format(
+						"Collected root objects for basket {0} after {1}",
+						rootURIs, stopwatch.toString()));
+			} else
+				rootObjects = new ObjectType[] { getContentSimple()
+						.getMetadata() };
+		}
+		return rootObjects;
+	}
+
+	/**
+	 * Returns a single, possibly virtual object representing the exporter's
+	 * root input. {@link AbstractExporter}'s implementation delegates to
+	 * {@link #getContentBasket()} or {@link #getContentSimple()}, depending on
+	 * the request type.
+	 *
+	 * @throws IllegalStateException
+	 *             if not supported.
+	 */
+	protected TGOSupplier<InputStream> getContent() {
+		if (sourceType == SourceType.BASKET) {
+			return getContentBasket();
+		} else
+			return getContentSimple();
+	}
+
+	/**
+	 * Constructs a single virtual object representing the root objects. Clients
+	 * who need this functionality must override.
+	 *
+	 * @throws IllegalStateException
+	 *             if not supported.
+	 */
+	protected TGOSupplier<InputStream> getContentBasket() {
+		throw new IllegalStateException(
+				"This exporter does not support retrieving the basket content as single object.");
+	}
+
+	/**
+	 * Returns a {@link TGOSupplier} with content and metadata in case only one
+	 * object has been requested.
+	 *
+	 * This method may cause a TG-crud READ request that is left open after the
+	 * metadata part until content data is requested from the supplier's input
+	 * stream.
+	 *
+	 * @throws IllegalStateException
+	 *             when more than one root object is requested
+	 */
+	protected TGOSupplier<InputStream> getContentSimple() {
+		Preconditions
+				.checkState(
+						rootURIs.length == 1,
+						"This method is not available for basket requests (requested %s)",
+						(Object[]) rootURIs);
+		if (content == null) {
+			content = repository.read(rootURIs[0], sid.orNull());
+			logger.info(MessageFormat.format(
+					"Fetched source for {0} up to metadata after {1}",
+					rootURIs[0], stopwatch));
+		}
+		return content;
+	}
+
+	/**
+	 * Sets the session Id
+	 */
+	public AbstractExporter sid(final String sid) {
+		if (sid == null || sid.isEmpty()) {
+			this.sid = Optional.absent();
+		} else {
+			this.sid = Optional.of(sid);
+		}
+		return this;
+	}
+
+	/**
+	 * Returns a last modified date suitable for use in HTTP requests.
+	 *
+	 * Implementers may override or extend.
+	 */
+	protected Date getLastModified() throws MetadataParseFault,
+			ObjectNotFoundFault, IoFault, AuthFault {
+		if (lastModified == null) {
+			XMLGregorianCalendar youngest = null;
+			for (final ObjectType object : getRootObjects()) {
+				final XMLGregorianCalendar current = object.getGeneric().getGenerated().getLastModified();
+				if (youngest == null || current.compare(youngest) == DatatypeConstants.GREATER)
+					youngest = current;
+			}
+			youngest.setMillisecond(0);
+			this.lastModified = youngest.toGregorianCalendar().getTime();
+		}
+		return lastModified;
+	}
+
+	/**
+	 * Checks whether we can stop exporting and return 304 Not Modified instead.
+	 *
+	 * The {@link AbstractExporter} implementation simply checks the results of
+	 * {@link #getLastModified()}.
+	 *
+	 * @return a configured {@link ResponseBuilder} when the content is not
+	 *         modified, <code>null</code> when the request should be fulfilled
+	 *         normally.
+	 */
+	protected final ResponseBuilder evaluatePreconditions()
+			throws MetadataParseFault, ObjectNotFoundFault, IoFault, AuthFault {
+
+		final ResponseBuilder builder = request == null ? null
+				: doEvaluatePreconditions();
+		if (builder == null) {
+			this.responseBuilder = Response.ok();
+			this.notModified = false;
+		} else {
+			this.responseBuilder = builder;
+			this.notModified = true;
+		}
+		return builder;
+	}
+
+	/**
+	 * The {@link AbstractExporter} implementation simply checks the results of
+	 * {@link #getLastModified()}.
+	 */
+	protected ResponseBuilder doEvaluatePreconditions()
+			throws MetadataParseFault, ObjectNotFoundFault, IoFault, AuthFault {
+		return request.evaluatePreconditions(getLastModified());
+	}
+
+	/**
+	 * Returns <code>true</code> when we know from the request that we can skip
+	 * generating a real response and can just return
+	 * <code>304 Not Modified</code> instead, allowing the client to serve the
+	 * request from its local cache.
+	 *
+	 * <p>
+	 * This runs {@link #evaluatePreconditions()}, if necessary. Clients who
+	 * wish to influence this behaviour should override
+	 * {@link #doEvaluatePreconditions()}.
+	 * </p>
+	 *
+	 * @see #evaluatePreconditions()
+	 * @see {@link #doEvaluatePreconditions()}
+	 */
+	protected final boolean isNotModified() throws MetadataParseFault,
+			ObjectNotFoundFault, IoFault, AuthFault {
+		if (notModified == null)
+			evaluatePreconditions();
+		return notModified;
+	}
+
+	/**
+	 * Returns the current state of the exporter's internal
+	 * {@link ResponseBuilder}. If none exists yet,
+	 * {@link #evaluatePreconditions()} is called and a minimal response builder
+	 * is constructed.
+	 */
+	protected ResponseBuilder getResponseBuilder() throws MetadataParseFault,
+			ObjectNotFoundFault, IoFault, AuthFault {
+		if (responseBuilder == null)
+			evaluatePreconditions();
+		return responseBuilder;
+	}
+
+	/**
+	 * Configures caching parameters into the current response builder.
+	 *
+	 * @see #getResponseBuilder()
+	 * @see #createResponse()
+	 */
+	protected void configureCache() throws MetadataParseFault,
+			ObjectNotFoundFault, IoFault, AuthFault {
+		ResponseBuilder builder = getResponseBuilder();
+				builder.lastModified(getLastModified());
+				final CacheControl cacheControl = new CacheControl();
+				cacheControl.setPrivate(sid.isPresent());
+				cacheControl.setMaxAge(86400);	// one day
+				builder.cacheControl(cacheControl);
+	}
+
+	/**
+	 * Configures the content-disposition header, which contains the
+	 * content-disposition and a suggested filename.
+	 *
+	 * @see #setDisposition(Disposition)
+	 * @see #setTitle(String)
+	 * @see #setFileExtension(String)
+	 */
+	protected void configureFilename() throws MetadataParseFault,
+			ObjectNotFoundFault, IoFault, AuthFault {
+
+		getResponseBuilder().header(
+				"Content-Disposition",
+				getDisposition()
+						+ ";filename=\""
+						+ calculateFilename() + "\"");
+	}
+	
+	
+	private String calculateFilename() throws MetadataParseFault, ObjectNotFoundFault, IoFault, AuthFault {
+		final String title = calculateTitle();
+		final StringBuilder result = new StringBuilder(title.length() + 30);
+		result.append(DefaultFilenamePolicy.INSTANCE.translate(title));
+		if (getRootObjects().length == 1)
+			result.append('.').append(URI.create(rootObjects[0].getGeneric().getGenerated().getTextgridUri().getValue()).getSchemeSpecificPart());
+		if (fileExtension != null && !fileExtension.isEmpty())
+			result.append('.').append(fileExtension);
+		return result.toString();
+	}
+
+	private String calculateTitle() throws MetadataParseFault, ObjectNotFoundFault, IoFault, AuthFault {
+		ObjectType[] rootObjects = getRootObjects();
+		if (title != null && !title.isEmpty())
+			return title;
+		else if (rootObjects.length == 0)
+			return "(nothing)";
+		else if (rootObjects.length == 1)
+			return rootObjects[0].getGeneric().getProvided().getTitle().get(0);
+		else
+			return rootObjects[0].getGeneric().getProvided().getTitle().get(0) + " etc";
+	}
+
+	/**
+	 * Creates a response builder that is fully configured and ready to build a
+	 * response and return it.
+	 */
+	public ResponseBuilder createResponse() throws MetadataParseFault,
+			ObjectNotFoundFault, IoFault, AuthFault,
+			ProtocolNotImplementedFault, IOException, SaxonApiException {
+		if (isNotModified()) {
+			return getResponseBuilder();
+		} else {
+			configureFilename();
+			configureCache();
+			getResponseBuilder().type(getMediaType()).entity(this);
+			return getResponseBuilder();
+		}
+	}
+
+	public String getMediaType() {
+		return mediaType;
+	}
+
+	public void setMediaType(final String mediaType) {
+		this.mediaType = mediaType;
+	}
+
+	@Override
+	protected void finalize() throws Throwable {
+		if (stopwatch.isRunning())
+			stopwatch.stop();
+		super.finalize();
+	}
+
+
+
+}
\ No newline at end of file
diff --git a/src/main/java/info/textgrid/services/aggregator/ArgUtils.java b/src/main/java/info/textgrid/services/aggregator/ArgUtils.java
deleted file mode 100644
index 30cc75c7e440f1d20c9a64a624b1c59721741b97..0000000000000000000000000000000000000000
--- a/src/main/java/info/textgrid/services/aggregator/ArgUtils.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package info.textgrid.services.aggregator;
-
-import info.textgrid.namespaces.metadata.core._2010.MetadataContainerType;
-import info.textgrid.namespaces.metadata.core._2010.ObjectType;
-import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.AuthFault;
-import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.IoFault;
-import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.MetadataParseFault;
-import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.ObjectNotFoundFault;
-import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.TGCrudService;
-import info.textgrid.utils.export.filenames.DefaultFilenamePolicy;
-
-import java.net.URI;
-
-public class ArgUtils {
-
-	public static ObjectType[] extractRootObjects(final String uriList, final String sid,
-			final TGCrudService crud) throws MetadataParseFault,
-			ObjectNotFoundFault, IoFault, AuthFault {
-		final String[] uriStrings = uriList.split(",");
-		if (uriStrings.length == 0)
-			throw new IllegalArgumentException("Specify at least one URI to zip");
-		final URI[] uris = new URI[uriStrings.length];
-		final ObjectType[] objects = new ObjectType[uriStrings.length];
-		for (int i = 0; i < uriStrings.length; i++) {
-			uris[i] = URI.create(uriStrings[i]);
-			final MetadataContainerType container = crud.readMetadata(sid, null, uris[i].toString());
-			objects[i] = container.getObject();
-		}
-		return objects;
-	}
-
-	public static String createTitle(final ObjectType[] rootObjects, final String title) {
-		if (title != null && !title.isEmpty())
-			return title;
-		else if (rootObjects.length == 0)
-			return "(nothing)";
-		else if (rootObjects.length == 1)
-			return rootObjects[0].getGeneric().getProvided().getTitle().get(0);
-		else
-			return rootObjects[0].getGeneric().getProvided().getTitle().get(0) + " etc";
-	}
-
-	public static String createFilename(final ObjectType[] rootObjects, final String suggestedTitle, final String extension) {
-		final String title = createTitle(rootObjects, suggestedTitle);
-		final StringBuilder result = new StringBuilder(title.length() + 30);
-		result.append(DefaultFilenamePolicy.INSTANCE.translate(title));
-		if (rootObjects.length == 1)
-			result.append('.').append(URI.create(rootObjects[0].getGeneric().getGenerated().getTextgridUri().getValue()).getSchemeSpecificPart());
-		if (extension != null && !extension.isEmpty())
-			result.append('.').append(extension);
-		return result.toString();
-	}
-
-}
diff --git a/src/main/java/info/textgrid/services/aggregator/GenericExceptionMapper.java b/src/main/java/info/textgrid/services/aggregator/GenericExceptionMapper.java
index cc98793a931b31e9f0de1e4cb39c72d40c9a215a..27029a65c850d3876e1f54fcae858f691c18ecbb 100644
--- a/src/main/java/info/textgrid/services/aggregator/GenericExceptionMapper.java
+++ b/src/main/java/info/textgrid/services/aggregator/GenericExceptionMapper.java
@@ -91,7 +91,7 @@ public Response toResponse(final Exception exception) {
 				.getSimpleName(), exception.getMessage(), status
 						.getStatusCode(), status.toString()), exception);
 
-		final Response response = toResponse(status, message, Throwables.getStackTraceAsString(exception));
+		final Response response = toResponse(status, message, Throwables.getStackTraceAsString(exception)).build();
 		return response;
 	}
 
@@ -102,12 +102,12 @@ public Response toResponse(final Exception exception) {
 	 * @param detail a detail message, e.g., a stack trace. Will be HTML-escaped and displayed in a preformatted way below the status msg.
 	 * @return the generated response.
 	 */
-	public static Response toResponse(final Status status,
+	public static ResponseBuilder toResponse(final Status status,
 			final String message, final String detail) {
 		final ResponseBuilder builder = Response.status(status);
 		builder.type(MediaType.APPLICATION_XHTML_XML_TYPE);
 		builder.entity(prepareXHTMLMessage(status, message, detail));
-		return builder.build();
+		return builder;
 	}
 
 	public static String prepareXHTMLMessage(final Status status,
diff --git a/src/main/java/info/textgrid/services/aggregator/REST.java b/src/main/java/info/textgrid/services/aggregator/REST.java
new file mode 100644
index 0000000000000000000000000000000000000000..a58308258aea9c359c8bc5a41498c0a66cd2eef7
--- /dev/null
+++ b/src/main/java/info/textgrid/services/aggregator/REST.java
@@ -0,0 +1,174 @@
+package info.textgrid.services.aggregator;
+
+import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.AuthFault;
+import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.IoFault;
+import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.MetadataParseFault;
+import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.ObjectNotFoundFault;
+import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.ProtocolNotImplementedFault;
+import info.textgrid.services.aggregator.epub.EPUBSerializer;
+import info.textgrid.services.aggregator.html.HTMLWriter;
+import info.textgrid.services.aggregator.pdf.PDF;
+import info.textgrid.services.aggregator.teicorpus.TEICorpusExporter;
+import info.textgrid.services.aggregator.util.StylesheetManager;
+import info.textgrid.services.aggregator.zip.ZipResult;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.concurrent.ExecutionException;
+
+import javax.servlet.ServletContext;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Request;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.StreamingOutput;
+
+import net.sf.saxon.s9api.SaxonApiException;
+
+import org.apache.cxf.jaxrs.model.wadl.Description;
+import org.apache.cxf.jaxrs.model.wadl.Descriptions;
+import org.apache.cxf.jaxrs.model.wadl.DocTarget;
+
+import com.google.common.base.Optional;
+
+public class REST {
+	
+	private final ITextGridRep repository;
+	
+	
+	private StylesheetManager stylesheetManager;
+	private StylesheetManager getStylesheetManager() {
+		if (stylesheetManager == null)
+			stylesheetManager = new StylesheetManager(servlet, repository);
+		return stylesheetManager;
+	}
+
+	@Context
+	private ServletContext servlet;
+
+
+	private final String version;
+
+	public REST(final ITextGridRep repository, final String version) {
+		this.repository = repository;
+		this.version = version;
+	}
+	
+	@GET
+	@Path("/teicorpus/{uris}")
+	@Produces("application/tei+xml")
+	@Descriptions({
+			@Description(target=DocTarget.METHOD, value="Creates a TEI corpus of all the TEI documents (recursively) aggregated by the given aggregation"),
+			@Description(target=DocTarget.RETURN, value="TEI corpus document")
+	})
+	public Response getCorpus(@Description("TextGrid URIs of the root objects, separated by commas") @PathParam("uris") final String uriList,
+			@Description("Whether to generate a Content-Disposition: attachment header") @QueryParam("attach") @DefaultValue("true") final boolean attach,
+			@Description("If true, no intermediate TEI corpus documents will be generated for intermediate aggregations, hierarchical structure will be lost") @QueryParam("flat") @DefaultValue("false") final boolean flat,
+			@Description("Title for the container if multiple root objects are given") @QueryParam("title") final String titleArgument, 
+			@Description("Session id for accessing restricted resources") @QueryParam("sid") final String sid,
+			@Context Request request)
+					throws URISyntaxException, ObjectNotFoundFault, MetadataParseFault, IoFault, AuthFault, ProtocolNotImplementedFault, IOException, SaxonApiException {
+		
+		TEICorpusExporter exporter = new TEICorpusExporter(repository, request, uriList);
+		exporter.setFlat(flat);
+		exporter.setTitle(titleArgument);
+		
+		return exporter.createResponse().build();
+	}
+	
+	
+	@GET
+	@Path(value = "/epub/{object}")
+	@Produces(value = "application/epub+zip")
+	@Description("Converts the given TEI object or the aggregation of TEI objects to an E-Book in EPUB format")
+	public Response getEPUB(
+			@Description("The TextGrid URI(s) of the object(s) to convert, separated by commas. Should be either TEI objects or aggregations of TEI (and maybe other) objects")
+			@PathParam("object") final String uriList,
+			@Description("URL of an alternative stylesheet to use. Must be compatible.") @QueryParam("stylesheet") final URI xsluri,
+			@Description("Title if multiple root objects given") @QueryParam("title") final String titleParam,
+			@Description("Session ID for accessing protected objects") @QueryParam("sid") final String sid,
+			@Context final Request request)
+					throws ObjectNotFoundFault, MetadataParseFault, IoFault, AuthFault,
+					ProtocolNotImplementedFault, IOException, SaxonApiException {
+
+		final EPUBSerializer serializer = new EPUBSerializer(repository, getStylesheetManager(), uriList, Optional.fromNullable(sid), request);
+		serializer.setStylesheet(xsluri);
+		serializer.setTitle(titleParam);
+
+		return serializer.createResponse().build();
+	}
+
+	@GET
+	@Path(value = "/html/{object}")
+	@Produces(value = "text/html")
+	public Response getHTML(
+			@Description("The TextGrid URIs of the TEI document(s) or aggregation(s) to transform, separated by commas") @PathParam("object") final String uriList,
+			@Description("If given, an alternative XSLT stylesheet to use") @QueryParam("stylesheet") final URI xsluri,
+			@Description("If true, check for an <?xsl-stylesheet?> processing instruction in the document to render") @QueryParam("pi") final boolean pi,
+			@Description("If true and a stylesheet has been given, force reloading the stylesheet, do not cache") @QueryParam("refreshStylesheet") final boolean refreshStylesheet,
+			@Description("Session ID to access protected resources") @QueryParam("sid") final String sid,
+			@Description("If true, pass the information the stylesheet that its result will be embedded into some website") @QueryParam("embedded") final boolean embedded,
+			@Description("URL of the CSS that should be referenced in the HTML that is created") @QueryParam("css") final URI css,
+			@Description("The requested content type. E.g., text/html or text/xml") @QueryParam("mediatype") final String mediaType,
+			@Context final Request request) throws ObjectNotFoundFault,
+			MetadataParseFault, IoFault, AuthFault,
+			ProtocolNotImplementedFault, WebApplicationException, IOException,
+			SaxonApiException, ExecutionException {
+
+
+		final HTMLWriter writer = new HTMLWriter(repository, getStylesheetManager(), uriList, xsluri,
+				refreshStylesheet, pi, embedded, css, sid, mediaType, request);
+		return writer.createResponse().build();
+	}
+	
+	@GET
+	@Path(value = "/zip/{objects}")
+	@Produces("application/zip")
+	public Response getZIP(
+			@Description("The TextGridURIs of the TEI documents or aggregations to zip, separated by commas (,)")
+			@PathParam("objects") final String uriList,
+			@Description("Session ID to access protected resources")
+			@QueryParam("sid") final String sid,
+			@Description("(optional) title for the exported data, currently only used for generating the filename. If none is given, the first title of the first object will be used.")
+			@QueryParam("title") String title,
+			@Context final Request request) throws MetadataParseFault, ObjectNotFoundFault, IoFault, AuthFault, ProtocolNotImplementedFault, IOException, SaxonApiException {
+		
+		ZipResult zipResult = new ZipResult(repository, request, uriList);
+		if (title != null)
+			zipResult.setTitle(title);
+		if (sid != null)
+			zipResult.sid(sid);
+		return zipResult.createResponse().build();
+	}
+	
+	@GET
+	@Path(value = "/pdf/{object}")
+	@Produces("application/pdf")
+	public Response getPDF(
+			@PathParam("object") final URI uri,
+			@QueryParam("sid") final String sid,
+			@Context final Request request
+			) throws MetadataParseFault, ObjectNotFoundFault, IoFault, AuthFault, ProtocolNotImplementedFault, IOException, SaxonApiException {
+		final PDF pdf = new PDF(repository, getStylesheetManager(), request, uri);
+		pdf.sid(sid);
+		return pdf.createResponse().build(); 
+		
+	}
+	
+	
+	@GET
+	@Path(value = "/version")
+	@Produces("text/html")
+	public StreamingOutput getVersion() {
+		Version version = new Version(repository, getStylesheetManager(), this.version);
+		return version.get();
+	}
+
+}
diff --git a/src/main/java/info/textgrid/services/aggregator/RESTUtils.java b/src/main/java/info/textgrid/services/aggregator/RESTUtils.java
deleted file mode 100644
index f466c1945d2a7d452923ce393e37747e7d6dc8ea..0000000000000000000000000000000000000000
--- a/src/main/java/info/textgrid/services/aggregator/RESTUtils.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package info.textgrid.services.aggregator;
-
-import info.textgrid.namespaces.metadata.core._2010.ObjectType;
-
-import java.util.Date;
-
-import javax.ws.rs.core.CacheControl;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.Response.ResponseBuilder;
-import javax.ws.rs.core.UriBuilder;
-import javax.xml.datatype.DatatypeConstants;
-import javax.xml.datatype.XMLGregorianCalendar;
-
-public class RESTUtils {
-
-	public static ResponseBuilder addAttachmentFilename(final ResponseBuilder builder, final String fileName) {
-		final String asciiFileName = fileName.replaceAll("[^A-Za-z0-9.-]", "_");
-		final String extendedNameSpec = "UTF-8''" + UriBuilder.fromPath(fileName).build().toASCIIString();
-		
-		if (!asciiFileName.equals(fileName))
-			builder.header("Content-Disposition", "attachment;filename*=\"" + extendedNameSpec + "\";" +
-				/*		builder.header("Content-Disposition", */ "filename=\"" + asciiFileName + "\"" );
-		else
-			builder.header("Content-Disposition", "attachment;filename=\"" + asciiFileName + "\"" );
-		builder.header("X-Attachment-Orig-Filename", fileName);
-		return builder;
-	}
-
-	public static ResponseBuilder attachmentResponse(final String fileName) {
-		return addAttachmentFilename(Response.ok(), fileName);
-	}
-
-	/**
-	 * Creates a last-modified date suitable for use in HTTP headers. I.e. it extrats
-	 * the lastModified header fields from the given objects, finds the youngest one
-	 * and strips the millisecond part.
-	 * 
-	 * @param objects The objects to consider in the calculation
-	 * @return a date/time stamp, with the milliseconds stripped
-	 */
-	public static Date createLastModified(final ObjectType... objects) {
-		XMLGregorianCalendar youngest = null;
-		for (final ObjectType object : objects) {
-			final XMLGregorianCalendar current = object.getGeneric().getGenerated().getLastModified();
-			if (youngest == null || current.compare(youngest) == DatatypeConstants.GREATER)
-				youngest = current;
-		}
-		youngest.setMillisecond(0);
-		return youngest.toGregorianCalendar().getTime();
-	}
-	
-	public static ResponseBuilder configureCache(final ResponseBuilder builder, Date lastModified, boolean isPrivate) {
-        builder.lastModified(lastModified);
-		final CacheControl cacheControl = new CacheControl();
-		cacheControl.setPrivate(isPrivate);
-		cacheControl.setMaxAge(86400);	// one day
-		builder.cacheControl(cacheControl);
-		return builder;
-	}
-
-}
diff --git a/src/main/java/info/textgrid/services/aggregator/Version.java b/src/main/java/info/textgrid/services/aggregator/Version.java
index 134cc961e5effe32c88278f414dc430268d478e5..3b3fef02d15c867716ffda20f138ae35e71b7ad7 100644
--- a/src/main/java/info/textgrid/services/aggregator/Version.java
+++ b/src/main/java/info/textgrid/services/aggregator/Version.java
@@ -1,9 +1,14 @@
 package info.textgrid.services.aggregator;
 
+import info.textgrid.services.aggregator.util.StylesheetManager;
+
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.io.PrintStream;
+import java.net.URI;
+import java.net.URL;
 import java.util.Properties;
 
 import javax.ws.rs.GET;
@@ -13,6 +18,7 @@
 import javax.ws.rs.core.StreamingOutput;
 
 import com.google.common.base.Joiner;
+import com.google.common.io.CharStreams;
 
 @Path("/version")
 public class Version {
@@ -20,10 +26,12 @@ public class Version {
 	private final ITextGridRep repository;
 	private final String version;
 	private Properties gitInfo;
+	private final StylesheetManager stylesheetManager;
 
-	public Version(final ITextGridRep repository, final String version) {
+	public Version(final ITextGridRep repository, final StylesheetManager stylesheetManager, final String version) {
 		this.repository = repository;
 		this.version = version;
+		this.stylesheetManager = stylesheetManager;
 	}
 
 	private Properties loadGitInfo() {
@@ -63,7 +71,7 @@ public void write(final OutputStream outputStream)
 						info.getProperty("git.branch", "?"),
 						info.getProperty("git.commit.time", "?"),
 						info.getProperty("git.build.time", "?"));
-
+				
 				out.printf(
 						"<p>TextGridRep Configuration Endpoint: <a href='%1$s'>%1$s</a></p>\n\n",
 						repository.getCONF_ENDPOINT());
@@ -74,8 +82,19 @@ public void write(final OutputStream outputStream)
 				out.println("</td></tr></table>");
 				out.printf("<p>Using TG-crud version: <strong>%s</strong>\n",
 						repository.getCRUDService().getVersion());
-
-				out.println("</body></html>");
+				
+				URL xslVersionURL = stylesheetManager.resolveInternalPath("/WEB-INF/xml/tei/stylesheet/VERSION");
+				InputStream xslVersionStream = xslVersionURL.openStream();
+				String xslVersion = CharStreams.toString(new InputStreamReader(xslVersionStream));  
+				out.printf("<p>Using TEI stylesheets version: <strong>%s</strong>\n", xslVersion);
+				
+				out.printf("<h2>StylesheetManager statistics</h2>\n");
+				out.printf("<p>%s</p>\n", stylesheetManager.stats().toString());
+				out.printf("<p>The following stylesheets are currently cached: </p><ol>\n");
+				for (URI uri : stylesheetManager.knownStylesheets())
+					out.printf("<li>%s</li>", uri);
+				
+				out.println("</ol></body></html>");
 				out.close();
 				outputStream.close();
 			}
diff --git a/src/main/java/info/textgrid/services/aggregator/epub/EPUB.java b/src/main/java/info/textgrid/services/aggregator/epub/EPUB.java
deleted file mode 100644
index 351b75d3174d86621078f47a63e706fb34a36cf0..0000000000000000000000000000000000000000
--- a/src/main/java/info/textgrid/services/aggregator/epub/EPUB.java
+++ /dev/null
@@ -1,183 +0,0 @@
-package info.textgrid.services.aggregator.epub;
-
-import info.textgrid.namespaces.metadata.core._2010.ObjectType;
-import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.AuthFault;
-import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.IoFault;
-import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.MetadataParseFault;
-import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.ObjectNotFoundFault;
-import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.ProtocolNotImplementedFault;
-import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.TGCrudService;
-import info.textgrid.services.aggregator.ArgUtils;
-import info.textgrid.services.aggregator.ITextGridRep;
-import info.textgrid.services.aggregator.RESTUtils;
-import info.textgrid.services.aggregator.TextGridRepProvider;
-import info.textgrid.services.aggregator.teicorpus.TEICorpusSerializer;
-import info.textgrid.utils.export.filenames.DefaultFilenamePolicy;
-
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.MalformedURLException;
-import java.net.URI;
-import java.net.URL;
-import java.util.Date;
-import java.util.logging.Logger;
-
-import javax.servlet.ServletContext;
-import javax.ws.rs.GET;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.WebApplicationException;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.Request;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.Response.ResponseBuilder;
-import javax.xml.transform.stream.StreamSource;
-
-import net.sf.saxon.s9api.Processor;
-import net.sf.saxon.s9api.SaxonApiException;
-import net.sf.saxon.s9api.XsltCompiler;
-import net.sf.saxon.s9api.XsltExecutable;
-import net.sf.saxon.s9api.XsltTransformer;
-
-import org.apache.cxf.jaxrs.model.wadl.Description;
-
-import com.google.common.io.Files;
-import com.google.common.io.InputSupplier;
-
-@Path("/epub")
-@Description("Converts the given TEI object or the aggregation of TEI objects to an E-Book in EPUB format")
-public class EPUB {
-
-	private static final String EPUB_XSL = "/WEB-INF/stylesheets/epub.xsl";
-
-	private ITextGridRep repository = TextGridRepProvider.getInstance();
-
-	final Logger logger = Logger.getLogger("info.textgrid.services.aggregator.EPUB");
-
-	private XsltExecutable teiToEpub;
-
-	private final Processor xsltProcessor;
-
-	@Context
-	private ServletContext servlet;
-
-	private XsltExecutable getTeiToEpub() {
-		if (teiToEpub == null) {
-			final XsltCompiler xsltCompiler = xsltProcessor.newXsltCompiler();
-			try {
-				URL stylesheet;
-				if (servlet == null) {
-					logger.info("No servlet context, trying fallback property");
-					final String dir = System.getProperty("webapp.directory");
-					if (dir == null)
-						throw new IllegalStateException(
-								"Could not find stylesheet: Neither ServletContext nor fallback property webapp.directory have been defined.");
-					stylesheet = new URL("file://" + dir + EPUB_XSL);
-				} else {
-					stylesheet = servlet.getResource(EPUB_XSL);
-				}
-				logger.info("Stylesheet: " + stylesheet.toExternalForm());
-				teiToEpub = xsltCompiler.compile(new StreamSource(stylesheet
-						.toString()));
-			} catch (final MalformedURLException e) {
-				throw new IllegalStateException(e);
-			} catch (final SaxonApiException e) {
-				throw new IllegalStateException(e);
-			}
-		}
-		return teiToEpub;
-	}
-
-	public EPUB(final ITextGridRep repository) {
-		this.repository = repository;
-		xsltProcessor = new Processor(false);
-	}
-
-	@GET
-	@Path(value = "/{object}")
-	@Produces(value = "application/epub+zip")
-	public Response get(
-			@Description("The TextGrid URI(s) of the object(s) to convert, separated by commas. Should be either TEI objects or aggregations of TEI (and maybe other) objects")
-			@PathParam("object") final String uriList,
-			@Description("URL of an alternative stylesheet to use. Must be compatible.") @QueryParam("stylesheet") final URI xsluri,
-			@Description("Title if multiple root objects given") @QueryParam("title") final String titleParam,
-			@Description("Session ID for accessing protected objects") @QueryParam("sid") final String sid,
-			@Context Request request)
-					throws ObjectNotFoundFault, MetadataParseFault, IoFault, AuthFault,
-					ProtocolNotImplementedFault {
-		logger.fine("EPUB called for root objects: " + uriList);
-		final TGCrudService crud = repository.getCRUDService();
-		try {
-			ObjectType[] rootObjects = ArgUtils.extractRootObjects(uriList, sid, crud);
-			
-			// Return fast if client already has our file
-			Date lastModified = RESTUtils.createLastModified(rootObjects);
-			ResponseBuilder builder = request.evaluatePreconditions(lastModified);
-			if (builder != null) {
-				logger.fine("Skipping EPUB generation for "+ uriList+ " due to request preconditions");
-				return builder.build();
-			}
-
-			final String title = ArgUtils.createTitle(rootObjects, titleParam);
-			final boolean multiple = (rootObjects.length > 1 || rootObjects.length == 1 && rootObjects[0].getGeneric().getProvided().getFormat().contains("aggregation"));
-
-			final File workingDir = Files.createTempDir();
-			logger.fine("Using " + workingDir + " to build the E-Book");
-
-			final File corpus = new File(workingDir, "corpus.xml");
-			if (multiple) {
-				// First, use the aggregator to create a TEI corpus file to build on
-				final TEICorpusSerializer corpusSerializer = new TEICorpusSerializer(rootObjects, true, sid);
-				corpusSerializer.setTitle(title);
-				final FileOutputStream corpusOutput = new FileOutputStream(corpus);
-				corpusSerializer.write(corpusOutput);
-				corpusOutput.close();
-			} else {
-				final InputStream tei = repository.getContent(URI.create(rootObjects[0].getGeneric().getGenerated().getTextgridUri().getValue()), sid);
-				Files.copy(new InputSupplier<InputStream>() {
-
-					@Override
-					public InputStream getInput() throws IOException {
-						return tei;
-					}
-				}, corpus);
-			}
-
-			// Now, run the EPUB stylesheet
-			// TODO cache the saxon stuff
-			final XsltTransformer transformer;
-			if (xsluri == null) {
-				transformer = getTeiToEpub().load();
-			} else {
-				logger.info("Loading stylesheet from " + xsluri);
-				final StreamSource xsltSource = new StreamSource(xsluri.toURL().openStream());
-				final XsltExecutable xsltExecutable = xsltProcessor.newXsltCompiler().compile(xsltSource);
-				transformer = xsltExecutable.load();
-			}
-			transformer.setDestination(xsltProcessor.newSerializer(new File(
-					workingDir, "output.xml")));
-			transformer.setSource(new StreamSource(corpus));
-			transformer.transform();
-
-			ResponseBuilder responseBuilder = RESTUtils
-					.attachmentResponse(
-							DefaultFilenamePolicy.INSTANCE.translate(title)
-							+ ".epub").type("application/epub+zip");
-			RESTUtils.configureCache(responseBuilder, lastModified, sid == null || sid.isEmpty());
-			return responseBuilder.entity(new EPUBSerializer(workingDir, repository, corpus,
-							sid)).build();
-		} catch (final FileNotFoundException e) {
-			throw new WebApplicationException(e);
-		} catch (final IOException e) {
-			throw new WebApplicationException(e);
-		} catch (final SaxonApiException e) {
-			throw new WebApplicationException(e);
-		}
-	}
-
-}
diff --git a/src/main/java/info/textgrid/services/aggregator/epub/EPUBSerializer.java b/src/main/java/info/textgrid/services/aggregator/epub/EPUBSerializer.java
index d1406b25ac8defde80a65bb6cda7a06db0f9df4e..2bbda1b785dd7172e5725cf91c3f228e91793a05 100644
--- a/src/main/java/info/textgrid/services/aggregator/epub/EPUBSerializer.java
+++ b/src/main/java/info/textgrid/services/aggregator/epub/EPUBSerializer.java
@@ -6,6 +6,8 @@
 import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.ObjectNotFoundFault;
 import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.ProtocolNotImplementedFault;
 import info.textgrid.services.aggregator.ITextGridRep;
+import info.textgrid.services.aggregator.teicorpus.CorpusBasedExporter;
+import info.textgrid.services.aggregator.util.StylesheetManager;
 import info.textgrid.utils.linkrewriter.ConfigurableXMLRewriter;
 
 import java.io.File;
@@ -14,57 +16,112 @@
 import java.io.OutputStream;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.text.MessageFormat;
 import java.util.Deque;
 import java.util.LinkedList;
 import java.util.Map.Entry;
+import java.util.logging.Logger;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipOutputStream;
 
 import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Request;
+import javax.ws.rs.core.Response.ResponseBuilder;
 import javax.ws.rs.core.StreamingOutput;
 import javax.xml.stream.XMLStreamException;
+import javax.xml.transform.Source;
+
+import net.sf.saxon.s9api.SaxonApiException;
+import net.sf.saxon.s9api.XsltTransformer;
 
 import org.apache.cxf.helpers.FileUtils;
 import org.xml.sax.SAXException;
 
+import com.google.common.base.Optional;
 import com.google.common.io.ByteStreams;
 import com.google.common.io.Files;
 
-public class EPUBSerializer implements StreamingOutput {
-
-	private final File workingDir;
-	private final ITextGridRep repository;
-	private final File corpus;
-	private final String sid;
-
-
-
-	public EPUBSerializer(final File workingDir, final ITextGridRep repository,
-			final File corpus, final String sid) {
-		super();
-		this.workingDir = workingDir;
-		this.repository = repository;
-		this.corpus = corpus;
+public class EPUBSerializer extends CorpusBasedExporter implements
+		StreamingOutput {
+
+	private static final URI EPUB_XSL = URI
+			.create("/WEB-INF/stylesheets/epub.xsl");
+	private static final Logger logger = Logger.getLogger(EPUBSerializer.class
+			.getCanonicalName());
+
+	private final Optional<String> sid;
+	private StylesheetManager stylesheetManager;
+	private File workingDir;
+	private Optional<URI> providedStylesheet = Optional.absent();
+	private File corpus;
+
+	public EPUBSerializer(final ITextGridRep repository,
+			final StylesheetManager stylesheetManager, final String uriList,
+			final Optional<String> sid, final Request request) {
+		super(repository, request, uriList);
+		this.stylesheetManager = stylesheetManager;
 		this.sid = sid;
+		setMediaType("application/epub+zip");
+		setFileExtension("epub");
+		setFlatCorpus(true);
+		logger.info(MessageFormat.format("Started EPUB serialization for {0} after {1}",
+				uriList, stopwatch));
 	}
 
+	public void setStylesheet(final URI stylesheet) {
+		this.providedStylesheet = Optional.fromNullable(stylesheet);
+	}
 
+	@Override
+	public ResponseBuilder createResponse() throws MetadataParseFault,
+			ObjectNotFoundFault, IoFault, AuthFault,
+			ProtocolNotImplementedFault, IOException, SaxonApiException {
+
+		super.createResponse();
+		if (isNotModified())
+			return getResponseBuilder();
+
+		workingDir = Files.createTempDir();
+		logger.fine(MessageFormat.format("Using {0} to build the E-Book", workingDir));
+
+		corpus = new File(workingDir, "corpus.xml");
+		setBufferFactory(new FileBufferFactory(corpus));
+		final Source source = loadSource(true);
+
+		final XsltTransformer transformer;
+		if (providedStylesheet.isPresent())
+			transformer = getStylesheetManager().getStylesheet(
+					providedStylesheet.get(), sid, false, false).load();
+		else
+			transformer = getStylesheetManager().getStylesheet(EPUB_XSL, sid,
+					false, true).load();
+
+		transformer.setDestination(getStylesheetManager().xsltProcessor
+				.newSerializer(new File(workingDir, "output.xml")));
+		transformer.setSource(source);
+		transformer.transform();
+		logger.fine(MessageFormat.format("Finished EPUB transformation for {0} after {1}",
+				getRootURIs(), stopwatch));
+		return getResponseBuilder();
+	}
+
+	private StylesheetManager getStylesheetManager() {
+		return stylesheetManager;
+	}
 
 	@Override
 	public void write(final OutputStream output) throws IOException,
-	WebApplicationException {
+			WebApplicationException {
+		logger.info("Starting EPUB zipping for " + getRootURIs() + " after " + stopwatch);
 		final ZipOutputStream zip = new ZipOutputStream(output);
 		try {
 
 			// first entry is the uncompressed mimetype marker
 			new MimeTypeEntry("application/epub+zip").writeTo(zip);
 
-			// now filtered filesystem structure. FIXME add rewriting +
-			// images
 			final File mimeTypeFile = new File(workingDir, "mimetype");
 			final URI base = workingDir.toURI();
 			final File opsDir = new File(workingDir, "OPS");
-			final URI ops = opsDir.toURI();
 			final OPFManifest manifest = new OPFManifest(new File(opsDir,
 					"content.opf"));
 			final ConfigurableXMLRewriter xhtmlRewriter = new ConfigurableXMLRewriter(
@@ -96,11 +153,11 @@ public void write(final OutputStream output) throws IOException,
 						zip.putNextEntry(new ZipEntry(name));
 
 						if (Files.getFileExtension(name).equals("html")) {
-							xhtmlRewriter.rewrite(
-									new FileInputStream(child), zip);
-						} else if (Files.getFileExtension(name).equals("opf")) {
-							opfRewriter.rewrite(new FileInputStream(child),
+							xhtmlRewriter.rewrite(new FileInputStream(child),
 									zip);
+						} else if (Files.getFileExtension(name).equals("opf")) {
+							opfRewriter
+									.rewrite(new FileInputStream(child), zip);
 						} else {
 							Files.copy(child, zip);
 						}
@@ -109,14 +166,24 @@ public void write(final OutputStream output) throws IOException,
 				}
 			}
 
+			logger.info(MessageFormat.format(
+					"Adding {0} external items to EPUB for {1} after {2}",
+					manifest.externalItems.size(), getRootURIs(), stopwatch));
+
 			// now we need to add those files that are referenced by
 			// absolute URI in the manifest
-			for (final Entry<URI, String> externalItem : manifest.externalItems.entrySet()) {
-				final String pseudoFileName = base.relativize(new File(opsDir, manifest.getFileName(externalItem.getKey().toString())).toURI()).getPath();
+			for (final Entry<URI, String> externalItem : manifest.externalItems
+					.entrySet()) {
+				final String pseudoFileName = base.relativize(
+						new File(opsDir, manifest.getFileName(externalItem
+								.getKey().toString())).toURI()).getPath();
 				zip.putNextEntry(new ZipEntry(pseudoFileName));
 				ByteStreams.copy(
-						repository.getContent(externalItem.getKey(), sid), zip);
+						repository.getContent(externalItem.getKey(), sid.orNull()), zip);
 				zip.closeEntry();
+				logger.info(MessageFormat.format(
+						"Successfully exported EPUB for {0} after {1}",
+						getRootURIs(), stopwatch));
 			}
 		} catch (final SAXException e) {
 			throw new WebApplicationException(e);
@@ -137,6 +204,7 @@ public void write(final OutputStream output) throws IOException,
 		} finally {
 			zip.close();
 			FileUtils.removeDir(workingDir);
+			stopwatch.stop();
 		}
 
 	}
diff --git a/src/main/java/info/textgrid/services/aggregator/html/HTML.java b/src/main/java/info/textgrid/services/aggregator/html/HTML.java
deleted file mode 100644
index a77dd24f75dc8fc65636d30ea6d456d40fb6ef03..0000000000000000000000000000000000000000
--- a/src/main/java/info/textgrid/services/aggregator/html/HTML.java
+++ /dev/null
@@ -1,241 +0,0 @@
-package info.textgrid.services.aggregator.html;
-
-import info.textgrid.namespaces.metadata.core._2010.ObjectType;
-import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.AuthFault;
-import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.IoFault;
-import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.MetadataParseFault;
-import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.ObjectNotFoundFault;
-import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.ProtocolNotImplementedFault;
-import info.textgrid.services.aggregator.ITextGridRep;
-import info.textgrid.services.aggregator.ITextGridRep.TGOSupplier;
-import info.textgrid.services.aggregator.TextGridRepProvider;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.MalformedURLException;
-import java.net.URI;
-import java.net.URL;
-import java.text.MessageFormat;
-import java.util.concurrent.ExecutionException;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import javax.servlet.ServletContext;
-import javax.ws.rs.GET;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.WebApplicationException;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.Request;
-import javax.ws.rs.core.Response;
-import javax.xml.transform.stream.StreamSource;
-
-import net.sf.saxon.s9api.Processor;
-import net.sf.saxon.s9api.SaxonApiException;
-import net.sf.saxon.s9api.XsltCompiler;
-import net.sf.saxon.s9api.XsltExecutable;
-
-import org.apache.cxf.jaxrs.model.wadl.Description;
-
-import com.google.common.base.Optional;
-import com.google.common.cache.Cache;
-import com.google.common.cache.CacheBuilder;
-import com.google.common.cache.CacheLoader;
-import com.google.common.cache.RemovalListener;
-import com.google.common.cache.RemovalNotification;
-
-@Path("/html")
-@Description("Creates an HTML representation of the given TEI document, or aggregation of TEI documents. This is currently extremely experimental and probably broken.")
-public class HTML {
-
-	private static final String TO_HTML_XSL = "/WEB-INF/stylesheets/db2xhtml.xsl";
-	private static final String EXTRACT_BODY_XSL = "/WEB-INF/stylesheets/extractbody.xsl";
-
-	ITextGridRep repository = TextGridRepProvider.getInstance();
-
-	final Logger logger = Logger
-			.getLogger("info.textgrid.services.aggregator.html.HTML");
-
-	private XsltExecutable toHtml;
-	private XsltExecutable extractBody;
-
-	final Processor xsltProcessor;
-
-	@Context
-	private ServletContext servlet;
-
-	private Cache<URI, XsltExecutable> stylesheets;
-
-	XsltExecutable getToHtml() {
-		if (toHtml == null) {
-			try {
-				final URL stylesheet = servlet.getResource(TO_HTML_XSL);
-				final XsltCompiler xsltCompiler = xsltProcessor
-						.newXsltCompiler();
-				toHtml = xsltCompiler.compile(new StreamSource(stylesheet
-						.toString()));
-			} catch (final MalformedURLException e) {
-				throw new IllegalStateException(e);
-			} catch (final SaxonApiException e) {
-				throw new IllegalStateException(e);
-			}
-		}
-		return toHtml;
-	}
-
-	XsltExecutable getExtractBody() {
-		if (extractBody == null) {
-			try {
-				URL stylesheet;
-				stylesheet = servlet.getResource(EXTRACT_BODY_XSL);
-				extractBody = xsltProcessor.newXsltCompiler().compile(
-						new StreamSource(stylesheet.toString()));
-			} catch (final MalformedURLException e) {
-				throw new IllegalStateException(e);
-			} catch (final SaxonApiException e) {
-				throw new IllegalStateException(e);
-			}
-		}
-		return extractBody;
-	}
-
-	public HTML(final ITextGridRep repository) throws IOException {
-		this.repository = repository;
-		xsltProcessor = new Processor(false);
-		xsltProcessor.getUnderlyingConfiguration().setURIResolver(
-				new TGUriResolver(repository));
-		// xsltProcessor.getUnderlyingConfiguration().setAllowExternalFunctions(
-		// false); // we run external stylesheets
-		xsltProcessor.getUnderlyingConfiguration().setCompileWithTracing(true);
-		stylesheets = CacheBuilder.newBuilder().maximumSize(50).softValues()
-				.removalListener(new RemovalListener<URI, XsltExecutable>() {
-
-					@Override
-					public void onRemoval(
-							final RemovalNotification<URI, XsltExecutable> notification) {
-						logger.info(MessageFormat
-								.format("Removed stylesheet {0} from cache, reason: {1}",
-										notification.getKey(),
-										notification.getCause()));
-					}
-				}).build(new CacheLoader<URI, XsltExecutable>() {
-
-					@Override
-					public XsltExecutable load(final URI url) throws Exception {
-						final XsltCompiler compiler = xsltProcessor
-								.newXsltCompiler();
-						try {
-							final XsltExecutable executable = compiler
-									.compile(new StreamSource(url.toString()));
-							logger.log(Level.INFO,
-									"Successfully loaded stylesheet {0}", url);
-							return executable;
-						} catch (final Exception e) {
-							logger.log(Level.SEVERE, MessageFormat.format(
-									"Failed to load stylesheet {0}", url), e);
-							throw e;
-						}
-					}
-				});
-
-	}
-
-	/**
-	 * Returns an appropriate stylesheet for the given URI.
-	 *
-	 * Basically, we try the following options in order:
-	 * <ol>
-	 * <li>The stylesheet is cached -> return the cached version.
-	 * <li>The stylesheet is public or external -> load & cache it.
-	 * <li>The stylesheet is non-public TextGrid internal -> load & do not cache
-	 * it.
-	 * </ol>
-	 *
-	 * @param uri
-	 *            the URI of the stylesheet to load
-	 * @param sid
-	 *            the session ID to use, if present
-	 * @param forceLoad
-	 *            do not use a cached version even if present.
-	 * @throws IOException
-	 *             if an error occurs reading the stylesheet.
-	 * @throws SaxonApiException
-	 *             if saxon fails to compile the stylesheet.
-	 */
-	protected XsltExecutable getStylesheet(final URI uri,
-			final Optional<String> sid, final boolean forceLoad)
-			throws SaxonApiException, IOException {
-		XsltExecutable executable = null;
-
-		// (1) try cached version, if it exists
-		if (!forceLoad) {
-			executable = stylesheets.getIfPresent(uri);
-		}
-
-		if (executable == null) {
-
-			final XsltCompiler compiler = xsltProcessor.newXsltCompiler();
-			if (TGUriResolver.isResolveable(uri)) {
-
-				// (2/3) it's a TextGrid object, load it from TG-crud.
-				final TGOSupplier<InputStream> xsltSupplier = repository.read(
-						uri, sid.orNull());
-				executable = compiler.compile(new StreamSource(xsltSupplier
-						.getInput(), uri.toString()));
-
-				if (isPublic(xsltSupplier.getMetadata())) {
-					// (2) it's public -> we can cache it.
-					stylesheets.put(uri, executable);
-					logger.log(Level.INFO, "Cached public stylesheet {0}", uri);
-				} else {
-					logger.log(Level.INFO, "Loaded private stylesheet {0}", uri);
-				}
-			} else {
-				// (2) it's non-TextGrid -- load & cache it.
-				executable = compiler.compile(new StreamSource(uri.toString()));
-				stylesheets.put(uri, executable);
-				logger.log(Level.INFO, "Cached external stylesheet {0}", uri);
-			}
-		} else {
-			logger.log(Level.INFO, "Reusing cached stylesheed {0}", uri);
-		}
-
-		return executable;
-	}
-
-	private static boolean isPublic(final ObjectType metadata) {
-		try {
-			return metadata.getGeneric().getGenerated().getAvailability()
-					.contains("public");
-		} catch (final NullPointerException e) {
-			return false;
-		}
-	}
-
-	@GET
-	@Path(value = "/{object}")
-	@Produces(value = "text/xml")
-	public Response get(
-			@Description("The TextGrid URIs of the TEI document(s) or aggregation(s) to transform, separated by commas") @PathParam("object") final String uriList,
-			@Description("If given, an alternative XSLT stylesheet to use") @QueryParam("stylesheet") final URI xsluri,
-			@Description("If true, check for an <?xsl-stylesheet?> processing instruction in the document to render") @QueryParam("pi") final boolean pi,
-			@Description("If true and a stylesheet has been given, force reloading the stylesheet, do not cache") @QueryParam("refreshStylesheet") final boolean refreshStylesheet,
-			@Description("Session ID to access protected resources") @QueryParam("sid") final String sid,
-			@Description("If true, pass the information the stylesheet that its result will be embedded into some website") @QueryParam("embedded") final boolean embedded,
-			@Description("URL of the CSS that should be referenced in the HTML that is created") @QueryParam("css") final URI css,
-			@Description("The requested content type. E.g., text/html or text/xml") @QueryParam("mediatype") final String mediaType,
-			@Context final Request request) throws ObjectNotFoundFault,
-			MetadataParseFault, IoFault, AuthFault,
-			ProtocolNotImplementedFault, WebApplicationException, IOException,
-			SaxonApiException, ExecutionException {
-
-		logger.fine("HTML called for root objects: " + uriList);
-
-		final HTMLWriter writer = new HTMLWriter(this, uriList, xsluri,
-				refreshStylesheet, pi, embedded, css, sid, mediaType, request);
-		return writer.createResponse().build();
-	}
-
-}
diff --git a/src/main/java/info/textgrid/services/aggregator/html/HTMLWriter.java b/src/main/java/info/textgrid/services/aggregator/html/HTMLWriter.java
index a47fad3cc9e994f3a2cb55bc8a326cdb6e828e1a..1389d08b74e171a4d2cb1a0973e5c1acae2b7fc6 100644
--- a/src/main/java/info/textgrid/services/aggregator/html/HTMLWriter.java
+++ b/src/main/java/info/textgrid/services/aggregator/html/HTMLWriter.java
@@ -6,37 +6,30 @@
 import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.MetadataParseFault;
 import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.ObjectNotFoundFault;
 import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.ProtocolNotImplementedFault;
-import info.textgrid.services.aggregator.ArgUtils;
-import info.textgrid.services.aggregator.GenericExceptionMapper;
+import info.textgrid.services.aggregator.AbstractExporter;
 import info.textgrid.services.aggregator.ITextGridRep;
-import info.textgrid.services.aggregator.ITextGridRep.TGOSupplier;
-import info.textgrid.services.aggregator.RESTUtils;
-import info.textgrid.services.aggregator.teicorpus.TEICorpusSerializer;
+import info.textgrid.services.aggregator.teicorpus.CorpusBasedExporter;
+import info.textgrid.services.aggregator.util.StylesheetManager;
 
 import java.io.IOException;
-import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.text.MessageFormat;
-import java.util.Date;
 import java.util.Properties;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
 import javax.ws.rs.WebApplicationException;
-import javax.ws.rs.core.CacheControl;
+import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Request;
-import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.ResponseBuilder;
-import javax.ws.rs.core.Response.Status;
 import javax.ws.rs.core.StreamingOutput;
 import javax.xml.transform.OutputKeys;
 import javax.xml.transform.Source;
 import javax.xml.transform.TransformerConfigurationException;
 import javax.xml.transform.TransformerFactory;
 import javax.xml.transform.TransformerFactoryConfigurationError;
-import javax.xml.transform.stream.StreamSource;
 
 import net.sf.saxon.s9api.QName;
 import net.sf.saxon.s9api.SaxonApiException;
@@ -46,10 +39,7 @@
 import net.sf.saxon.s9api.XsltTransformer;
 
 import com.google.common.base.Optional;
-import com.google.common.base.Stopwatch;
 import com.google.common.base.Throwables;
-import com.google.common.io.ByteStreams;
-import com.google.common.io.FileBackedOutputStream;
 
 /**
  * The essential steps:
@@ -84,107 +74,73 @@
  * @author Thorsten Vitt <thorsten.vitt@uni-wuerzburg.de>
  *
  */
-public class HTMLWriter implements StreamingOutput {
+public class HTMLWriter extends CorpusBasedExporter implements StreamingOutput {
+	private static final URI TO_HTML_XSL = URI.create("/WEB-INF/stylesheets/db2xhtml.xsl");
+	private static final URI EXTRACT_BODY_XSL = URI.create("/WEB-INF/stylesheets/extractbody.xsl");
 
-	private final HTML service;
-
-	private final static Logger logger = Logger.getLogger(HTMLWriter.class.getCanonicalName());
-
-	// Options
-	private final URI rootURI;
-	private Optional<URI> stylesheetURI = Optional.absent();
+	private Optional<URI> explicitStylesheetURI = Optional.absent();
 	private boolean refreshStylesheet = false;
-	private boolean readStylesheetPI = false;
 	private boolean embedded = false;
 
 	private Optional<URI> css = Optional.absent();
-	private Optional<String> sid;
-
-	// detected and extracted
-	private enum SourceType {
-		UNKNOWN, XML, AGGREGATION, BASKET
-	}
-
-	private SourceType sourceType = SourceType.UNKNOWN;
-
-	private final ITextGridRep repository;
-
-	private ObjectType metadata;
+	ObjectType metadata;
 
-	private Optional<URI> associatedStylesheet = Optional.absent();
-
-	private Source source = null;
+	private Optional<URI> associatedStylesheetURI = Optional.absent();
 
 	private XsltTransformer transformer;
 
-	private final Stopwatch stopwatch;
-
-	private String actualStylesheet;
-
-	private TGOSupplier<InputStream> content;
-
-	private Request request;
+	private String actualStylesheetLabel;
 
 	private Optional<String> requestedMediaType;
 
-	private String rootURIs;
+	private Source source;
+	private StylesheetManager stylesheetManager;
+	private static final Logger logger = Logger.getLogger(HTMLWriter.class.getCanonicalName());
+
 
-	private ObjectType[] rootObjects;
 
 	// Constructor and configuration
 
-	public HTMLWriter(final HTML service, final String rootURIs) {
-		stopwatch = new Stopwatch();
-		stopwatch.start();
+	public HTMLWriter(final ITextGridRep repository, final StylesheetManager stylesheetManager, final String rootURIs, final Request request) {
+		super(repository, request, rootURIs);
 		logger.log(Level.INFO, "Starting HTML export for {0}", rootURIs);
-		this.service = service;
-		if (rootURIs.contains(",")) {
-			this.sourceType = SourceType.BASKET;
-			this.rootURI = null;
-			this.rootURIs = rootURIs;
-		} else
-			this.rootURI = URI.create(rootURIs);
-		this.repository = service.repository;
+		this.stylesheetManager = stylesheetManager;
+		setDisposition(Disposition.INLINE);
+		setFileExtension("html");
+		setMediaType(MediaType.TEXT_HTML);
+		logger.info(MessageFormat.format("Started HTML serialization for {0} after {1}",
+				this.rootURIs, stopwatch));
 	}
 
-	public HTMLWriter(final HTML service, final String rootURIs,
+	public HTMLWriter(final ITextGridRep repository, final StylesheetManager stylesheetManager, final String rootURIs,
 			final URI stylesheetURI, final boolean refreshStylesheet,
 			final boolean readStylesheetPI, final boolean embedded,
 			final URI css, final String sid, final String mediaType,
 			final Request request) {
 
-		this(service, rootURIs);
+		this(repository, stylesheetManager, rootURIs, request);
 
-		this.stylesheetURI = Optional.fromNullable(stylesheetURI);
+		this.explicitStylesheetURI = Optional.fromNullable(stylesheetURI);
 		this.refreshStylesheet = refreshStylesheet;
 		this.readStylesheetPI = readStylesheetPI;
 		this.embedded = embedded;
+
 		this.css = Optional.fromNullable(css);
 		this.sid(sid);
 		this.requestedMediaType = Optional.fromNullable(mediaType);
-		this.request = request;
 	}
 
-	public HTMLWriter sid(final String sid) {
-		if (sid == null || sid.isEmpty()) {
-			this.sid = Optional.absent();
-		} else {
-			this.sid = Optional.of(sid);
-		}
+	public AbstractExporter stylesheet(final URI uri) {
+		this.explicitStylesheetURI = Optional.fromNullable(uri);
 		return this;
 	}
 
-	public HTMLWriter stylesheet(final URI uri) {
-		this.stylesheetURI = Optional.fromNullable(uri);
-		return this;
-	}
-
-	public HTMLWriter refresh(final boolean refresh) {
+	public AbstractExporter refresh(final boolean refresh) {
 		this.refreshStylesheet = refresh;
 		return this;
 	}
 
-	public HTMLWriter embedded(final boolean embedded) {
+	public AbstractExporter embedded(final boolean embedded) {
 		this.readStylesheetPI = embedded;
 		if (embedded) {
 			this.sourceType = SourceType.XML;
@@ -192,82 +148,17 @@ public HTMLWriter embedded(final boolean embedded) {
 		return this;
 	}
 
-	public HTMLWriter css(final URI css) {
+	public AbstractExporter css(final URI css) {
 		this.css = Optional.fromNullable(css);
 		return this;
 	}
 
-	protected HTMLWriter loadSource() throws ObjectNotFoundFault,
-			MetadataParseFault,
-			IoFault, ProtocolNotImplementedFault, AuthFault, IOException {
-
-		if (sourceType != SourceType.BASKET) {
-			metadata = getContent().getMetadata();
-			final String format = metadata.getGeneric().getProvided().getFormat();
-			if (format.contains("aggregation")) {
-				sourceType = SourceType.AGGREGATION;
-			} else if (format.matches("^text/.*xml.*$")) {
-				sourceType = SourceType.XML;
-			} else {
-				final String errorMsg = MessageFormat
-						.format("The HTML export can only convert aggregations or XML documents to EPUB, however, the document {0} you referred to has the MIME type {1}.",
-								rootURI, format);
-				throw new WebApplicationException(
-						GenericExceptionMapper.toResponse(
-								Status.UNSUPPORTED_MEDIA_TYPE, errorMsg, ""));
-			}
-		}
-
-		if (sourceType == SourceType.AGGREGATION || sourceType == SourceType.BASKET) {
-			final TEICorpusSerializer corpusSerializer = sourceType == SourceType.AGGREGATION? new TEICorpusSerializer(
-					metadata, false, sid.orNull()) : new TEICorpusSerializer(getRootObjects(), false, sid.orNull());
-			final FileBackedOutputStream corpusBuffer = new FileBackedOutputStream(
-					1024 * 1024, true);
-			corpusSerializer.write(corpusBuffer);
-			corpusBuffer.close();
-			this.source = new StreamSource(corpusBuffer.getSupplier()
-					.getInput());
-		} else if (sourceType == SourceType.XML && readStylesheetPI) {
-			final FileBackedOutputStream xmlBuffer = new FileBackedOutputStream(
-					1024 * 1024, true);
-			ByteStreams.copy(getContent(), xmlBuffer);
-			detectEmbeddedStylesheet(xmlBuffer.getSupplier().getInput());
-			this.source = new StreamSource(xmlBuffer.getSupplier().getInput());
-		} else {
-			this.source = new StreamSource(getContent().getInput(),
-					rootURI.toString());
-		}
-		logger.log(Level.INFO, MessageFormat.format("Fetched source for {0}, type={1}, after {2}", rootURI, sourceType, stopwatch.toString()));
-
-		return this;
-	}
-
-	private ObjectType[] getRootObjects() throws MetadataParseFault, ObjectNotFoundFault, IoFault, AuthFault {
-		if (rootObjects == null) {
-			if (sourceType == SourceType.BASKET) {
-				rootObjects = ArgUtils.extractRootObjects(rootURIs, sid.orNull(), repository.getCRUDService());
-				logger.log(Level.INFO, MessageFormat.format("Collected root objects for basket {0} after {1}", rootURIs, stopwatch.toString()));
-			} else
-				rootObjects = new ObjectType[] { getContent().getMetadata() };
-		}
-		return rootObjects;
-	}
-
-	private TGOSupplier<InputStream> getContent() {
-		if (content == null) {
-			content = service.repository.read(rootURI, sid.orNull());
-			logger.info(MessageFormat.format("Fetched source for {0} up to metadata after {1}", rootURI, stopwatch));
-		}
-		return content;
-	}
-
-	private void detectEmbeddedStylesheet(final InputStream input) {
+	void detectEmbeddedStylesheet(final Source source) {
 		try {
-			final Source associatedStylesheet = TransformerFactory
-					.newInstance().getAssociatedStylesheet(
-							new StreamSource(input, rootURI.toString()), null,
-							null, null);
-			this.associatedStylesheet = Optional.of(new URI(
+			final Source associatedStylesheet =
+					TransformerFactory.newInstance()
+					.getAssociatedStylesheet(source, null, null, null);
+			this.associatedStylesheetURI = Optional.of(new URI(
 					associatedStylesheet.getSystemId()));
 
 		} catch (final TransformerConfigurationException e) {
@@ -281,36 +172,43 @@ private void detectEmbeddedStylesheet(final InputStream input) {
 
 	private XsltExecutable getStylesheet() throws SaxonApiException,
 			IOException {
-		if (stylesheetURI.isPresent()) {
-			actualStylesheet = stylesheetURI.get().toString() + " (explicit)";
-			return service.getStylesheet(stylesheetURI.get(), sid,
-					refreshStylesheet);
-		} else if (associatedStylesheet.isPresent()) {
-			actualStylesheet = associatedStylesheet.get().toString() + " (associated)";
-			return service.getStylesheet(associatedStylesheet.get(), sid,
-					refreshStylesheet);
+		if (explicitStylesheetURI.isPresent()) {
+			actualStylesheetLabel = explicitStylesheetURI.get().toString() + " (explicit)";
+			return stylesheetManager.getStylesheet(explicitStylesheetURI.get(), getSid(),
+					refreshStylesheet, false);
+		} else if (associatedStylesheetURI.isPresent()) {
+			actualStylesheetLabel = associatedStylesheetURI.get().toString() + " (associated)";
+			return stylesheetManager.getStylesheet(associatedStylesheetURI.get(), getSid(),
+					refreshStylesheet, false);
 		} else {
-			actualStylesheet = "(internal)";
-			return service.getToHtml();
+			actualStylesheetLabel = "(internal)";
+			return stylesheetManager.getStylesheet(TO_HTML_XSL, getSid(), false, true);
 		}
 	}
 
-	protected HTMLWriter loadStylesheet() throws SaxonApiException,
-			IOException, ObjectNotFoundFault, MetadataParseFault, IoFault,
-			ProtocolNotImplementedFault, AuthFault {
-		if (source == null) {
-			loadSource();
+	private Source getSource() throws ObjectNotFoundFault, MetadataParseFault, IoFault, ProtocolNotImplementedFault, AuthFault, IOException {
+		if (this.source == null) {
+			this.source = loadSource(readStylesheetPI);
+			if (readStylesheetPI && sourceType == SourceType.XML) {
+				detectEmbeddedStylesheet(source);
+				source = loadSource(readStylesheetPI);
+			}
 		}
+		return source;
+	}
+
+	public XsltTransformer getTransformer() throws SaxonApiException, IOException {
+		if (transformer != null)
+			return transformer;
 
 		final XsltTransformer transformer = getStylesheet().load();
-		if (sid.isPresent()) {
-			transformer.setURIResolver(new TGUriResolver(repository, sid));
+		if (getSid().isPresent()) {
+			transformer.setURIResolver(new TGUriResolver(repository, getSid()));
 		}
 		transformer.setParameter(new QName("graphicsURLPattern"),
 				new XdmAtomicValue(repository.getCRUDRestEndpoint()
 						+ "/@URI@/data"
-						+ ((sid == null || "".equals(sid)) ? ""
-								: ("?sessionId=" + sid))));
+						+ (getSid().isPresent()? "?sessionId=" + getSid().get() : "")));
 		if (css.isPresent()) {
 			transformer.setParameter(new QName("cssFile"), new XdmAtomicValue(
 					css.get()));
@@ -326,76 +224,61 @@ protected HTMLWriter loadStylesheet() throws SaxonApiException,
 		}
 
 		this.transformer = transformer;
-		logger.log(Level.INFO, MessageFormat.format("Prepared XSLT stylesheet {1} for {0} after {2}", rootURI, actualStylesheet, stopwatch.toString()));
-		return this;
+		logger.log(Level.INFO, MessageFormat.format("Prepared XSLT stylesheet {1} for {0} after {2}", rootURIs, actualStylesheetLabel, stopwatch.toString()));
+		return transformer;
 	}
 
 	@Override
 	public void write(final OutputStream out) throws IOException,
 			WebApplicationException {
 		try {
-			if (source == null) {
-				loadSource();
-			}
-			if (transformer == null) {
-				loadStylesheet();
-			}
-			logger.log(Level.INFO, MessageFormat.format("Ready for transformation of {0} after {1}", rootURI, stopwatch.toString()));
-			final Serializer serializer = service.xsltProcessor
-					.newSerializer(out);
+			logger.log(Level.INFO, MessageFormat.format("Ready for transformation of {0} after {1}", rootURIs, stopwatch.toString()));
+			final Serializer serializer = stylesheetManager.xsltProcessor.newSerializer(out);
 			if (embedded) {
-				final XsltTransformer extractBody = service.getExtractBody()
-						.load();
+				final XsltTransformer extractBody = stylesheetManager.getStylesheet(EXTRACT_BODY_XSL, getSid(), false, true).load();
 				extractBody.setDestination(serializer);
-				transformer.setDestination(extractBody);
+				getTransformer().setDestination(extractBody);
 			} else {
-				transformer.setDestination(serializer);
+				getTransformer().setDestination(serializer);
 			}
-			transformer.transform();
+			getTransformer().transform();
 
 		} catch (final Exception e) {
-			logger.log(Level.SEVERE, MessageFormat.format("Transformation of {0} failed ({2}) after {1}", rootURI, stopwatch.toString(), e.getMessage()));
+			logger.log(Level.SEVERE, MessageFormat.format("Transformation of {0} failed ({2}) after {1}", rootURIs, stopwatch.toString(), e.getMessage()));
 			Throwables.propagateIfPossible(e, IOException.class,
 					WebApplicationException.class);
 			Throwables.propagate(e);
 		}
-		logger.log(Level.INFO, MessageFormat.format("Finished and delivered transformation of {0} after {1}", rootURI, stopwatch.toString()));
+		logger.log(Level.INFO, MessageFormat.format("Finished and delivered transformation of {0} after {1}", rootURIs, stopwatch.toString()));
 		stopwatch.stop();
 	}
 
+
+
+	@Override
+	protected ResponseBuilder doEvaluatePreconditions()
+			throws MetadataParseFault, ObjectNotFoundFault, IoFault, AuthFault {
+		if (refreshStylesheet)
+			return null;
+		else
+			return super.doEvaluatePreconditions();
+	}
+
+	@Override
 	public ResponseBuilder createResponse() throws ObjectNotFoundFault, MetadataParseFault, IoFault, ProtocolNotImplementedFault, AuthFault, IOException, SaxonApiException {
-		final Date lastModified = RESTUtils.createLastModified(getRootObjects());
-		if (!refreshStylesheet && request != null) {
-			final ResponseBuilder builder = request.evaluatePreconditions(lastModified);
-			if (builder != null) {
-				logger.info(MessageFormat.format("Aborting: client already has HTML for {0} (after {1})", rootURI, stopwatch));
-				return builder;
-			} else {
-				logger.info("Creating new transformation.");
-			}
-		}
+		if (isNotModified())
+			return getResponseBuilder();
 
-		loadSource();
-		loadStylesheet();
+		getTransformer().setSource(getSource());
 
-		final ResponseBuilder builder = Response.ok();
 		if (requestedMediaType.isPresent()) {
-			builder.type(requestedMediaType.get());
+			setMediaType(requestedMediaType.get());
 		} else {
-			final Properties outputProperties = transformer
+			final Properties outputProperties = getTransformer()
 					.getUnderlyingController().getOutputProperties();
-			builder.type(outputProperties.getProperty(OutputKeys.MEDIA_TYPE,
+			setMediaType(outputProperties.getProperty(OutputKeys.MEDIA_TYPE,
 					"text/html"));
 		}
-
-		RESTUtils.configureCache(builder, lastModified, sid.isPresent());
-		builder.header(
-				"Content-Disposition",
-				"inline;filename=\""
-						+ ArgUtils.createFilename(getRootObjects(), null, "html")
-						+ "\"");
-		builder.entity(this);
-		return builder;
+		return super.createResponse();
 	}
-
-}
+}
\ No newline at end of file
diff --git a/src/main/java/info/textgrid/services/aggregator/pdf/PDF.java b/src/main/java/info/textgrid/services/aggregator/pdf/PDF.java
index d37df11790a0044837c76274493bcf01a5ca639e..54c96ed18cbaa4673d2c3d5212c76a312566ab56 100644
--- a/src/main/java/info/textgrid/services/aggregator/pdf/PDF.java
+++ b/src/main/java/info/textgrid/services/aggregator/pdf/PDF.java
@@ -1,43 +1,41 @@
 package info.textgrid.services.aggregator.pdf;
 
 import info.textgrid._import.RewriteMethod;
+import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.AuthFault;
+import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.IoFault;
+import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.MetadataParseFault;
+import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.ObjectNotFoundFault;
+import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.ProtocolNotImplementedFault;
+import info.textgrid.services.aggregator.AbstractExporter;
 import info.textgrid.services.aggregator.GenericExceptionMapper;
 import info.textgrid.services.aggregator.ITextGridRep;
 import info.textgrid.services.aggregator.ITextGridRep.TGOSupplier;
-import info.textgrid.services.aggregator.RESTUtils;
-import info.textgrid.services.aggregator.XSLTErrorListener;
+import info.textgrid.services.aggregator.util.StylesheetManager;
+import info.textgrid.services.aggregator.util.XSLTErrorListener;
 import info.textgrid.utils.linkrewriter.ConfigurableXMLRewriter;
 import info.textgrid.utils.linkrewriter.ImportMapping;
 
 import java.io.File;
-import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.net.MalformedURLException;
+import java.io.OutputStream;
 import java.net.URI;
-import java.net.URL;
 import java.nio.charset.Charset;
 import java.util.Set;
 import java.util.logging.Logger;
 
 import javax.servlet.ServletContext;
-import javax.ws.rs.GET;
 import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
 import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.core.Context;
-import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Request;
+import javax.ws.rs.core.Response.ResponseBuilder;
 import javax.ws.rs.core.Response.Status;
 import javax.xml.stream.XMLStreamException;
 import javax.xml.transform.stream.StreamSource;
 
-import net.sf.saxon.s9api.Processor;
 import net.sf.saxon.s9api.SaxonApiException;
-import net.sf.saxon.s9api.XsltCompiler;
-import net.sf.saxon.s9api.XsltExecutable;
 import net.sf.saxon.s9api.XsltTransformer;
 
 import org.apache.commons.io.FileUtils;
@@ -51,31 +49,34 @@
 		"This is currently highly experimental, and it requires LaTeX and " +
 		"the TEI stylesheets to be installed on the machine onto which " +
 		"this service is deployed.")
-public class PDF {
+public class PDF extends AbstractExporter {
 
-	private static final String TO_PDF_XSL = "/WEB-INF/xml/tei/stylesheet/latex2/tei.xsl";
+	private static final URI TO_PDF_XSL = URI.create("/WEB-INF/xml/tei/stylesheet/latex2/tei.xsl");
 
 	final Logger logger = Logger
 			.getLogger("info.textgrid.services.aggregator.pdf");
-	private XsltExecutable teiToLatex;
-	private final Processor xsltProcessor;
-	private final ITextGridRep repository;
 
 	@Context
 	private ServletContext servlet;
 
-	public PDF(final ITextGridRep repository) {
-		this.repository = repository;
-		xsltProcessor = new Processor(false);
+	private StylesheetManager stylesheetManager;
+
+	private File workingDir;
+
+	public PDF(final ITextGridRep repository, final StylesheetManager manager, final Request request, final URI uri) {
+		super(repository, request, uri.toString());
+		this.stylesheetManager = manager;
+		
+		this.setFileExtension("pdf");
+		this.setMediaType("application/pdf");
 	}
 
-	@GET
-	@Path("/{uri}")
-	@Produces("application/pdf")
-	public Response get(
-			@Description("The TextGrid URI of a TEI document to transform") @PathParam("uri") final URI uri,
-			@Description("Session ID to access protected resources") @QueryParam("sid") final String sid) {
-		final File workingDir = Files.createTempDir();
+	public ResponseBuilder createResponse() throws MetadataParseFault, ObjectNotFoundFault, IoFault, AuthFault, ProtocolNotImplementedFault, IOException, SaxonApiException {
+		super.createResponse();
+		if (isNotModified())
+			return getResponseBuilder();
+		
+		workingDir = Files.createTempDir();
 		try {
 
 			// First step: Rewrite with a non-initalized mapping to a temporary
@@ -86,10 +87,10 @@ public Response get(
 					mapping, true);
 			rewriter.configure(URI.create("internal:tei#tei"));
 			rewriter.recordMissingReferences();
-			final TGOSupplier<InputStream> textSupplier = repository.read(uri, sid);
+			final TGOSupplier<InputStream> textSupplier = getContent();
 			final String rootObjectFormat = textSupplier.getMetadata().getGeneric().getProvided().getFormat();
 			if (!rootObjectFormat.startsWith("text/xml"))
-				return GenericExceptionMapper.toResponse(Status.UNSUPPORTED_MEDIA_TYPE, "PDF generation currently supports only text/xml documents. The requested document, " + uri + ", has an invalid document type, however:", rootObjectFormat);
+				return GenericExceptionMapper.toResponse(Status.UNSUPPORTED_MEDIA_TYPE, "PDF generation currently supports only text/xml documents. The requested documen has an invalid document type, however:", rootObjectFormat);
 
 			final FileBackedOutputStream buffer = new FileBackedOutputStream(
 					1024 * 1024);
@@ -107,7 +108,7 @@ public Response get(
 								&& mapping
 								.getImportObjectForTextGridURI(missingReference) == null) {
 					final TGOSupplier<InputStream> supplier = repository.read(
-							missingURI, sid);
+							missingURI, getSid().get());
 					final String format = supplier.getMetadata().getGeneric()
 							.getProvided().getFormat();
 					String fileName = null;
@@ -140,9 +141,9 @@ public Response get(
 			buffer.close();
 
 			// now generate the TeX
-			final XsltTransformer transformer = getTeiToLatex().load();
+			final XsltTransformer transformer = stylesheetManager.getStylesheet(TO_PDF_XSL, getSid(), false, true).load();
 			final File tex = new File(workingDir, "data.tex");
-			transformer.setDestination(xsltProcessor.newSerializer(tex));
+			transformer.setDestination(stylesheetManager.xsltProcessor.newSerializer(tex));
 			transformer.setSource(new StreamSource(teiFile));
 			transformer.setErrorListener(new XSLTErrorListener(logger));
 			transformer.transform();
@@ -154,22 +155,12 @@ public Response get(
 			if (latexProcess.waitFor() != 0) {
 				final File logFile = new File(workingDir, "data.log");
 				if (logFile.canRead())
-					return GenericExceptionMapper.toResponse(Status.INTERNAL_SERVER_ERROR, "The requested document, " + uri + ", was transformed to a file LaTeX failed to deal with. Below is the LaTeX log file.",
+					return GenericExceptionMapper.toResponse(Status.INTERNAL_SERVER_ERROR, "The requested document was transformed to a file LaTeX failed to deal with. Below is the LaTeX log file.",
 							Files.toString(logFile, Charset.forName("UTF-8")));
 				else
 					throw new IllegalStateException("LaTeX process failed, no log file found.");
 			}
-
-			final Response response = RESTUtils
-					.attachmentResponse(
-							textSupplier.getMetadata().getGeneric()
-							.getProvided().getTitle().get(0)
-							.concat(".pdf"))
-							.type("application/pdf")
-							.entity(new FileInputStream(
-									new File(workingDir, "data.pdf"))).build();
-			FileUtils.deleteDirectory(workingDir);
-			return response;
+			return getResponseBuilder();
 
 		} catch (final IOException e) {
 			throw new WebApplicationException(e);
@@ -182,24 +173,24 @@ public Response get(
 		}
 	}
 
-	public XsltExecutable getTeiToLatex() {
-		if (teiToLatex == null) {
-			try {
-				final URL stylesheet = servlet.getResource(TO_PDF_XSL);
-				final XsltCompiler xsltCompiler = xsltProcessor
-						.newXsltCompiler();
-				teiToLatex = xsltCompiler.compile(new StreamSource(stylesheet
-						.toString()));
-			} catch (final MalformedURLException e) {
-				throw new IllegalStateException(
-						"Failed to initialize TEI to LaTeX stylesheet", e);
-			} catch (final SaxonApiException e) {
-				throw new IllegalStateException(
-						"Failed to initialize TEI to LaTeX stylesheet", e);
-			}
+
+	@Override
+	public void write(OutputStream output) throws IOException {
+		try {
+			Files.copy(new File(workingDir, "data.pdf"), output);
+		} finally {
+			FileUtils.deleteDirectory(workingDir);
+			workingDir = null;
 		}
-		return teiToLatex;
 	}
 
+	@Override
+	protected void finalize() throws Throwable {
+		if (workingDir != null && workingDir.exists())
+			FileUtils.deleteDirectory(workingDir);
+		super.finalize();
+	}
+
+	
 
 }
diff --git a/src/main/java/info/textgrid/services/aggregator/teicorpus/CorpusBasedExporter.java b/src/main/java/info/textgrid/services/aggregator/teicorpus/CorpusBasedExporter.java
new file mode 100644
index 0000000000000000000000000000000000000000..df25677304cd1a35baaec222391382f519dfc1a0
--- /dev/null
+++ b/src/main/java/info/textgrid/services/aggregator/teicorpus/CorpusBasedExporter.java
@@ -0,0 +1,173 @@
+package info.textgrid.services.aggregator.teicorpus;
+
+import info.textgrid.namespaces.metadata.core._2010.ObjectType;
+import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.AuthFault;
+import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.IoFault;
+import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.MetadataParseFault;
+import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.ObjectNotFoundFault;
+import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.ProtocolNotImplementedFault;
+import info.textgrid.services.aggregator.AbstractExporter;
+import info.textgrid.services.aggregator.GenericExceptionMapper;
+import info.textgrid.services.aggregator.ITextGridRep;
+import info.textgrid.services.aggregator.AbstractExporter.SourceType;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.text.MessageFormat;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Request;
+import javax.ws.rs.core.Response.Status;
+import javax.xml.transform.Source;
+import javax.xml.transform.stream.StreamSource;
+
+import com.google.common.io.ByteStreams;
+import com.google.common.io.FileBackedOutputStream;
+import com.google.common.io.Files;
+import com.google.common.io.InputSupplier;
+
+public abstract class CorpusBasedExporter extends AbstractExporter {
+
+	private static final Logger logger = Logger.getLogger(CorpusBasedExporter.class.getCanonicalName());
+	protected boolean readStylesheetPI = false;
+	private InputSupplier<? extends InputStream> sourceBuffer = null;
+	private IBufferFactory bufferFactory = new FBBAOSBufferFactory();
+	private boolean flatCorpus = false;
+
+	public CorpusBasedExporter(final ITextGridRep repository,
+			final Request request, final String uriList) {
+		super(repository, request, uriList);
+	}
+
+	public static interface IBufferFactory {
+		public OutputStream getBufferSink();
+		public InputSupplier<? extends InputStream> getBufferContents();
+	}
+
+	public static final class FBBAOSBufferFactory implements IBufferFactory {
+		private FileBackedOutputStream sink;
+
+		@Override
+		public OutputStream getBufferSink() {
+			this.sink = new FileBackedOutputStream(1024 * 1024, true);
+			return sink;
+		}
+
+		@Override
+		public InputSupplier<InputStream> getBufferContents() {
+			return sink.getSupplier();
+		}
+	}
+
+	public static final class FileBufferFactory implements IBufferFactory {
+		private final File file;
+		private FileOutputStream sink;
+
+		public FileBufferFactory(final File file) throws FileNotFoundException {
+			this.file = file;
+			this.sink = new FileOutputStream(file);
+		}
+
+		@Override
+		public InputSupplier<? extends InputStream> getBufferContents() {
+			return Files.newInputStreamSupplier(file);
+		}
+
+		@Override
+		public OutputStream getBufferSink() {
+			return sink;
+		}
+	}
+
+	/**
+	 * Loads a source for the given document.
+	 *
+	 * <p>
+	 * If the input is a basket or aggregation, it will be read and converted to
+	 * TEIcorpus document. If it is an XML document, it will be returned as-is.
+	 * </p>
+	 *
+	 * @param bufferRequired
+	 *            if <code>true</code>, the document will be cached locally and
+	 *            subsequent calls to this method will return a source created
+	 *            from the local cache. Use this if you intend to read the
+	 *            document multiple times.
+	 *
+	 *            Currently, documents that are not XML documents are buffered
+	 *            regardless of this parameter.
+	 */
+	protected Source loadSource(final boolean bufferRequired)
+			throws ObjectNotFoundFault, MetadataParseFault, IoFault,
+			ProtocolNotImplementedFault, AuthFault, IOException {
+
+		if (sourceBuffer != null)
+			return new StreamSource(sourceBuffer.getInput());
+
+		ObjectType metadata;
+		if (sourceType != SourceType.BASKET) {
+			metadata = getContentSimple().getMetadata();
+			final String format = metadata.getGeneric().getProvided()
+					.getFormat();
+			if (format.contains("aggregation")) {
+				sourceType = SourceType.AGGREGATION;
+			} else if (format.matches("^text/.*xml.*$")) {
+				sourceType = SourceType.XML;
+			} else {
+				final String errorMsg = MessageFormat
+						.format("This exporter can only convert aggregations or XML documents, however, the document {0} you referred to has the MIME type {1}.",
+								rootURIs, format);
+				throw new WebApplicationException(
+						GenericExceptionMapper.toResponse(
+								Status.UNSUPPORTED_MEDIA_TYPE, errorMsg, "").build());
+			}
+		}
+
+		StreamSource source;
+		if (sourceType == SourceType.AGGREGATION
+				|| sourceType == SourceType.BASKET) {
+			final TEICorpusSerializer corpusSerializer =
+					new TEICorpusSerializer(getRootObjects(), isFlatCorpus(), getSid().orNull());
+			final OutputStream corpusBuffer = getBufferFactory().getBufferSink();
+			corpusSerializer.write(corpusBuffer);
+			corpusBuffer.close();
+			sourceBuffer = getBufferFactory().getBufferContents();
+			source = new StreamSource(sourceBuffer.getInput());
+		} else if (sourceType == SourceType.XML && bufferRequired) {
+			final OutputStream xmlBuffer = getBufferFactory().getBufferSink();
+			ByteStreams.copy(getContentSimple(), xmlBuffer);
+			sourceBuffer = getBufferFactory().getBufferContents();
+			source = new StreamSource(sourceBuffer.getInput());
+		} else {
+			source = new StreamSource(getContentSimple().getInput(),
+					rootURIs.toString());
+		}
+		logger.log(Level.INFO, MessageFormat.format(
+				"Fetched source for {0}, type={1}, after {2}", rootURIs,
+				sourceType, stopwatch.toString()));
+
+		return source;
+	}
+
+	private IBufferFactory getBufferFactory() {
+		return bufferFactory;
+	}
+
+	protected void setBufferFactory(final IBufferFactory bufferFactory) {
+		this.bufferFactory = bufferFactory;
+	}
+
+	public boolean isFlatCorpus() {
+		return flatCorpus;
+	}
+
+	public void setFlatCorpus(final boolean flatCorpus) {
+		this.flatCorpus = flatCorpus;
+	}
+
+}
\ No newline at end of file
diff --git a/src/main/java/info/textgrid/services/aggregator/teicorpus/TEICorpus.java b/src/main/java/info/textgrid/services/aggregator/teicorpus/TEICorpus.java
deleted file mode 100644
index e03a3999d87a5c33d13c1099b7f1074659925f6c..0000000000000000000000000000000000000000
--- a/src/main/java/info/textgrid/services/aggregator/teicorpus/TEICorpus.java
+++ /dev/null
@@ -1,125 +0,0 @@
-package info.textgrid.services.aggregator.teicorpus;
-
-import info.textgrid.namespaces.metadata.core._2010.ObjectType;
-import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.AuthFault;
-import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.IoFault;
-import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.MetadataParseFault;
-import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.ObjectNotFoundFault;
-import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.TGCrudService;
-import info.textgrid.services.aggregator.ArgUtils;
-import info.textgrid.services.aggregator.ITextGridRep;
-import info.textgrid.services.aggregator.RESTUtils;
-import info.textgrid.utils.export.filenames.DefaultFilenamePolicy;
-
-import java.net.URISyntaxException;
-import java.util.Date;
-import java.util.logging.Logger;
-
-import javax.ws.rs.DefaultValue;
-import javax.ws.rs.GET;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.Request;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.Response.ResponseBuilder;
-
-import org.apache.cxf.jaxrs.model.wadl.Description;
-import org.apache.cxf.jaxrs.model.wadl.Descriptions;
-import org.apache.cxf.jaxrs.model.wadl.DocTarget;
-
-/*
- * Generating a TEIcorpus document from a (single) aggregation
- * 
- * Header:
- * 
- * Header could be generated in the same way as in the metadata editor, i.e.
- * paste together the ancestor metadata records (including matching work
- * records) and then run an XSLT stylesheet.
- * 
- * Children's Header:
- * 
- * For descendant aggregations, we still need a header. Either we consider this
- * redundant, or we preserve the metadata records we copied together for the
- * respective ancestor levels, append the current level & run the
- * transformation.
- * 
- * Body:
- * 
- * Naïve approach: Load and recursively process the aggregations, check each
- * one's content type Probably better: SearchClient has a listAggregation()
- * method that is also used by the navigator and that returns metadata records.
- * We could use this, so it's only one large request per aggregation. Should be
- * reasonably fast.
- */
-@Description("Creates a TEI corpus of all the TEI documents (recursively) aggregated by the given aggregation")
-@Path("/teicorpus")
-public class TEICorpus {
-
-	private final ITextGridRep repository;
-
-	final Logger logger = Logger.getLogger("info.textgrid.services.aggregator");
-
-	public TEICorpus(final ITextGridRep repository) {
-		this.repository = repository;
-	}
-
-
-	@GET
-	@Path(value="/{uris}")
-	@Produces("text/xml")
-	@Descriptions({
-			@Description(target=DocTarget.METHOD, value="Creates a TEI corpus of all the TEI documents (recursively) aggregated by the given aggregation"),
-			@Description(target=DocTarget.RETURN, value="TEI corpus document")
-	})
-	public Response get(@Description("TextGrid URIs of the root objects, separated by commas") @PathParam("uris") final String uriList,
-			@Description("Whether to generate a Content-Disposition: attachment header") @QueryParam("attach") @DefaultValue("true") final boolean attach,
-			@Description("If true, no intermediate TEI corpus documents will be generated for intermediate aggregations, hierarchical structure will be lost") @QueryParam("flat") @DefaultValue("false") final boolean flat,
-			@Description("Title for the container if multiple root objects are given") @QueryParam("title") final String titleArgument, 
-			@Description("Session id for accessing restricted resources") @QueryParam("sid") final String sid,
-			@Context Request request)
-					throws URISyntaxException, ObjectNotFoundFault, MetadataParseFault, IoFault, AuthFault {
-		logger.fine("TEIcorpus called for root object(s): " + uriList);
-		final TGCrudService crud = repository.getCRUDService();
-		logger.finest("Yo, clients are there.");
-		final ObjectType[] rootObjects = ArgUtils.extractRootObjects(uriList, sid, crud);
-		
-		// Return fast for If-Modified-Since
-		final Date lastModified = RESTUtils.createLastModified(rootObjects);
-		if (request != null) {
-			ResponseBuilder response304 = request.evaluatePreconditions(lastModified);
-			if (response304 != null)
-				return response304.build();
-		}
-		
-		final String title = ArgUtils.createTitle(rootObjects, titleArgument);
-		
-		logger.finer("CRUD request for root aggregations successful");
-		
-		final TEICorpusSerializer serializer = new TEICorpusSerializer(rootObjects, flat, sid);
-		serializer.setTitle(title);
-		final ResponseBuilder builder = Response.ok(serializer, "text/xml");
-		RESTUtils.configureCache(builder, lastModified, sid == null || sid.isEmpty());
-		if (attach) {
-			final String fileName = DefaultFilenamePolicy.INSTANCE.translate(title)
-					+ (flat ? ".flat" : "") + ".xml";
-			RESTUtils.addAttachmentFilename(builder, fileName);
-		}
-		return builder.build();
-
-		// final Response response = search.listAggregation(uri.toString());
-		// for (final ResultType result : response.getResult()) {
-		// final String contentType =
-		// result.getObject().getGeneric().getProvided().getFormat();
-		// if (contentType.contains("aggregation")) {
-		// // TODO deal with aggregation
-		// } else if ("text/xml".equals(contentType)) {
-		// // TODO deal with
-		// }
-		// }
-		// return null;
-	}
-
-}
diff --git a/src/main/java/info/textgrid/services/aggregator/teicorpus/TEICorpusExporter.java b/src/main/java/info/textgrid/services/aggregator/teicorpus/TEICorpusExporter.java
new file mode 100644
index 0000000000000000000000000000000000000000..5fb14ac6ff09e8e89751d196c788072ee135e609
--- /dev/null
+++ b/src/main/java/info/textgrid/services/aggregator/teicorpus/TEICorpusExporter.java
@@ -0,0 +1,55 @@
+package info.textgrid.services.aggregator.teicorpus;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Request;
+import javax.ws.rs.core.Response.Status;
+
+import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.AuthFault;
+import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.IoFault;
+import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.MetadataParseFault;
+import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.ObjectNotFoundFault;
+import info.textgrid.services.aggregator.AbstractExporter;
+import info.textgrid.services.aggregator.ITextGridRep;
+
+public class TEICorpusExporter extends AbstractExporter {
+	
+	private boolean flat;
+
+	public TEICorpusExporter(ITextGridRep repository, Request request,
+			String uriList) {
+		super(repository, request, uriList);
+		setFileExtension("xml");
+		setMediaType("application/tei+xml");
+	}
+
+	@Override
+	public void write(OutputStream output) throws IOException,
+			WebApplicationException {
+		TEICorpusSerializer serializer;
+		try {
+			serializer = new TEICorpusSerializer(getRootObjects(), isFlat(), getSid().orNull());
+			serializer.setTitle(getTitle());
+			serializer.write(output);
+		} catch (ObjectNotFoundFault e) {
+			throw new WebApplicationException(e, Status.NOT_FOUND);
+		} catch (MetadataParseFault e) {
+			throw new WebApplicationException(e, Status.BAD_REQUEST);
+		} catch (IoFault e) {
+			throw new WebApplicationException(e);
+		} catch (AuthFault e) {
+			throw new WebApplicationException(e, Status.FORBIDDEN);
+		}
+	}
+
+	public boolean isFlat() {
+		return flat;
+	}
+
+	public void setFlat(boolean flat) {
+		this.flat = flat;
+	}
+
+}
diff --git a/src/main/java/info/textgrid/services/aggregator/teicorpus/TEICorpusSerializer.java b/src/main/java/info/textgrid/services/aggregator/teicorpus/TEICorpusSerializer.java
index 7933bed9baba4ff964430625da6bd580365b3905..589240651e73ce0e9cebf7ad6b02c6dae6b7b0b9 100644
--- a/src/main/java/info/textgrid/services/aggregator/teicorpus/TEICorpusSerializer.java
+++ b/src/main/java/info/textgrid/services/aggregator/teicorpus/TEICorpusSerializer.java
@@ -8,10 +8,9 @@
 import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.MetadataParseFault;
 import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.ObjectNotFoundFault;
 import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.ProtocolNotImplementedFault;
-import info.textgrid.services.aggregator.AggregationTreeWalker;
-import info.textgrid.services.aggregator.TextGridRepProvider;
+import info.textgrid.services.aggregator.tree.AggregationTreeWalker;
+import info.textgrid.services.aggregator.util.TextGridRepProvider;
 import info.textgrid.utils.linkrewriter.ConfigurableXMLRewriter;
-import info.textgrid.utils.linkrewriter.ConfigurableXMLRewriter.DefaultMergeLinkAdjuster;
 
 import java.io.IOException;
 import java.io.OutputStream;
diff --git a/src/main/java/info/textgrid/services/aggregator/teicorpus/TEIHeaderStack.java b/src/main/java/info/textgrid/services/aggregator/teicorpus/TEIHeaderStack.java
index 58d221c637f5a605e8b2e43b01e8a2a950b83613..c1a0f89f7f7a4c4989888401942eb77a8f0e0bab 100644
--- a/src/main/java/info/textgrid/services/aggregator/teicorpus/TEIHeaderStack.java
+++ b/src/main/java/info/textgrid/services/aggregator/teicorpus/TEIHeaderStack.java
@@ -16,7 +16,7 @@
 import info.textgrid.namespaces.middleware.tgsearch.PathResponse;
 import info.textgrid.namespaces.middleware.tgsearch.PathType;
 import info.textgrid.services.aggregator.ITextGridRep;
-import info.textgrid.services.aggregator.TextGridRepProvider;
+import info.textgrid.services.aggregator.util.TextGridRepProvider;
 
 import java.io.InputStream;
 import java.util.List;
diff --git a/src/main/java/info/textgrid/services/aggregator/tree/AggregationTreeFactory.java b/src/main/java/info/textgrid/services/aggregator/tree/AggregationTreeFactory.java
index c3233f83b7d1343f310796f61c0556d9dd4b0aec..33a35476f25712d967821a639478024d4eddc36b 100644
--- a/src/main/java/info/textgrid/services/aggregator/tree/AggregationTreeFactory.java
+++ b/src/main/java/info/textgrid/services/aggregator/tree/AggregationTreeFactory.java
@@ -5,7 +5,6 @@
 import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.IoFault;
 import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.MetadataParseFault;
 import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.ObjectNotFoundFault;
-import info.textgrid.services.aggregator.AggregationTreeWalker;
 import info.textgrid.services.aggregator.ITextGridRep;
 import info.textgrid.utils.export.aggregations.Aggregation;
 import info.textgrid.utils.export.aggregations.AggregationEntry;
diff --git a/src/main/java/info/textgrid/services/aggregator/AggregationTreeWalker.java b/src/main/java/info/textgrid/services/aggregator/tree/AggregationTreeWalker.java
similarity index 98%
rename from src/main/java/info/textgrid/services/aggregator/AggregationTreeWalker.java
rename to src/main/java/info/textgrid/services/aggregator/tree/AggregationTreeWalker.java
index 90ed64d96cb3c939725d414ae2fd494ea5d16748..5d67a39818132028af96c7150b5a35bed9f7096c 100644
--- a/src/main/java/info/textgrid/services/aggregator/AggregationTreeWalker.java
+++ b/src/main/java/info/textgrid/services/aggregator/tree/AggregationTreeWalker.java
@@ -1,9 +1,10 @@
-package info.textgrid.services.aggregator;
+package info.textgrid.services.aggregator.tree;
 
 import info.textgrid.middleware.tgsearch.client.SearchClient;
 import info.textgrid.namespaces.metadata.core._2010.ObjectType;
 import info.textgrid.namespaces.middleware.tgsearch.Response;
 import info.textgrid.namespaces.middleware.tgsearch.ResultType;
+import info.textgrid.services.aggregator.ITextGridRep;
 
 import java.util.List;
 import java.util.Set;
diff --git a/src/main/java/info/textgrid/services/aggregator/util/StylesheetManager.java b/src/main/java/info/textgrid/services/aggregator/util/StylesheetManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..f25ae2fb739ffec65e98cb7dc6a72011ab87be84
--- /dev/null
+++ b/src/main/java/info/textgrid/services/aggregator/util/StylesheetManager.java
@@ -0,0 +1,270 @@
+package info.textgrid.services.aggregator.util;
+
+import info.textgrid.namespaces.metadata.core._2010.ObjectType;
+import info.textgrid.services.aggregator.ITextGridRep;
+import info.textgrid.services.aggregator.ITextGridRep.TGOSupplier;
+import info.textgrid.services.aggregator.html.TGUriResolver;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URL;
+import java.text.MessageFormat;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.servlet.ServletContext;
+import javax.xml.transform.stream.StreamSource;
+
+import net.sf.saxon.s9api.Processor;
+import net.sf.saxon.s9api.SaxonApiException;
+import net.sf.saxon.s9api.XsltCompiler;
+import net.sf.saxon.s9api.XsltExecutable;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Throwables;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.CacheStats;
+import com.google.common.cache.RemovalListener;
+import com.google.common.cache.RemovalNotification;
+import com.google.common.cache.Weigher;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+
+/**
+ * Manages and caches XSLT stylesheets across service calls, also maintains
+ * processor policies etc. All stylesheets invoked throughout the service should
+ * be invoked through the manager's
+ * {@link #getStylesheet(URI, Optional, boolean, boolean)} method.
+ */
+public class StylesheetManager {
+	private static final Logger logger = Logger
+			.getLogger(StylesheetManager.class.getCanonicalName());
+	public final Processor xsltProcessor;
+	private Cache<URI, XsltExecutable> stylesheets;
+	private ITextGridRep repository;
+	private ServletContext servlet;
+	private Set<URI> importantStylesheets = Sets.newHashSet();
+
+	/**
+	 * Initializes a stylesheet manager.
+	 *
+	 * @param servlet
+	 *            The servlet context, used for retrieving internal stylesheets.
+	 * @param repository
+	 *            The TextGridRep instance, used for retrieving textgrid
+	 *            stylesheets.
+	 */
+	public StylesheetManager(final ServletContext servlet,
+			final ITextGridRep repository) {
+		this.servlet = servlet;
+		this.repository = repository;
+
+		xsltProcessor = new Processor(false);
+		xsltProcessor.getUnderlyingConfiguration().setURIResolver(
+				new TGUriResolver(repository));
+		// xsltProcessor.getUnderlyingConfiguration().setAllowExternalFunctions(
+		// false); // we run external stylesheets
+		xsltProcessor.getUnderlyingConfiguration().setCompileWithTracing(true);
+		stylesheets = CacheBuilder.newBuilder().recordStats()
+				.maximumWeight(200).weigher(new Weigher<URI, XsltExecutable>() {
+					@Override
+					public int weigh(final URI key, final XsltExecutable value) {
+						return importantStylesheets.contains(key) ? 1 : 10;
+					}
+				}).removalListener(new RemovalListener<URI, XsltExecutable>() {
+					@Override
+					public void onRemoval(
+							final RemovalNotification<URI, XsltExecutable> notification) {
+						logger.info(MessageFormat
+								.format("Removed stylesheet {0} from cache, reason: {1}",
+										notification.getKey(),
+										notification.getCause()));
+						logger.info(stylesheets.stats().toString());
+					}
+				}).build(new CacheLoader<URI, XsltExecutable>() {
+
+					@Override
+					public XsltExecutable load(final URI url) throws Exception {
+						final XsltCompiler compiler = xsltProcessor
+								.newXsltCompiler();
+						try {
+							final XsltExecutable executable = compiler
+									.compile(new StreamSource(url.toString()));
+							logger.log(Level.INFO,
+									"Successfully loaded stylesheet {0}", url);
+							return executable;
+						} catch (final Exception e) {
+							logger.log(Level.SEVERE, MessageFormat.format(
+									"Failed to load stylesheet {0}", url), e);
+							throw e;
+						}
+					}
+				});
+
+	}
+
+	/**
+	 * Returns an appropriate stylesheet for the given URI.
+	 *
+	 * Basically, we try the following options in order:
+	 * <ol>
+	 * <li>The stylesheet is cached -> return the cached version.
+	 * <li>The stylesheet is public or external -> load & cache it.
+	 * <li>The stylesheet is non-public TextGrid internal -> load & do not cache
+	 * it.
+	 * </ol>
+	 *
+	 * @param uri
+	 *            the URI of the stylesheet to load
+	 * @param sid
+	 *            the session ID to use, if present
+	 * @param forceLoad
+	 *            do not use a cached version even if present.
+	 * @param frequentlyUsed
+	 *            if <code>true</code>, this stylesheet will be less likely
+	 *            evicted.
+	 * @throws IOException
+	 *             if an error occurs reading the stylesheet.
+	 * @throws SaxonApiException
+	 *             if saxon fails to compile the stylesheet.
+	 */
+	public XsltExecutable getStylesheet(final URI uri,
+			final Optional<String> sid, final boolean forceLoad,
+			final boolean frequentlyUsed) throws SaxonApiException, IOException {
+
+		if (frequentlyUsed)
+			importantStylesheets.add(uri);
+
+		try {
+			if (forceLoad)
+				stylesheets.invalidate(uri);
+
+			return stylesheets.get(uri, new StylesheetLoader(uri, sid));
+		} catch (final ExecutionException e) {
+			final Throwable cause = e.getCause();
+			if (cause instanceof PrivateResourceException)
+				return ((PrivateResourceException) cause).getXsltExecutable();
+			else {
+				Throwables.propagateIfPossible(cause, SaxonApiException.class,
+						IOException.class);
+				return null; // will never be reached
+			}
+
+		}
+
+	}
+
+	private final class StylesheetLoader implements Callable<XsltExecutable> {
+		private final URI uri;
+		private final Optional<String> sid;
+
+		private StylesheetLoader(final URI uri, final Optional<String> sid) {
+			this.uri = uri;
+			this.sid = sid;
+		}
+
+		@Override
+		public XsltExecutable call() throws Exception {
+			final XsltCompiler compiler = xsltProcessor.newXsltCompiler();
+			final XsltExecutable executable;
+			if (uri.getScheme() == null || uri.getScheme() == "file") {
+				// (1) Internal
+				final URL resource = resolveInternalPath(uri.getPath());
+				executable = compiler.compile(new StreamSource(resource
+						.openStream(), resource.toExternalForm()));
+				logger.log(Level.INFO, "Cached internal stylesheet {0}",
+						resource);
+				return executable;
+			} else if (TGUriResolver.isResolveable(uri)) {
+
+				// (3/4) it's a TextGrid object, load it from TG-crud.
+				final TGOSupplier<InputStream> xsltSupplier = repository.read(
+						uri, sid.orNull());
+				executable = compiler.compile(new StreamSource(xsltSupplier
+						.getInput(), uri.toString()));
+
+				if (isPublic(xsltSupplier.getMetadata())) {
+					// (3) it's public -> we can cache it.
+					logger.log(Level.INFO, "Cached public stylesheet {0}", uri);
+					return executable;
+				} else {
+					logger.log(Level.INFO, "Loaded private stylesheet {0}", uri);
+					throw new PrivateResourceException(executable);
+				}
+			} else {
+				// (2) it's non-TextGrid -- load & cache it.
+				executable = compiler.compile(new StreamSource(uri.toString()));
+				logger.log(Level.INFO, "Cached external stylesheet {0}", uri);
+				return executable;
+			}
+		}
+	}
+
+	private static class PrivateResourceException extends Exception {
+
+		private static final long serialVersionUID = -3506322226718139009L;
+		private XsltExecutable xsltExecutable;
+
+		public XsltExecutable getXsltExecutable() {
+			return xsltExecutable;
+		}
+
+		public PrivateResourceException(final XsltExecutable xsltExecutable) {
+			super("The source object is private and will not be cached.");
+			this.xsltExecutable = xsltExecutable;
+		}
+	}
+
+
+	public URL resolveInternalPath(final String path)
+			throws MalformedURLException {
+		URL stylesheet;
+		if (servlet == null) {
+			logger.info("No servlet context, trying fallback property");
+			final String dir = System.getProperty("webapp.directory");
+			if (dir == null)
+				throw new IllegalStateException(
+						"Could not find stylesheet: Neither ServletContext nor fallback property webapp.directory have been defined.");
+			stylesheet = new URL("file://" + dir + path);
+		} else {
+			stylesheet = servlet.getResource(path);
+		}
+		logger.fine("Resolved internal stylesheet: "
+				+ stylesheet.toExternalForm());
+		return stylesheet;
+	}
+
+	private static boolean isPublic(final ObjectType metadata) {
+		try {
+			return metadata.getGeneric().getGenerated().getAvailability()
+					.contains("public");
+		} catch (final NullPointerException e) {
+			return false;
+		}
+	}
+
+	@Override
+	protected void finalize() throws Throwable {
+		if (stylesheets != null)
+			logger.log(Level.INFO,
+					"Shutting down stylesheet manager. Stats: {0}",
+					stylesheets.stats());
+		super.finalize();
+	}
+
+	public CacheStats stats() {
+		return stylesheets.stats();
+	}
+	
+	public Set<URI> knownStylesheets() {
+		return ImmutableSet.copyOf(stylesheets.asMap().keySet());
+	}
+
+}
\ No newline at end of file
diff --git a/src/main/java/info/textgrid/services/aggregator/TextGridRepProvider.java b/src/main/java/info/textgrid/services/aggregator/util/TextGridRepProvider.java
similarity index 95%
rename from src/main/java/info/textgrid/services/aggregator/TextGridRepProvider.java
rename to src/main/java/info/textgrid/services/aggregator/util/TextGridRepProvider.java
index d889dcc639e5388c42b7744cb68e4e5ca4a5146e..f82c0b866be83aca7e2c0c7f54375f1977479ee8 100644
--- a/src/main/java/info/textgrid/services/aggregator/TextGridRepProvider.java
+++ b/src/main/java/info/textgrid/services/aggregator/util/TextGridRepProvider.java
@@ -1,4 +1,4 @@
-package info.textgrid.services.aggregator;
+package info.textgrid.services.aggregator.util;
 
 import info.textgrid.middleware.confclient.ConfservClient;
 import info.textgrid.middleware.confclient.ConfservClientConstants;
@@ -11,14 +11,14 @@
 import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.ObjectNotFoundFault;
 import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.ProtocolNotImplementedFault;
 import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.TGCrudService;
-import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.TGCrudService_Service;
 import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.tgcrudclient.TGCrudClientUtilities;
+import info.textgrid.services.aggregator.ITextGridRep;
+import info.textgrid.services.aggregator.ITextGridRep.TGOSupplier;
 
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.MalformedURLException;
 import java.net.URI;
-import java.net.URL;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -26,9 +26,7 @@
 import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.core.Response.Status;
 import javax.xml.stream.XMLStreamException;
-import javax.xml.ws.BindingProvider;
 import javax.xml.ws.Holder;
-import javax.xml.ws.soap.MTOMFeature;
 
 import org.codehaus.jettison.json.JSONException;
 
diff --git a/src/main/java/info/textgrid/services/aggregator/XSLTErrorListener.java b/src/main/java/info/textgrid/services/aggregator/util/XSLTErrorListener.java
similarity index 95%
rename from src/main/java/info/textgrid/services/aggregator/XSLTErrorListener.java
rename to src/main/java/info/textgrid/services/aggregator/util/XSLTErrorListener.java
index 97b686823cd52e7936045239d2ad377131a5f9c6..eed9ac836e13536b7909fef08b58a2187b1b6714 100644
--- a/src/main/java/info/textgrid/services/aggregator/XSLTErrorListener.java
+++ b/src/main/java/info/textgrid/services/aggregator/util/XSLTErrorListener.java
@@ -1,4 +1,4 @@
-package info.textgrid.services.aggregator;
+package info.textgrid.services.aggregator.util;
 
 import java.util.logging.Level;
 import java.util.logging.Logger;
diff --git a/src/main/java/info/textgrid/services/aggregator/zip/ZIP.java b/src/main/java/info/textgrid/services/aggregator/zip/ZIP.java
deleted file mode 100644
index bb97193e8452268ed65b7f9a78615b44af46cde7..0000000000000000000000000000000000000000
--- a/src/main/java/info/textgrid/services/aggregator/zip/ZIP.java
+++ /dev/null
@@ -1,90 +0,0 @@
-package info.textgrid.services.aggregator.zip;
-
-import info.textgrid.namespaces.metadata.core._2010.ObjectType;
-import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.AuthFault;
-import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.IoFault;
-import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.MetadataParseFault;
-import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.ObjectNotFoundFault;
-import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.TGCrudService;
-import info.textgrid.services.aggregator.ArgUtils;
-import info.textgrid.services.aggregator.ITextGridRep;
-import info.textgrid.services.aggregator.RESTUtils;
-import info.textgrid.utils.export.filenames.DefaultFilenamePolicy;
-
-import java.util.Date;
-import java.util.logging.Logger;
-
-import javax.servlet.ServletContext;
-import javax.ws.rs.GET;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.Request;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.Response.ResponseBuilder;
-
-import org.apache.cxf.jaxrs.model.wadl.Description;
-
-@Path("/zip")
-public class ZIP {
-	private final ITextGridRep repository;
-	final Logger logger = Logger.getLogger(ZIP.class.getCanonicalName());
-
-	@Context
-	private ServletContext servlet;
-
-	public ZIP(final ITextGridRep repository) {
-		this.repository = repository;
-	}
-
-	@GET
-	@Path(value = "/{objects}")
-	@Produces("application/zip")
-	public Response get(
-			@Description("The TextGridURIs of the TEI documents or aggregations to zip, separated by commas (,)")
-			@PathParam("objects") final String uriList,
-			@Description("Session ID to access protected resources")
-			@QueryParam("sid") final String sid,
-			@Description("(optional) title for the exported data, currently only used for generating the filename. If none is given, the first title of the first object will be used.")
-			@QueryParam("title") String title,
-			@Context final Request request) throws MetadataParseFault, ObjectNotFoundFault, IoFault, AuthFault {
-
-		final TGCrudService crud = repository.getCRUDService();
-
-		if (uriList == null)
-			throw new IllegalArgumentException("Specify at least one URI to zip");
-
-		final ObjectType[] objects = ArgUtils.extractRootObjects(uriList, sid, crud);
-		final Date lastModified = RESTUtils.createLastModified(objects);
-		if (request != null) {
-			ResponseBuilder builder = request.evaluatePreconditions(lastModified);
-			if (builder != null)
-				return builder.build();
-		}
-
-		if (title == null) {
-			title = objects[0].getGeneric().getProvided().getTitle().get(0);
-		}
-
-		ResponseBuilder builder = RESTUtils
-				.attachmentResponse(DefaultFilenamePolicy.INSTANCE.translate(title) + ".zip")
-				.type("application/zip");
-		RESTUtils.configureCache(builder, lastModified, sid != null);
-		return builder
-				.entity(new ZipResult(repository, sid, objects))
-				.build();
-	}
-
-//	private void appendLS(final Aggregation aggregation, final StringBuilder output) {
-//		output.append(aggregation.getFileName(true)).append(":\n");
-//		for(final IAggregationEntry child : aggregation.getChildren())
-//			if (child instanceof Aggregation) {
-//				appendLS((Aggregation) child, output);
-//			} else {
-//				output.append(' ').append(child.getFileName(true)).append('\n');
-//			}
-//	}
-
-}
diff --git a/src/main/java/info/textgrid/services/aggregator/zip/ZipResult.java b/src/main/java/info/textgrid/services/aggregator/zip/ZipResult.java
index 1bdbae5a4ac4d67e9922438c5ad55d44c2c53128..022c4634849dbb09f6a4095577bcc6c94be5b468 100644
--- a/src/main/java/info/textgrid/services/aggregator/zip/ZipResult.java
+++ b/src/main/java/info/textgrid/services/aggregator/zip/ZipResult.java
@@ -3,6 +3,11 @@
 import info.textgrid._import.ImportObject;
 import info.textgrid._import.RewriteMethod;
 import info.textgrid.namespaces.metadata.core._2010.ObjectType;
+import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.AuthFault;
+import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.IoFault;
+import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.MetadataParseFault;
+import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.ObjectNotFoundFault;
+import info.textgrid.services.aggregator.AbstractExporter;
 import info.textgrid.services.aggregator.ITextGridRep;
 import info.textgrid.services.aggregator.tree.AggregationTreeFactory;
 import info.textgrid.utils.export.aggregations.AggregationEntry;
@@ -27,6 +32,8 @@
 import java.util.zip.ZipOutputStream;
 
 import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Request;
+import javax.ws.rs.core.Response.Status;
 import javax.ws.rs.core.StreamingOutput;
 import javax.xml.bind.JAXB;
 import javax.xml.stream.XMLStreamException;
@@ -42,110 +49,136 @@
 import com.google.common.io.ByteStreams;
 import com.google.common.io.FileBackedOutputStream;
 
-public class ZipResult implements StreamingOutput {
+public class ZipResult extends AbstractExporter implements StreamingOutput {
 
-	private final ObjectType[] rootObjects;
-	private final ITextGridRep repository;
-	private final String sid;
 	private final IFilenamePolicy policy;
 	private final IFilenamePolicy metaPolicy;
 	private ImportMapping mapping;
-	private final static Logger logger = Logger.getLogger(ZipResult.class.getCanonicalName());
-	
-	private final Set<URI> written = Sets.newHashSet();
+	private final static Logger logger = Logger.getLogger(ZipResult.class
+			.getCanonicalName());
 
+	private final Set<URI> written = Sets.newHashSet();
 
 	private static final Function<ObjectType, String> GetURI = new Function<ObjectType, String>() {
 
 		@Override
 		public String apply(final ObjectType input) {
-				return input.getGeneric().getGenerated().getTextgridUri().getValue();
+			return input.getGeneric().getGenerated().getTextgridUri()
+					.getValue();
 		}
 
 	};
 
-
-	public ZipResult(final ITextGridRep repository, final String sid, final ObjectType... rootObjects) {
-		this.rootObjects = rootObjects;
-		this.repository = repository;
-		this.sid = sid;
+	public ZipResult(final ITextGridRep repository, final Request request,
+			final String uriList) {
+		super(repository, request, uriList);
 		this.policy = new DefaultFilenamePolicy();
 		this.metaPolicy = new DefaultMetaFilenamePolicy(policy);
+		setFileExtension("zip");
+		setMediaType("application/zip");
 	}
 
 	@Override
 	public void write(final OutputStream output) throws IOException,
 			WebApplicationException {
-		final long startTime = System.currentTimeMillis();
-		final String uriList = Joiner.on(", ").join(Iterators.transform(Iterators.forArray(rootObjects), GetURI));
-		logger.log(Level.INFO, "Starting ZIP export of {0}", uriList);
 		final ZipOutputStream zip = new ZipOutputStream(output);
-		zip.setComment(MessageFormat.format("# Exported from TextGrid -- www.textgrid.de\nRoot objects: {0}", uriList));
+		try {
+			ObjectType[] rootObjects = getRootObjects();
+			final String uriList = Joiner.on(", ")
+					.join(Iterators.transform(Iterators.forArray(rootObjects),
+							GetURI));
+			logger.log(Level.INFO, MessageFormat.format(
+					"Starting ZIP export of {0} after {1}", uriList, stopwatch));
+			zip.setComment(MessageFormat
+					.format("# Exported from TextGrid -- www.textgrid.de\nRoot objects: {0}",
+							uriList));
 
-		mapping = new ImportMapping();
+			mapping = new ImportMapping();
 
-		final List<IAggregationEntry> roots = Lists.newArrayListWithCapacity(rootObjects.length);
-		for (final ObjectType rootMetadata : rootObjects) {
-			final IAggregationEntry entry;
-			if (rootMetadata.getGeneric().getProvided().getFormat().contains("aggregation")) {
-				entry = AggregationTreeFactory.create(rootMetadata, repository, sid);
-			} else {
-				entry = new AggregationEntry(rootMetadata, null);
+			final List<IAggregationEntry> roots = Lists
+					.newArrayListWithCapacity(rootObjects.length);
+			for (final ObjectType rootMetadata : rootObjects) {
+				final IAggregationEntry entry;
+				if (rootMetadata.getGeneric().getProvided().getFormat()
+						.contains("aggregation")) {
+					entry = AggregationTreeFactory.create(rootMetadata,
+							repository, getSid().orNull());
+				} else {
+					entry = new AggregationEntry(rootMetadata, null);
+				}
+				logger.log(Level.INFO, MessageFormat.format(
+						"  Built aggregation tree for {1} after {0}",
+						stopwatch, GetURI.apply(rootMetadata)));
+				roots.add(entry);
+				addToMapping(mapping, entry);
 			}
-			logger.log(Level.INFO, MessageFormat.format("  Built aggregation tree for {1} after {0,number} ms",
-					System.currentTimeMillis() - startTime, GetURI.apply(rootMetadata)));
-			roots.add(entry);
-			addToMapping(mapping, entry);
-		}
-
-		for (final IAggregationEntry root : roots) {
-			if (root instanceof IAggregation)
-				writeAggregation(zip, (IAggregation) root);
-			else
-				writeFile(zip, root);
-			logger.log(Level.INFO, MessageFormat.format("  Zipped {1} after {0,number} ms",
-					System.currentTimeMillis() - startTime, root.getTextGridURI()));
-		}
 
+			for (final IAggregationEntry root : roots) {
+				if (root instanceof IAggregation)
+					writeAggregation(zip, (IAggregation) root);
+				else
+					writeFile(zip, root);
+				logger.log(Level.INFO, MessageFormat.format(
+						"  Zipped {1} after {0}", stopwatch,
+						root.getTextGridURI()));
+			}
 
-		// now serializing the mapping
-		zip.putNextEntry(new ZipEntry(".INDEX.imex"));
-		JAXB.marshal(mapping.toImportSpec(), zip);
-		zip.closeEntry();
-		zip.close();
-		logger.log(Level.INFO, MessageFormat.format(
-				"Finished exporting after {0,number} ms",
-				System.currentTimeMillis() - startTime));
+			// now serializing the mapping
+			zip.putNextEntry(new ZipEntry(".INDEX.imex"));
+			JAXB.marshal(mapping.toImportSpec(), zip);
+			zip.closeEntry();
+		} catch (MetadataParseFault e) {
+			throw new WebApplicationException(e);
+		} catch (ObjectNotFoundFault e) {
+			throw new WebApplicationException(e, Status.NOT_FOUND);
+		} catch (IoFault e) {
+			throw new WebApplicationException(e);
+		} catch (AuthFault e) {
+			throw new WebApplicationException(e, Status.FORBIDDEN);
+		} finally {
+			zip.close();
+			stopwatch.stop();
+		}
+		logger.log(Level.INFO,
+				MessageFormat.format("Finished exporting after {0}", stopwatch));
 	}
 
 	// FIXME refactor -> Rewrite library
 	private static ImmutableMap<String, String> REWRITE_CONFIGS = null;
+
 	private static Optional<String> getRewriteConfig(final String contentType) {
 		if (REWRITE_CONFIGS == null) {
-			REWRITE_CONFIGS = ImmutableMap.<String, String>builder()
-					.put("text/tg.aggregation+xml", "internal:textgrid#aggregation")
-					.put("text/tg.edition+tg.aggregation+xml", "internal:textgrid#aggregation")
-					.put("text/tg.collection+tg.aggregation+xml", "internal:textgrid#aggregation")
+			REWRITE_CONFIGS = ImmutableMap
+					.<String, String> builder()
+					.put("text/tg.aggregation+xml",
+							"internal:textgrid#aggregation")
+					.put("text/tg.edition+tg.aggregation+xml",
+							"internal:textgrid#aggregation")
+					.put("text/tg.collection+tg.aggregation+xml",
+							"internal:textgrid#aggregation")
 					.put("text/xsd+xml", "internal:schema#xsd")
 					.put("text/linkeditorlinkedfile", "internal:tei#tei")
 					.put("text/xml", "internal:tei#tei")
-					.put("application/xhtml+xml", "internal:html#html")
-					.build();
+					.put("application/xhtml+xml", "internal:html#html").build();
 		}
 		return Optional.fromNullable(REWRITE_CONFIGS.get(contentType));
 	}
 
 	/**
 	 * Recursively adds the entry to the mapping.
+	 * 
 	 * @param mapping
 	 * @param entry
 	 */
-	private void addToMapping(final ImportMapping mapping, final IAggregationEntry entry) {
-		final Optional<String> rewriteConfig = getRewriteConfig(((AggregationEntry) entry).getFormat());
+	private void addToMapping(final ImportMapping mapping,
+			final IAggregationEntry entry) {
+		final Optional<String> rewriteConfig = getRewriteConfig(((AggregationEntry) entry)
+				.getFormat());
 		final ImportObject importObject = new ImportObject();
 		importObject.setTextgridUri(entry.getTextGridURI().toString());
 		importObject.setLocalData(policy.getFilename(entry, false).toString());
-		importObject.setLocalMetadata(metaPolicy.getFilename(entry, false).toString());
+		importObject.setLocalMetadata(metaPolicy.getFilename(entry, false)
+				.toString());
 		if (rewriteConfig.isPresent()) {
 			importObject.setRewriteMethod(RewriteMethod.XML);
 			importObject.setRewriteConfig(rewriteConfig.get());
@@ -162,16 +195,19 @@ private void addToMapping(final ImportMapping mapping, final IAggregationEntry e
 		}
 	}
 
-	private void writeAggregation(final ZipOutputStream zip, final IAggregation root) throws IOException {
+	private void writeAggregation(final ZipOutputStream zip,
+			final IAggregation root) throws IOException {
 		final URI uri = root.getTextGridURI();
 		if (written.contains(uri)) {
 			logger.log(Level.WARNING, "Skipping duplicate aggregation {0}", uri);
 			return;
 		}
 		writeFile(zip, root);
-		final ZipEntry zipEntry = new ZipEntry(policy.getFilename(root, true).toString());
+		final ZipEntry zipEntry = new ZipEntry(policy.getFilename(root, true)
+				.toString());
 		zip.putNextEntry(zipEntry);
-		zipEntry.setTime(root.getMetadata().getGeneric().getGenerated().getLastModified().toGregorianCalendar().getTimeInMillis());
+		zipEntry.setTime(root.getMetadata().getGeneric().getGenerated()
+				.getLastModified().toGregorianCalendar().getTimeInMillis());
 		zip.closeEntry();
 
 		for (final IAggregationEntry child : root.getChildren()) {
@@ -183,7 +219,8 @@ private void writeAggregation(final ZipOutputStream zip, final IAggregation root
 		}
 	}
 
-	private void writeFile(final ZipOutputStream zip, final IAggregationEntry child) throws IOException {
+	private void writeFile(final ZipOutputStream zip,
+			final IAggregationEntry child) throws IOException {
 		URI uri = child.getTextGridURI();
 		if (written.contains(uri)) {
 			logger.log(Level.WARNING, "Skipping duplicate object {0}", uri);
@@ -191,34 +228,42 @@ private void writeFile(final ZipOutputStream zip, final IAggregationEntry child)
 		}
 		written.add(uri);
 		writeMetadata(zip, child);
-		final ZipEntry zipEntry = new ZipEntry(policy.getFilename(child, false).toString());
-		zipEntry.setTime(child.getMetadata().getGeneric().getGenerated().getLastModified().toGregorianCalendar().getTimeInMillis());
+		final ZipEntry zipEntry = new ZipEntry(policy.getFilename(child, false)
+				.toString());
+		zipEntry.setTime(child.getMetadata().getGeneric().getGenerated()
+				.getLastModified().toGregorianCalendar().getTimeInMillis());
 
-		final ImportObject importObject = mapping.getImportObjectForTextGridURI(child.getTextGridURI().toString());
+		final ImportObject importObject = mapping
+				.getImportObjectForTextGridURI(child.getTextGridURI()
+						.toString());
 		try {
-			final InputStream content = repository.getContent(child.getTextGridURI(), sid);
+			final InputStream content = repository.getContent(
+					child.getTextGridURI(), getSid().orNull());
 			if (importObject.getRewriteMethod().equals(RewriteMethod.XML)) {
-				final ConfigurableXMLRewriter rewriter = new ConfigurableXMLRewriter(mapping, true);
+				final ConfigurableXMLRewriter rewriter = new ConfigurableXMLRewriter(
+						mapping, true);
 				rewriter.configure(URI.create(importObject.getRewriteConfig()));
 				final Optional<URI> base = policy.getBase(child);
 				if (base.isPresent()) {
 					rewriter.setBase(base.get());
 				}
-				final FileBackedOutputStream buffer = new FileBackedOutputStream(1024 * 1024);
+				final FileBackedOutputStream buffer = new FileBackedOutputStream(
+						1024 * 1024);
 				try {
 					rewriter.rewrite(content, buffer);
 					zip.putNextEntry(zipEntry);
 					ByteStreams.copy(buffer.getSupplier(), zip);
 				} catch (final XMLStreamException e) {
-					final String errorMsg = MessageFormat.format("Failed to rewrite {0} (error: {1}). Exported with verbatim links instead.", child, e.getMessage());
+					final String errorMsg = MessageFormat
+							.format("Failed to rewrite {0} (error: {1}). Exported with verbatim links instead.",
+									child, e.getMessage());
 					logger.log(Level.WARNING, errorMsg, e);
 					zipEntry.setComment(errorMsg);
 					importObject.setRewriteMethod(RewriteMethod.NONE);
 					zip.putNextEntry(zipEntry);
 					ByteStreams.copy(buffer.getSupplier(), zip);
 				}
-			}
-			else {
+			} else {
 				zip.putNextEntry(zipEntry);
 				ByteStreams.copy(content, zip);
 			}
@@ -229,23 +274,30 @@ private void writeFile(final ZipOutputStream zip, final IAggregationEntry child)
 		}
 	}
 
-	private void writeMetadata(final ZipOutputStream zip, final IAggregationEntry child) throws IOException {
-		final ZipEntry zipEntry = new ZipEntry(metaPolicy.getFilename(child, false).toString());
-		zipEntry.setTime(child.getMetadata().getGeneric().getGenerated().getLastModified().toGregorianCalendar().getTimeInMillis());
+	private void writeMetadata(final ZipOutputStream zip,
+			final IAggregationEntry child) throws IOException {
+		final ZipEntry zipEntry = new ZipEntry(metaPolicy.getFilename(child,
+				false).toString());
+		zipEntry.setTime(child.getMetadata().getGeneric().getGenerated()
+				.getLastModified().toGregorianCalendar().getTimeInMillis());
 		zip.putNextEntry(zipEntry);
-		final ConfigurableXMLRewriter rewriter = new ConfigurableXMLRewriter(mapping, true);
+		final ConfigurableXMLRewriter rewriter = new ConfigurableXMLRewriter(
+				mapping, true);
 		rewriter.configure(URI.create("internal:textgrid#metadata"));
 		final Optional<URI> base = metaPolicy.getBase(child);
 		if (base.isPresent()) {
 			rewriter.setBase(base.get());
 		}
 
-		final FileBackedOutputStream buffer = new FileBackedOutputStream(1024*1024);
+		final FileBackedOutputStream buffer = new FileBackedOutputStream(
+				1024 * 1024);
 		JAXB.marshal(child.getMetadata(), buffer);
 		try {
 			rewriter.rewrite(buffer.getSupplier().getInput(), zip);
 		} catch (final XMLStreamException e) {
-			logger.log(Level.SEVERE, MessageFormat.format("Error rewriting the metadata of {0}. Should not happen.", child), e);
+			logger.log(Level.SEVERE, MessageFormat.format(
+					"Error rewriting the metadata of {0}. Should not happen.",
+					child), e);
 		}
 		zip.closeEntry();
 	}
diff --git a/src/main/webapp/WEB-INF/beans.xml b/src/main/webapp/WEB-INF/beans.xml
index 482504acd8d4480ccb9786727d2fe3aa625b05c1..d1b3695cd3453d66fcea065d347e1775d39df6f7 100644
--- a/src/main/webapp/WEB-INF/beans.xml
+++ b/src/main/webapp/WEB-INF/beans.xml
@@ -18,74 +18,40 @@ http://cxf.apache.org/schemas/jaxrs.xsd">
   <bean class="org.springframework.web.context.support.ServletContextPropertyPlaceholderConfigurer"/>
   <bean class="org.springframework.beans.factory.config.PreferencesPlaceholderConfigurer"/>
   
-  <bean id="stable-repo" class="info.textgrid.services.aggregator.TextGridRepProvider">
+  <bean id="stable-repo" class="info.textgrid.services.aggregator.util.TextGridRepProvider">
       <property name="CONF_ENDPOINT" value="${aggregator.textgridrep.default}"/>
   </bean>
-  <bean id="dev-repo" class="info.textgrid.services.aggregator.TextGridRepProvider">
+  <bean id="dev-repo" class="info.textgrid.services.aggregator.util.TextGridRepProvider">
       <property name="CONF_ENDPOINT" value="${aggregator.textgridrep.dev}"/>
   </bean>
 
 
-   <jaxrs:server id="services" address="/" publishedEndpointUrl="${aggregator.endpoint.published}">
-    <jaxrs:serviceBeans>
-      <!-- bean class="info.textgrid.services.aggregator.HelloWorld" /-->
-      <bean class="info.textgrid.services.aggregator.teicorpus.TEICorpus" scope="singleton">
-      	<constructor-arg ref="stable-repo"/>
-      </bean>
-      <bean class="info.textgrid.services.aggregator.epub.EPUB" scope="singleton">
-      	<constructor-arg ref="stable-repo"/>
-      </bean>
-      <bean class="info.textgrid.services.aggregator.pdf.PDF" scope="singleton">
-      	<constructor-arg ref="stable-repo"/>
-      </bean>
-      <bean class="info.textgrid.services.aggregator.html.HTML" scope="singleton">
-      	<constructor-arg ref="stable-repo"/>
-      </bean>
-      <bean class="info.textgrid.services.aggregator.zip.ZIP" scope="singleton">
-      	<constructor-arg ref="stable-repo"/>
-      </bean>
-      <bean class="info.textgrid.services.aggregator.Version" scope="singleton">
-      	<constructor-arg ref="stable-repo"/>
-      	<constructor-arg name="version" value="${project.version}" />
-      </bean>
-    </jaxrs:serviceBeans>
-    <jaxrs:providers>
-        <!-- bean class="org.codehaus.jackson.jaxrs.JacksonJsonProvider"/-->
-        <bean class="info.textgrid.services.aggregator.GenericExceptionMapper"/>
-        <!--bean class="org.apache.cxf.jaxrs.impl.WebApplicationExceptionMapper"/-->        
-    </jaxrs:providers>
-    </jaxrs:server>
-    
-    <jaxrs:server id="services-dev" address="/dev" publishedEndpointUrl="${aggregator.endpoint.published}/dev">
-	<jaxrs:serviceBeans>
-		<!-- bean class="info.textgrid.services.aggregator.HelloWorld" / -->
-		<bean class="info.textgrid.services.aggregator.teicorpus.TEICorpus" scope="singleton">
-			<constructor-arg ref="dev-repo" />
-		</bean>
-		<bean class="info.textgrid.services.aggregator.epub.EPUB" scope="singleton">
-			<constructor-arg ref="dev-repo" />
-		</bean>
-		<bean class="info.textgrid.services.aggregator.pdf.PDF" scope="singleton">
-			<constructor-arg ref="dev-repo" />
-		</bean>
-		<bean class="info.textgrid.services.aggregator.html.HTML" scope="singleton">
-			<constructor-arg ref="dev-repo" />
-		</bean>
-		<bean class="info.textgrid.services.aggregator.zip.ZIP" scope="singleton">
-			<constructor-arg ref="dev-repo" />
-		</bean>
-	      <bean class="info.textgrid.services.aggregator.Version" scope="singleton">
-	      	<constructor-arg ref="dev-repo"/>
-	      	<constructor-arg name="version" value="${project.version}" />
-	      </bean>
-	</jaxrs:serviceBeans>
-	<jaxrs:providers>
-		<!-- bean class="org.codehaus.jackson.jaxrs.JacksonJsonProvider"/ -->
-		<!-- bean class="org.apache.cxf.jaxrs.impl.WebApplicationExceptionMapper" /-->
-        <bean class="info.textgrid.services.aggregator.GenericExceptionMapper"/>
-	</jaxrs:providers>
+	<jaxrs:server id="services" address="/"
+		publishedEndpointUrl="${aggregator.endpoint.published}">
+		<jaxrs:serviceBeans>
+			<bean class="info.textgrid.services.aggregator.REST" scope="singleton">
+				<constructor-arg ref="stable-repo" />
+				<constructor-arg name="version" value="${project.version}" />
+			</bean>
+		</jaxrs:serviceBeans>
+		<jaxrs:providers>
+			<bean class="info.textgrid.services.aggregator.GenericExceptionMapper" />
+		</jaxrs:providers>
+	</jaxrs:server>
+
+	<jaxrs:server id="services-dev" address="/dev"
+		publishedEndpointUrl="${aggregator.endpoint.published}/dev">
+		<jaxrs:serviceBeans>
+			<bean class="info.textgrid.services.aggregator.REST" scope="singleton">
+				<constructor-arg ref="stable-repo" />
+				<constructor-arg name="version" value="${project.version}" />
+			</bean>
+		</jaxrs:serviceBeans>
+		<jaxrs:providers>
+			<bean class="info.textgrid.services.aggregator.GenericExceptionMapper" />
+		</jaxrs:providers>
 
-</jaxrs:server>
+	</jaxrs:server>
     
 
 </beans>
diff --git a/src/test/java/info/textgrid/services/aggregator/ArgUtilsTest.java b/src/test/java/info/textgrid/services/aggregator/ArgUtilsTest.java
deleted file mode 100644
index 0a21dee82a61c5701c6f1606b705056455398a1f..0000000000000000000000000000000000000000
--- a/src/test/java/info/textgrid/services/aggregator/ArgUtilsTest.java
+++ /dev/null
@@ -1,66 +0,0 @@
-package info.textgrid.services.aggregator;
-
-import static org.junit.Assert.*;
-import info.textgrid.namespaces.metadata.core._2010.MetadataContainerType;
-import info.textgrid.namespaces.metadata.core._2010.ObjectType;
-
-import java.io.InputStream;
-import java.util.Date;
-
-import javax.xml.bind.JAXB;
-
-import org.junit.Before;
-import org.junit.Test;
-
-public class ArgUtilsTest {
-	
-	private ObjectType metadata0;
-	private ObjectType metadata1;
-	private ObjectType[] metadata;
-
-	@Before
-	public void setup() {
-		InputStream stream0 = getClass().getClassLoader().getResourceAsStream("kscz.0.meta");
-		metadata0 = JAXB.unmarshal(stream0, MetadataContainerType.class).getObject();
-		InputStream stream1 = getClass().getClassLoader().getResourceAsStream("ksd3.0.meta");
-		metadata1 = JAXB.unmarshal(stream1, MetadataContainerType.class).getObject();
-		metadata = new ObjectType[] { metadata0, metadata1 };
-	}
-
-//  Requires CRUD, ie IT
-//	@Test
-//	public void testExtractRootObjects() {
-//		fail("Not yet implemented");
-//	}
-
-	@Test
-	public void testCreateTitle2() {
-		String title = ArgUtils.createTitle(metadata, null);
-		assertEquals("Erzählung etc", title);
-	}
-	@Test
-	public void testCreateTitle1() {
-		String title = ArgUtils.createTitle(new ObjectType[] { metadata0 }, null);
-		assertEquals("Erzählung", title);
-	}
-
-	@Test
-	public void testCreateFilename2() {
-		String filename = ArgUtils.createFilename(metadata, null, "zip");
-		assertEquals("Erzaehlung_etc.zip", filename);
-	}
-
-	@Test
-	public void testCreateFilename1() {
-		String filename = ArgUtils.createFilename(new ObjectType[] { metadata0 }, null, "html");
-		assertEquals("Erzaehlung.kscz.0.html", filename);
-	}
-
-	@Test
-	public void testCreateLastModified() {
-		Date lastModified = RESTUtils.createLastModified(metadata);
-		Date expected = new Date(1325700856000l);
-		assertEquals(expected, lastModified);
-	}
-
-}
diff --git a/src/test/java/info/textgrid/services/aggregator/StylesheetManagerTest.java b/src/test/java/info/textgrid/services/aggregator/StylesheetManagerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..43373ad6c5aff4269a622f6a58a67a2e33034435
--- /dev/null
+++ b/src/test/java/info/textgrid/services/aggregator/StylesheetManagerTest.java
@@ -0,0 +1,53 @@
+package info.textgrid.services.aggregator;
+
+import static org.junit.Assert.*;
+import info.textgrid.services.aggregator.util.StylesheetManager;
+import info.textgrid.services.aggregator.util.TextGridRepProvider;
+
+import java.io.IOException;
+import java.net.URI;
+
+import net.sf.saxon.s9api.SaxonApiException;
+import net.sf.saxon.s9api.XsltExecutable;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import com.google.common.base.Optional;
+
+public class StylesheetManagerTest {
+
+	private static URI INTERNAL_XSL = URI.create("/WEB-INF/xml/tei/stylesheet/xhtml2/tei.xsl");
+	private StylesheetManager manager;
+
+	@Before
+	public void setUp() {
+		manager = new StylesheetManager(null, TextGridRepProvider.getInstance());
+	}
+
+	@Test
+	public void testGetInternalStylesheet() throws SaxonApiException, IOException {
+		testGetStylesheet(INTERNAL_XSL);
+	}
+	
+	@Ignore("Need a stable available XSLT")
+	@Test
+	public void testGetHttpStylesheet() throws SaxonApiException, IOException {
+		URI teiStylesheet = URI.create("http://www.tei-c.org/Vault/P5/2.5.0/xml/tei/stylesheet/html/html.xsl");
+		testGetStylesheet(teiStylesheet);
+	}
+
+	private void testGetStylesheet(final URI xslURI) throws SaxonApiException, IOException {
+		final long requestCount = manager.stats().requestCount();
+		final XsltExecutable executable = manager.getStylesheet(xslURI, Optional.<String>absent(), false, true);
+		assertNotNull("StylesheetManager failed to return a stylesheet",  executable);
+		assertTrue(manager.stats().requestCount() == requestCount+1);
+		final long hitCount = manager.stats().hitCount();
+		final XsltExecutable executable2 = manager.getStylesheet(xslURI, Optional.<String>absent(), false, true);
+		assertTrue(executable == executable2);
+		assertTrue(manager.stats().hitCount() == hitCount+1);
+		System.out.println(manager.stats());
+	}
+
+}
diff --git a/src/test/java/info/textgrid/services/aggregator/TextGridRepTest.java b/src/test/java/info/textgrid/services/aggregator/TextGridRepTest.java
index 91c135a2d1e504e60917357d44531f7575cb77af..a7d9162d559775293bd781de075d217178c72b29 100644
--- a/src/test/java/info/textgrid/services/aggregator/TextGridRepTest.java
+++ b/src/test/java/info/textgrid/services/aggregator/TextGridRepTest.java
@@ -5,6 +5,7 @@
 import info.textgrid.middleware.confclient.ConfservClientConstants;
 import info.textgrid.middleware.tgsearch.client.SearchClient;
 import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.TGCrudService;
+import info.textgrid.services.aggregator.util.TextGridRepProvider;
 
 import org.junit.Test;
 import org.springframework.stereotype.Service;
diff --git a/src/test/java/info/textgrid/services/aggregator/teicorpus/TEICorpusTest.java b/src/test/java/info/textgrid/services/aggregator/teicorpus/TEICorpusTest.java
index d7c405781383bb99f7d46ba6f84e65c1320864a5..ace24d874073deee4de45964e318f6261c55e7bf 100644
--- a/src/test/java/info/textgrid/services/aggregator/teicorpus/TEICorpusTest.java
+++ b/src/test/java/info/textgrid/services/aggregator/teicorpus/TEICorpusTest.java
@@ -4,18 +4,21 @@
 import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.IoFault;
 import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.MetadataParseFault;
 import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.ObjectNotFoundFault;
-import info.textgrid.services.aggregator.TextGridRepProvider;
+import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.ProtocolNotImplementedFault;
+import info.textgrid.services.aggregator.ITextGridRep;
+import info.textgrid.services.aggregator.util.TextGridRepProvider;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.net.URI;
 import java.net.URISyntaxException;
 
 import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.StreamingOutput;
 
+import net.sf.saxon.s9api.SaxonApiException;
+
 import org.apache.cxf.helpers.IOUtils;
 import org.junit.Assert;
 import org.junit.Before;
@@ -23,25 +26,23 @@
 
 public class TEICorpusTest {
 
-	private TEICorpus teiCorpus;
+	private ITextGridRep repository;
 
 	@Before
 	public void setUp() throws Exception {
-		teiCorpus = new TEICorpus(TextGridRepProvider.getInstance());
+		repository = TextGridRepProvider.getInstance();
 	}
 
-	@Test(expected = ObjectNotFoundFault.class)
-	public void testGet404() throws URISyntaxException, ObjectNotFoundFault, MetadataParseFault, IoFault, AuthFault {
-		final Response response = teiCorpus.get("textgrid:doesnotexist",
-				true, false, null, null, null);
+	@Test(expected = WebApplicationException.class)
+	public void testGet404() throws URISyntaxException, ObjectNotFoundFault, MetadataParseFault, IoFault, AuthFault, ProtocolNotImplementedFault, IOException, SaxonApiException {
+		final Response response = new TEICorpusExporter(repository, null, "textgrid:doesnotexist").createResponse().build();
 		Assert.assertEquals(404, response.getStatus());
 	}
 
 	@Test
 	public void testGet() throws URISyntaxException, WebApplicationException,
-	IOException, ObjectNotFoundFault, MetadataParseFault, IoFault, AuthFault {
-		final Response response = teiCorpus.get("textgrid:jmzg.0", true,
-				false, null, null, null);
+	IOException, ObjectNotFoundFault, MetadataParseFault, IoFault, AuthFault, ProtocolNotImplementedFault, SaxonApiException {
+		final Response response = new TEICorpusExporter(repository, null, "textgrid:jmzg.0").createResponse().build();
 		final Object entity = response.getEntity();
 		final ByteArrayOutputStream output = new ByteArrayOutputStream();
 		((StreamingOutput) entity).write(output);
diff --git a/src/test/java/info/textgrid/services/aggregator/teicorpus/TEIHeaderStackTest.java b/src/test/java/info/textgrid/services/aggregator/teicorpus/TEIHeaderStackTest.java
index 94d5f2f7c2e244e6bdf8fabd29696727c8c704f9..b9966fa846e460f96e7f9644509680067d8e1bfc 100644
--- a/src/test/java/info/textgrid/services/aggregator/teicorpus/TEIHeaderStackTest.java
+++ b/src/test/java/info/textgrid/services/aggregator/teicorpus/TEIHeaderStackTest.java
@@ -6,7 +6,7 @@
 import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.MetadataParseFault;
 import info.textgrid.namespaces.middleware.tgcrud.services.tgcrudservice.ObjectNotFoundFault;
 import info.textgrid.services.aggregator.ITextGridRep;
-import info.textgrid.services.aggregator.TextGridRepProvider;
+import info.textgrid.services.aggregator.util.TextGridRepProvider;
 
 import javax.xml.bind.JAXBException;
 import javax.xml.parsers.ParserConfigurationException;
diff --git a/unification.odt b/unification.odt
new file mode 100644
index 0000000000000000000000000000000000000000..5345d764a5311b6fa38690fb6b84941abfd6efa0
Binary files /dev/null and b/unification.odt differ