/* * Copyright (c) 2013 Cisco Systems, Inc. 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 */ package org.opendaylight.controller.config.persist.storage.file; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Charsets; import com.google.common.base.Optional; import com.google.common.base.Preconditions; import com.google.common.collect.Sets; import com.google.common.io.Files; import org.apache.commons.lang3.StringUtils; import org.opendaylight.controller.config.persist.api.storage.StorageAdapter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.SAXException; import javax.xml.parsers.ParserConfigurationException; import java.io.File; import java.io.IOException; import java.nio.charset.Charset; import java.util.Set; /** * StorageAdapter that stores configuration in a plan file. */ public class FileStorageAdapter implements StorageAdapter { private static final Logger logger = LoggerFactory.getLogger(FileStorageAdapter.class); // TODO prefix properties private static final Charset ENCODING = Charsets.UTF_8; public static final String FILE_STORAGE_PROP = "fileStorage"; public static final String NUMBER_OF_BACKUPS = "numberOfBackups"; private static final String SEPARATOR_E_PURE = "//END OF CONFIG"; private static final String SEPARATOR_E = newLine(SEPARATOR_E_PURE); private static final String SEPARATOR_M_PURE = "//END OF SNAPSHOT"; private static final String SEPARATOR_M = newLine(SEPARATOR_M_PURE); private static final String SEPARATOR_S = newLine("//START OF CONFIG"); private static final String SEPARATOR_SL_PURE = "//START OF CONFIG-LAST"; private static final String SEPARATOR_SL = newLine(SEPARATOR_SL_PURE); private static Integer numberOfStoredBackups; private File storage; @Override public void setProperties(PropertiesProvider propertiesProvider) { File storage = extractStorageFileFromProperties(propertiesProvider); logger.debug("Using file {}", storage.getAbsolutePath()); // Create file if it does not exist File parentFile = storage.getAbsoluteFile().getParentFile(); if (parentFile.exists() == false) { logger.debug("Creating parent folders {}", parentFile); parentFile.mkdirs(); } if (storage.exists() == false) { logger.debug("Storage file does not exist, creating empty file"); try { boolean result = storage.createNewFile(); if (result == false) throw new RuntimeException("Unable to create storage file " + storage); } catch (IOException e) { throw new RuntimeException("Unable to create storage file " + storage, e); } } if (numberOfStoredBackups == 0) { throw new RuntimeException(NUMBER_OF_BACKUPS + " property should be either set to positive value, or ommited. Can not be set to 0."); } setFileStorage(storage); } @VisibleForTesting void setFileStorage(File storage) { this.storage = storage; } @VisibleForTesting void setNumberOfBackups(Integer numberOfBackups) { numberOfStoredBackups = numberOfBackups; } private static File extractStorageFileFromProperties(PropertiesProvider propertiesProvider) { String fileStorageProperty = propertiesProvider.getProperty(FILE_STORAGE_PROP); Preconditions.checkNotNull(fileStorageProperty, "Unable to find " + propertiesProvider.getFullKeyForReporting(FILE_STORAGE_PROP)); File result = new File(fileStorageProperty); String numberOfBAckupsAsString = propertiesProvider.getProperty(NUMBER_OF_BACKUPS); if (numberOfBAckupsAsString != null) { numberOfStoredBackups = Integer.valueOf(numberOfBAckupsAsString); } else { numberOfStoredBackups = Integer.MAX_VALUE; } return result; } private static String newLine(String string) { return string + "\n"; } @Override public void persistConfig(ConfigSnapshotHolder holder) throws IOException { Preconditions.checkNotNull(storage, "Storage file is null"); String content = Files.toString(storage, ENCODING); if (numberOfStoredBackups == Integer.MAX_VALUE) { resetLastConfig(content); persistLastConfig(holder); } else { if (numberOfStoredBackups == 1) { Files.write("", storage, ENCODING); persistLastConfig(holder); } else { int count = StringUtils.countMatches(content, SEPARATOR_S); if ((count + 1) < numberOfStoredBackups) { resetLastConfig(content); persistLastConfig(holder); } else { String contentSubString = StringUtils.substringBefore(content, SEPARATOR_E); contentSubString = contentSubString.concat(SEPARATOR_E_PURE); content = StringUtils.substringAfter(content, contentSubString); resetLastConfig(content); persistLastConfig(holder); } } } } private void resetLastConfig(String content) throws IOException { content = content.replaceFirst(SEPARATOR_SL, SEPARATOR_S); Files.write(content, storage, ENCODING); } private void persistLastConfig(ConfigSnapshotHolder holder) throws IOException { Files.append(SEPARATOR_SL, storage, ENCODING); String snapshotAsString = holder.getConfigSnapshot(); Files.append(newLine(snapshotAsString), storage, ENCODING); Files.append(SEPARATOR_M, storage, ENCODING); Files.append(toStringCaps(holder.getCapabilities()), storage, ENCODING); Files.append(SEPARATOR_E, storage, ENCODING); } private CharSequence toStringCaps(Set capabilities) { StringBuilder b = new StringBuilder(); for (String capability : capabilities) { b.append(newLine(capability)); } return b.toString(); } @Override public Optional loadLastConfig() throws IOException { Preconditions.checkNotNull(storage, "Storage file is null"); if (!storage.exists()) { return Optional.absent(); } final LineProcessor lineProcessor = new LineProcessor(); String result = Files.readLines(storage, ENCODING, lineProcessor); try { if (lineProcessor.getConfigSnapshot().isPresent() == false) { return Optional.absent(); } else { return Optional. of(new PersistedConfigImpl(lineProcessor.getConfigSnapshot(), lineProcessor.getCapabilities())); } } catch (ParserConfigurationException | SAXException e) { throw new IOException("Unable to load last config ", e); } } private static final class LineProcessor implements com.google.common.io.LineProcessor { private boolean inLastConfig, inLastSnapshot; private final StringBuffer snapshotBuffer = new StringBuffer(); private final Set caps = Sets.newHashSet(); @Override public String getResult() { return null; } @Override public boolean processLine(String line) throws IOException { if (inLastConfig && line.equals(SEPARATOR_E_PURE)) { inLastConfig = false; return false; } if (inLastConfig && line.equals(SEPARATOR_M_PURE)) { inLastSnapshot = false; return true; } if (inLastConfig) { if (inLastSnapshot) { snapshotBuffer.append(line); snapshotBuffer.append(System.lineSeparator()); } else { caps.add(line); } } if (line.equals(SEPARATOR_SL_PURE)) { inLastConfig = true; inLastSnapshot = true; } return true; } Optional getConfigSnapshot() throws IOException, SAXException, ParserConfigurationException { final String xmlContent = snapshotBuffer.toString(); if (xmlContent == null || xmlContent.equals("")) { return Optional.absent(); } else return Optional.of(xmlContent); } Set getCapabilities() throws IOException, SAXException, ParserConfigurationException { return caps; } } @Override public void close() throws IOException { } @Override public String toString() { return "FileStorageAdapter [storage=" + storage + "]"; } private class PersistedConfigImpl implements ConfigSnapshotHolder { private final String snapshot; private final Set caps; public PersistedConfigImpl(Optional configSnapshot, Set capabilities) { this.snapshot = configSnapshot.get(); this.caps = capabilities; } @Override public String getConfigSnapshot() { return snapshot; } @Override public Set getCapabilities() { return caps; } } }