/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.nifi.persistence; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import org.apache.nifi.cluster.protocol.DataFlow; import org.apache.nifi.controller.FlowController; import org.apache.nifi.controller.MissingBundleException; import org.apache.nifi.controller.StandardFlowSynchronizer; import org.apache.nifi.controller.UninheritableFlowException; import org.apache.nifi.controller.serialization.FlowSerializationException; import org.apache.nifi.controller.serialization.FlowSynchronizationException; import org.apache.nifi.controller.serialization.FlowSynchronizer; import org.apache.nifi.controller.serialization.StandardFlowSerializer; import org.apache.nifi.encrypt.StringEncryptor; import org.apache.nifi.util.NiFiProperties; import org.apache.nifi.util.file.FileUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public final class StandardXMLFlowConfigurationDAO implements FlowConfigurationDAO { private final Path flowXmlPath; private final StringEncryptor encryptor; private final FlowConfigurationArchiveManager archiveManager; private final NiFiProperties nifiProperties; private static final Logger LOG = LoggerFactory.getLogger(StandardXMLFlowConfigurationDAO.class); public StandardXMLFlowConfigurationDAO(final Path flowXml, final StringEncryptor encryptor, final NiFiProperties nifiProperties) throws IOException { this.nifiProperties = nifiProperties; final File flowXmlFile = flowXml.toFile(); if (!flowXmlFile.exists()) { // createDirectories would throw an exception if the directory exists but is a symbolic link if (Files.notExists(flowXml.getParent())) { Files.createDirectories(flowXml.getParent()); } Files.createFile(flowXml); //TODO: find a better solution. With Windows 7 and Java 7, Files.isWritable(source.getParent()) returns false, even when it should be true. } else if (!flowXmlFile.canRead() || !flowXmlFile.canWrite()) { throw new IOException(flowXml + " exists but you have insufficient read/write privileges"); } this.flowXmlPath = flowXml; this.encryptor = encryptor; this.archiveManager = new FlowConfigurationArchiveManager(flowXmlPath, nifiProperties); } @Override public boolean isFlowPresent() { final File flowXmlFile = flowXmlPath.toFile(); return flowXmlFile.exists() && flowXmlFile.length() > 0; } @Override public synchronized void load(final FlowController controller, final DataFlow dataFlow) throws IOException, FlowSerializationException, FlowSynchronizationException, UninheritableFlowException, MissingBundleException { final FlowSynchronizer flowSynchronizer = new StandardFlowSynchronizer(encryptor, nifiProperties); controller.synchronize(flowSynchronizer, dataFlow); if (StandardFlowSynchronizer.isEmpty(dataFlow)) { // If the dataflow is empty, we want to save it. We do this because when we start up a brand new cluster with no // dataflow, we need to ensure that the flow is consistent across all nodes in the cluster and that upon restart // of NiFi, the root group ID does not change. However, we don't always want to save it, because if the flow is // not empty, then we can get into a bad situation, since the Processors, etc. don't have the appropriate "Scheduled // State" yet (since they haven't yet been scheduled). So if there are components in the flow and we save it, we // may end up saving the flow in such a way that all components are stopped. // We save based on the controller, not the provided data flow because Process Groups may contain 'local' templates. save(controller); } } @Override public synchronized void load(final OutputStream os) throws IOException { if (!isFlowPresent()) { return; } try (final InputStream inStream = Files.newInputStream(flowXmlPath, StandardOpenOption.READ); final InputStream gzipIn = new GZIPInputStream(inStream)) { FileUtils.copy(gzipIn, os); } } @Override public void load(final OutputStream os, final boolean compressed) throws IOException { if (compressed) { Files.copy(flowXmlPath, os); } else { load(os); } } @Override public synchronized void save(final InputStream is) throws IOException { try (final OutputStream outStream = Files.newOutputStream(flowXmlPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE); final OutputStream gzipOut = new GZIPOutputStream(outStream)) { FileUtils.copy(is, gzipOut); } } @Override public void save(final FlowController flow) throws IOException { LOG.trace("Saving flow to disk"); try (final OutputStream outStream = Files.newOutputStream(flowXmlPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE); final OutputStream gzipOut = new GZIPOutputStream(outStream)) { save(flow, gzipOut); } LOG.debug("Finished saving flow to disk"); } @Override public synchronized void save(final FlowController flow, final OutputStream os) throws IOException { try { final StandardFlowSerializer xmlTransformer = new StandardFlowSerializer(encryptor); flow.serialize(xmlTransformer, os); } catch (final FlowSerializationException fse) { throw new IOException(fse); } } @Override public synchronized void save(final FlowController controller, final boolean archive) throws IOException { if (null == controller) { throw new NullPointerException(); } Path tempFile; Path configFile; configFile = flowXmlPath; tempFile = configFile.getParent().resolve(configFile.toFile().getName() + ".new.xml.gz"); try (final OutputStream fileOut = Files.newOutputStream(tempFile); final OutputStream outStream = new GZIPOutputStream(fileOut)) { final StandardFlowSerializer xmlTransformer = new StandardFlowSerializer(encryptor); controller.serialize(xmlTransformer, outStream); Files.deleteIfExists(configFile); FileUtils.renameFile(tempFile.toFile(), configFile.toFile(), 5, true); } catch (final FlowSerializationException fse) { throw new IOException(fse); } finally { Files.deleteIfExists(tempFile); } if (archive) { try { archiveManager.archive(); } catch (final Exception ex) { LOG.error("Unable to archive flow configuration as requested due to " + ex); if (LOG.isDebugEnabled()) { LOG.error("", ex); } } } } }