/******************************************************************************* * 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.eclipse.skalli.services.search.SearchQuery; 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 EXCLUDE_PARAM = "exclude"; //$NON-NLS-1$ private static final String INCLUDE_PARAM = "include"; //$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"); } Set<String> categories = getCategories(); ZipOutputRepresentation zipRepresentation = new ZipOutputRepresentation(storageService, categories); 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"); } String action = getQueryAttribute(ACTION_PARAM); Set<String> categories = getCategories(); 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() { Set<String> categories = new HashSet<String>(CATEGORIES); Set<String> include = CollectionUtils.asSet(StringUtils.split(getQueryAttribute(INCLUDE_PARAM), SearchQuery.PARAM_LIST_SEPARATOR)); Set<String> exclude = CollectionUtils.asSet(StringUtils.split(getQueryAttribute(EXCLUDE_PARAM), SearchQuery.PARAM_LIST_SEPARATOR)); 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 StorageService storageService; private Set<String> categories; private boolean withHistory; public ZipOutputRepresentation(StorageService storageService, Set<String> categories) { super(MediaType.APPLICATION_ZIP); this.storageService = storageService; this.categories = categories; 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 { String entryName = MessageFormat.format("{0}/{1}.xml", category, key); //$NON-NLS-1$ write(entryName, storageService.read(category, key), 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 { 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); } } } }