blob: 479b29da0d41bb1d08bce44c6163e6c567f080e9 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2010-2014 SAP AG and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* SAP AG - initial API and implementation
*******************************************************************************/
package org.eclipse.skalli.core.rest.admin;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.MessageFormat;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import org.eclipse.skalli.commons.CollectionUtils;
import org.eclipse.skalli.commons.FormatUtils;
import org.eclipse.skalli.commons.ThreadPool;
import org.eclipse.skalli.core.storage.FileStorageComponent;
import org.eclipse.skalli.services.BundleProperties;
import org.eclipse.skalli.services.ServiceFilter;
import org.eclipse.skalli.services.Services;
import org.eclipse.skalli.services.extension.rest.ResourceBase;
import org.eclipse.skalli.services.permit.Permits;
import org.eclipse.skalli.services.persistence.PersistenceService;
import org.eclipse.skalli.services.persistence.StorageConsumer;
import org.eclipse.skalli.services.persistence.StorageService;
import org.restlet.data.Disposition;
import org.restlet.data.MediaType;
import org.restlet.data.Status;
import org.restlet.representation.OutputRepresentation;
import org.restlet.representation.Representation;
import org.restlet.resource.Get;
import org.restlet.resource.Put;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ProjectBackupResource extends ResourceBase {
private static final Logger LOG = LoggerFactory.getLogger(ProjectBackupResource.class);
private static final String FILE_NAME = "backup.zip"; //$NON-NLS-1$
private static final String ACTION_PARAM = "action"; //$NON-NLS-1$
private static final String ACTION_OVERWRITE = "overwrite"; //$NON-NLS-1$
// error codes for logging and error responses
private static final String ID_PREFIX = "rest:api/admin/backup:"; //$NON-NLS-1$
private static final String ERROR_ID_IO_ERROR = ID_PREFIX + "00"; //$NON-NLS-1$
private static final String ERROR_ID_NO_STORAGE_SERVICE = ID_PREFIX + "10"; //$NON-NLS-1$
private static final String ERROR_ID_FAILED_TO_RETRIEVE_KEYS = ID_PREFIX + "20"; //$NON-NLS-1$
private static final String ERROR_ID_FAILED_TO_STORE = ID_PREFIX + "30"; //$NON-NLS-1$
private static final String ERROR_ID_OVERWRITE_EXISTING_DATA = ID_PREFIX + "40"; //$NON-NLS-1$
@SuppressWarnings("nls")
private static final Set<String> CATEGORIES = CollectionUtils.asSet("customization", "Project", "Issues",
"Favorites", "User", "Group", "History");
@Get
public Representation backup() {
if (!Permits.isAllowed(getAction(), getPath())) {
return createUnauthorizedRepresentation();
}
StorageService storageService = getStorageService();
if (storageService == null) {
return createServiceUnavailableRepresentation(ERROR_ID_NO_STORAGE_SERVICE, "Storage Service");
}
BackupQuery query = new BackupQuery(getQueryAttributes());
Set<String> categories = getCategories(query);
ZipOutputRepresentation zipRepresentation = new ZipOutputRepresentation(storageService, categories,
query.getFrom(), query.getTo());
Disposition disposition = new Disposition(Disposition.TYPE_ATTACHMENT);
disposition.setFilename(FILE_NAME);
zipRepresentation.setDisposition(disposition);
setStatus(Status.SUCCESS_OK);
return zipRepresentation;
}
@Put
public Representation restore(Representation entity) {
if (!Permits.isAllowed(getAction(), getPath())) {
return createUnauthorizedRepresentation();
}
StorageService storageService = getStorageService();
if (storageService == null) {
return createServiceUnavailableRepresentation(ERROR_ID_NO_STORAGE_SERVICE, "Storage Service");
}
BackupQuery query = new BackupQuery(getQueryAttributes());
String action = getQueryAttribute(ACTION_PARAM);
Set<String> categories = getCategories(query);
Set<String> accepted = new HashSet<String>();
Set<String> rejected = new HashSet<String>();
for (String category: CATEGORIES) {
if (categories.contains(category)) {
try {
if (ACTION_OVERWRITE.equals(action) || storageService.keys(category).isEmpty()) {
accepted.add(category);
} else {
rejected.add(category);
}
} catch (IOException e) {
LOG.error(MessageFormat.format("Failed to retrieve keys for category {0} ({1})",
category, ERROR_ID_FAILED_TO_RETRIEVE_KEYS), e);
return createErrorRepresentation(Status.SERVER_ERROR_INTERNAL, ERROR_ID_FAILED_TO_RETRIEVE_KEYS ,
"Failed to store the attached backup resource");
}
}
}
if (rejected.size() > 0) {
return createErrorRepresentation(
Status.CLIENT_ERROR_PRECONDITION_FAILED,
ERROR_ID_OVERWRITE_EXISTING_DATA,
MessageFormat.format(
"Restore might overwrite existing data in the folling categories:\n{0}\n" +
"Either exclude these categories from the restore with a \"exclude=<comma-separated-list>\" " +
"parameter or enforce the restore with \"action=overwrite\".",
CollectionUtils.toString(rejected, '\n')));
}
if (accepted.isEmpty()) {
setStatus(Status.SUCCESS_NO_CONTENT);
return null;
}
boolean withHistory = accepted.contains("History"); //$NON-NLS-1$
int countItems = 0;
int countHistoryItems = 0;
ZipInputStream zipStream = null;
try {
zipStream = new ZipInputStream(entity.getStream());
ZipEntry entry = zipStream.getNextEntry();
while (entry != null) {
try {
if (!entry.isDirectory()) {
String entryName = entry.getName().replace('\\', '/');
String[] parts = StringUtils.split(entryName, '/');
if (parts.length != 2) {
LOG.info(MessageFormat.format("Restore: {0} is not recognized as entity key", entryName));
continue;
}
String category = parts[0];
String name = parts[1];
if (name.endsWith(".xml")) { //$NON-NLS-1$
name = name.substring(0, name.length() - 4);
};
String[] nameParts = StringUtils.split(name, '_');
String key = nameParts[0];
long timestamp = NumberUtils.toLong(nameParts.length == 2 ? nameParts[1] : null, -1L);
// ensure that the category of the entry, i.e. the directory name,
// is in the set of accepted categories
if (accepted.contains(category)) {
if (timestamp < 0) {
try {
storageService.write(category, key, zipStream);
++countItems;
} catch (IOException e) {
LOG.error(MessageFormat.format(
"Failed to store entity with key {0} and category {1} ({2})",
key, category, ERROR_ID_FAILED_TO_STORE), e);
return createErrorRepresentation(Status.SERVER_ERROR_INTERNAL, ERROR_ID_FAILED_TO_STORE,
"Failed to store the attached backup");
}
} else if (withHistory) {
try {
storageService.writeToArchive(category, key, timestamp, zipStream);
++countHistoryItems;
} catch (IOException e) {
LOG.error(MessageFormat.format(
"Failed to store history entry for timestamp {0} with key {1} and category {2} ({3})",
FormatUtils.formatUTCWithMillis(timestamp), key, category,
ERROR_ID_FAILED_TO_STORE), e);
return createErrorRepresentation(Status.SERVER_ERROR_INTERNAL,
ERROR_ID_FAILED_TO_STORE,
"Failed to store the attached backup");
}
}
} else {
LOG.info(MessageFormat.format("Restore: Excluded {0} (category ''{1}'' not accepted)",
key, category));
}
}
} finally {
zipStream.closeEntry();
entry = zipStream.getNextEntry();
}
}
} catch (IOException e) {
return createIOErrorRepresentation(ERROR_ID_IO_ERROR, e);
} finally {
IOUtils.closeQuietly(zipStream);
}
if (LOG.isInfoEnabled()) {
LOG.info(MessageFormat.format("Restored {0} items and {1} history items", countItems, countHistoryItems));
}
// ensure that the persistence service attached to the storage
// refreshes all caches and reloads all entities --- do that
// in the background, otherwise we might run into timeouts
ThreadPool.submit(new Runnable() {
@Override
public void run() {
PersistenceService persistenceService = Services.getService(PersistenceService.class);
if (persistenceService != null) {
LOG.info("Refreshing all caches");
persistenceService.refreshAll();
}
}
});
setStatus(Status.SUCCESS_NO_CONTENT);
return null;
}
/**
* Start with a result set that contains all entries from CATEGORIES,
* except if there is an include list; in this case start with an empty
* result set. Then first add all categories contained in the include list
* and remove afterwards all entries contained in the exclude list.
*/
private Set<String> getCategories(BackupQuery query) {
Set<String> categories = new HashSet<String>(CATEGORIES);
Set<String> include = query.getIncluded();
Set<String> exclude = query.getExcluded();
if (!include.isEmpty()) {
categories.removeAll(CATEGORIES);
}
categories.addAll(include);
categories.removeAll(exclude);
return categories;
}
private StorageService getStorageService() {
final String storageServiceClassName = BundleProperties.getProperty(
BundleProperties.PROPERTY_STORAGE_SERVICE, FileStorageComponent.class.getName());
Set<StorageService> storageServices = Services.getServices(StorageService.class, new ServiceFilter<StorageService>() {
@Override
public boolean accept(StorageService instance) {
return instance.getClass().getName().equals(storageServiceClassName);
}
});
return storageServices.size() > 0? storageServices.iterator().next() : null;
}
private static class ZipOutputRepresentation extends OutputRepresentation {
private static final int BUFFER = 2048;
private final StorageService storageService;
private final Set<String> categories;
private final long startDate;
private final long endDate;
private final boolean withHistory;
public ZipOutputRepresentation(StorageService storageService, Set<String> categories, long startDate, long endDate) {
super(MediaType.APPLICATION_ZIP);
this.storageService = storageService;
this.categories = categories;
this.startDate = startDate;
this.endDate = endDate;
this.withHistory = categories.contains("History"); //$NON-NLS-1$
}
@Override
public void write(OutputStream out) throws IOException {
ZipOutputStream zipStream = null;
try {
zipStream = new ZipOutputStream(new BufferedOutputStream(out));
for (String category: CATEGORIES) {
if (categories.contains(category)) {
write(category, zipStream);
}
}
zipStream.flush();
} finally {
IOUtils.closeQuietly(zipStream);
}
}
private void write(String category, ZipOutputStream target) throws IOException {
List<String> keys = storageService.keys(category);
for (String key: keys) {
write(category, key, target);
if (withHistory) {
writeHistory(category, key, target);
}
}
}
private void write(String category, String key, final ZipOutputStream target) throws IOException {
storageService.read(category, key, new StorageConsumer() {
@Override
public void consume(String category, String key, long lastModified, InputStream blob)
throws IOException {
if (inRange(lastModified)) {
String entryName = MessageFormat.format("{0}/{1}.xml", category, key); //$NON-NLS-1$
write(entryName, blob, target);
}
}
});
}
private void writeHistory(String category, String key, final ZipOutputStream target) throws IOException {
storageService.readFromArchive(category, key, new StorageConsumer() {
@Override
public void consume(String category, String key, long lastModified, InputStream blob)
throws IOException {
if (inRange(lastModified)) {
String entryName = MessageFormat.format("{0}/{1}_{2}.xml", //$NON-NLS-1$
category, key, Long.toString(lastModified));
write(entryName, blob, target);
}
}
});
}
private void write(String entryName, InputStream blob, ZipOutputStream target) throws IOException {
BufferedInputStream source = null;
try {
source = new BufferedInputStream(blob, BUFFER);
ZipEntry entry = new ZipEntry(entryName);
target.putNextEntry(entry);
int count;
byte data[] = new byte[BUFFER];
while ((count = source.read(data, 0, BUFFER)) != -1) {
target.write(data, 0, count);
}
} finally {
IOUtils.closeQuietly(source);
}
}
private boolean inRange(long lastModified) {
return (startDate <= 0 || startDate <= lastModified) && (endDate <= 0 || lastModified <= endDate);
}
}
}