/* ================================================================== * DefaultBackupManager.java - Mar 27, 2013 9:17:24 AM * * Copyright 2007-2013 SolarNetwork.net Dev Team * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA * 02111-1307 USA * ================================================================== */ package net.solarnetwork.node.backup; import java.io.FilterOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import java.util.zip.ZipOutputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.HierarchicalMessageSource; import org.springframework.context.MessageSource; import org.springframework.context.support.ResourceBundleMessageSource; import org.springframework.util.FileCopyUtils; import net.solarnetwork.node.settings.SettingSpecifier; import net.solarnetwork.node.settings.support.BasicRadioGroupSettingSpecifier; import net.solarnetwork.node.util.PrefixedMessageSource; import net.solarnetwork.util.DynamicServiceUnavailableException; import net.solarnetwork.util.OptionalService; import net.solarnetwork.util.StringUtils; import net.solarnetwork.util.UnionIterator; /** * Default implementation of {@link BackupManager}. * * <p> * The configurable properties of this class are: * </p> * * <dl class="class-properties"> * <dt>backupServiceTracker</dt> * <dd>A tracker for the desired backup service to use.</dd> * * <dt>resourceProviders</dt> * <dd>The collection of {@link BackupResourceProvider} instances that provide * the resources to be backed up.</dd> * </dl> * * @author matt * @version 1.3 */ public class DefaultBackupManager implements BackupManager { private final Logger log = LoggerFactory.getLogger(getClass()); private Collection<BackupService> backupServices; private OptionalService<BackupService> backupServiceTracker; private Collection<BackupResourceProvider> resourceProviders; private ExecutorService executorService = defaultExecutorService(); private int backupRestoreDelaySeconds = 15; private static HierarchicalMessageSource getMessageSourceInstance() { ResourceBundleMessageSource source = new ResourceBundleMessageSource(); source.setBundleClassLoader(DefaultBackupManager.class.getClassLoader()); source.setBasename(DefaultBackupManager.class.getName()); return source; } private static ExecutorService defaultExecutorService() { // we want at most one backup happening at a time by default return new ThreadPoolExecutor(0, 1, 5, TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>(3, true)); } /** * Initialize after all properties set. */ public void init() { // look for marked backup to restore scheduleRestore(); } private void scheduleRestore() { Thread t = new Thread(new Runnable() { @Override public void run() { boolean retry = true; try { // sleep for just a bit here Thread.sleep(backupRestoreDelaySeconds * 1000L); } catch ( InterruptedException e ) { return; } log.debug("Looking to see if there is a marked backup to restore"); BackupService backupService = (backupServiceTracker != null ? backupServiceTracker.service() : null); if ( backupService != null ) { Map<String, String> props = new HashMap<String, String>(); Backup backup = backupService.markedBackupForRestore(props); if ( backup != null ) { if ( restoreBackupInternal(backup, props) ) { // clear marked backup if ( backupService.markBackupForRestore(null, null) ) { retry = false; finishRestore(backup); } } } else { // no marked backup to restore retry = false; } } if ( retry ) { log.debug( "Will retry looking to see if there is a marked backup to restore in {} seconds", backupRestoreDelaySeconds); scheduleRestore(); } } }); t.setDaemon(true); t.start(); } private void finishRestore(Backup backup) { log.info("Restore from backup {} complete", backup.getKey()); } @Override public String getSettingUID() { return getClass().getName(); } @Override public String getDisplayName() { return "Backup Manager"; } @Override public MessageSource getMessageSource() { HierarchicalMessageSource source = getMessageSourceInstance(); HierarchicalMessageSource child = source; for ( BackupService backupService : backupServices ) { PrefixedMessageSource ps = new PrefixedMessageSource(); ps.setDelegate(backupService.getSettingSpecifierProvider().getMessageSource()); ps.setPrefix(backupService.getKey() + "."); child.setParentMessageSource(ps); child = ps; } return source; } @Override public List<SettingSpecifier> getSettingSpecifiers() { List<SettingSpecifier> results = new ArrayList<SettingSpecifier>(20); BasicRadioGroupSettingSpecifier serviceSpec = new BasicRadioGroupSettingSpecifier( "backupServiceTracker.propertyFilters['key']", FileSystemBackupService.KEY); Map<String, String> serviceSpecValues = new TreeMap<String, String>(); for ( BackupService service : backupServices ) { serviceSpecValues.put(service.getKey(), service.getSettingSpecifierProvider().getDisplayName()); } serviceSpec.setValueTitles(serviceSpecValues); results.add(serviceSpec); return results; } @Override public BackupService activeBackupService() { return backupServiceTracker.service(); } @Override public Iterable<BackupResource> resourcesForBackup() { BackupService service = activeBackupService(); if ( service == null ) { log.debug("No BackupService available, can't find resources for backup"); return Collections.emptyList(); } if ( service.getInfo().getStatus() != BackupStatus.Configured ) { log.info("BackupService {} in {} state, can't find resources for backup", service.getKey(), service.getInfo().getStatus()); return Collections.emptyList(); } final List<Iterator<BackupResource>> resources = new ArrayList<Iterator<BackupResource>>(10); for ( BackupResourceProvider provider : resourceProviders ) { // map each resource into a sub directory Iterator<BackupResource> itr = provider.getBackupResources().iterator(); resources.add(new PrefixedBackupResourceIterator(itr, provider.getKey())); } return new Iterable<BackupResource>() { @Override public Iterator<BackupResource> iterator() { return new UnionIterator<BackupResource>(resources); } }; } @Override public Backup createBackup() { return createBackup(null); } @Override public Backup createBackup(final Map<String, String> props) { final BackupService service = activeBackupService(); if ( service == null ) { log.debug("No active backup service available, cannot perform backup"); return null; } final BackupServiceInfo info = service.getInfo(); final BackupStatus status = info.getStatus(); if ( !(status == BackupStatus.Configured || status == BackupStatus.Error) ) { log.info("BackupService {} is in the {} state; cannot perform backup", service.getKey(), status); return null; } log.info("Initiating backup to service {}", service.getKey()); final Backup backup = service.performBackup(resourcesForBackup()); if ( backup != null ) { log.info("Backup {} {} with service {}", backup.getKey(), (backup.isComplete() ? "completed" : "initiated"), service.getKey()); } return backup; } @Override public Future<Backup> createAsynchronousBackup() { return createAsynchronousBackup(null); } @Override public Future<Backup> createAsynchronousBackup(final Map<String, String> props) { assert executorService != null; return executorService.submit(new Callable<Backup>() { @Override public Backup call() throws Exception { return createBackup(props); } }); } @Override public void exportBackupArchive(String backupKey, OutputStream out) throws IOException { exportBackupArchive(backupKey, out, null); } @Override public void exportBackupArchive(String backupKey, OutputStream out, Map<String, String> props) throws IOException { final BackupService service = activeBackupService(); if ( service == null ) { return; } final Backup backup = service.backupForKey(backupKey); if ( backup == null ) { return; } // create the zip archive for the backup files ZipOutputStream zos = new ZipOutputStream(out); try { BackupResourceIterable resources = service.getBackupResources(backup); for ( BackupResource r : resources ) { zos.putNextEntry(new ZipEntry(r.getBackupPath())); FileCopyUtils.copy(r.getInputStream(), new FilterOutputStream(zos) { @Override public void close() throws IOException { // FileCopyUtils closed the stream, which we don't want here } }); } resources.close(); } finally { zos.flush(); zos.finish(); zos.close(); } } @Override public Future<Backup> importBackupArchive(InputStream archive) throws IOException { return importBackupArchive(archive, null); } @Override public Future<Backup> importBackupArchive(InputStream archive, final Map<String, String> props) throws IOException { final BackupService service = backupServiceTracker.service(); if ( service == null ) { throw new DynamicServiceUnavailableException( "No BackupService available to import backup with"); } final ZipInputStream zin = new ZipInputStream(archive); return executorService.submit(new Callable<Backup>() { @Override public Backup call() throws Exception { final BackupResourceIterable itr = new ZipStreamBackupResourceIterable(zin, props); return service.importBackup(null, itr, props); } }); } @Override public void restoreBackup(Backup backup) { restoreBackup(backup, null); } @Override public void restoreBackup(Backup backup, Map<String, String> props) { restoreBackupInternal(backup, props); } public boolean restoreBackupInternal(Backup backup, Map<String, String> props) { BackupService service = backupServiceTracker.service(); if ( service == null ) { log.warn("No BackupService available to restore backup with"); return false; } final Set<String> providerKeySet = (props == null ? null : StringUtils.commaDelimitedStringToSet(props.get(RESOURCE_PROVIDER_FILTER))); BackupResourceIterable resources = service.getBackupResources(backup); boolean result = true; try { for ( final BackupResource r : resources ) { // top-level dir is the key of the provider final String path = r.getBackupPath(); log.debug("Inspecting backup {} resource {}", backup.getKey(), path); final int providerIndex = path.indexOf('/'); if ( providerIndex != -1 ) { final String providerKey = path.substring(0, providerIndex); if ( providerKeySet != null && !providerKeySet.isEmpty() && !providerKeySet.contains(providerKey) ) { log.debug("Skipping backup {} resource {} (provider filtered)", backup.getKey(), path); continue; } boolean resourceHandled = false; for ( BackupResourceProvider provider : resourceProviders ) { if ( providerKey.equals(provider.getKey()) ) { log.debug("Restoring backup {} resource {}", backup.getKey(), path); resourceHandled = provider.restoreBackupResource(new BackupResource() { @Override public String getProviderKey() { return providerKey; } @Override public String getBackupPath() { return path.substring(providerIndex + 1); } @Override public InputStream getInputStream() throws IOException { return r.getInputStream(); } @Override public long getModificationDate() { return r.getModificationDate(); } }); break; } } if ( !resourceHandled ) { result = false; log.warn( "Backup {} resource {} could not be restored because no resource provider handled the resource.", backup.getKey(), path); } } } } catch ( RuntimeException e ) { log.error("Error restoring backup {}", backup.getKey(), e); } finally { try { resources.close(); } catch ( IOException e ) { // ignore } } return result; } @Override public BackupInfo infoForBackup(final String key, final Locale locale) { BackupService service = activeBackupService(); if ( service == null ) { log.debug("No BackupService available, can't find resources for backup"); return null; } Backup backup = service.backupForKey(key); if ( backup == null ) { log.debug("No backup avaialble from service {} for key {}", service.getKey(), key); return null; } Map<String, BackupResourceProviderInfo> providerInfos = new LinkedHashMap<String, BackupResourceProviderInfo>(); List<BackupResourceInfo> resourceInfos = new ArrayList<BackupResourceInfo>(); BackupResourceIterable resources = null; try { resources = service.getBackupResources(backup); for ( BackupResource r : resources ) { final String path = r.getBackupPath(); final int providerIndex = path.indexOf('/'); if ( providerIndex == -1 ) { continue; } final String providerKey = path.substring(0, providerIndex); BackupResourceProvider provider = providerForKey(providerKey); if ( provider == null ) { continue; } if ( !providerInfos.containsKey(providerKey) ) { providerInfos.put(providerKey, provider.providerInfo(locale)); } BackupResourceInfo info = provider.resourceInfo(r, locale); if ( info != null ) { String name = info.getName(); if ( name != null && name.equals(path) ) { name = path.substring(providerIndex + 1); } resourceInfos .add(new SimpleBackupResourceInfo(providerKey, name, info.getDescription())); } } } finally { if ( resources != null ) { try { resources.close(); } catch ( IOException e ) { // ignore } } } return new SimpleBackupInfo(key, backup.getDate(), providerInfos.values(), resourceInfos); } private BackupResourceProvider providerForKey(String key) { Collection<BackupResourceProvider> providers = resourceProviders; if ( providers == null || providers.isEmpty() ) { return null; } for ( BackupResourceProvider provider : providers ) { if ( key.equals(provider.getKey()) ) { return provider; } } return null; } public void setBackupServiceTracker(OptionalService<BackupService> backupServiceTracker) { this.backupServiceTracker = backupServiceTracker; } public void setBackupServices(Collection<BackupService> backupServices) { this.backupServices = backupServices; } public void setResourceProviders(Collection<BackupResourceProvider> resourceProviders) { this.resourceProviders = resourceProviders; } public void setExecutorService(ExecutorService executorService) { this.executorService = executorService; } /** * Set a number of seconds to delay the attempt of restoring a backup, when * a backup has been previously marked for restoration. This delay gives the * platform time to boot up and register the backup resource providers and * other services required to perform the restore. * * @param backupRestoreDelaySeconds * The number of seconds to delay attempting to restore from backup. * @since 1.1 */ public void setBackupRestoreDelaySeconds(int backupRestoreDelaySeconds) { this.backupRestoreDelaySeconds = backupRestoreDelaySeconds; } }