Repository: nifi-registry
Updated Branches:
  refs/heads/master a520c7575 -> 7dd88eb44


http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/9eb0cef0/nifi-registry-provider-impl/src/main/java/org/apache/nifi/registry/metadata/MetadataHolder.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-provider-impl/src/main/java/org/apache/nifi/registry/metadata/MetadataHolder.java
 
b/nifi-registry-provider-impl/src/main/java/org/apache/nifi/registry/metadata/MetadataHolder.java
new file mode 100644
index 0000000..6ce3dfb
--- /dev/null
+++ 
b/nifi-registry-provider-impl/src/main/java/org/apache/nifi/registry/metadata/MetadataHolder.java
@@ -0,0 +1,141 @@
+/*
+ * 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.registry.metadata;
+
+import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.flow.VersionedFlow;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
+import org.apache.nifi.registry.metadata.generated.Buckets;
+import org.apache.nifi.registry.metadata.generated.Flow;
+import org.apache.nifi.registry.metadata.generated.Flows;
+import org.apache.nifi.registry.metadata.generated.Metadata;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Provides atomic access to the Metadata object.
+ */
+public class MetadataHolder {
+
+    private final Metadata metadata;
+    private final Map<String,Bucket> bucketsById;
+    private final Map<String,VersionedFlow> flowsById;
+    private final Map<String,Set<VersionedFlow>> flowsByBucket;
+
+    public MetadataHolder(final Metadata metadata) {
+        this.metadata = metadata;
+        this.bucketsById = 
Collections.unmodifiableMap(createBucketsBydId(metadata));
+        this.flowsByBucket = 
Collections.unmodifiableMap(createFlowsByBucket(metadata));
+        this.flowsById = 
Collections.unmodifiableMap(createFlowsById(flowsByBucket));
+    }
+
+    private Map<String,Bucket> createBucketsBydId(final Metadata metadata) {
+        final Map<String,Bucket> bucketsById = new HashMap<>();
+
+        final Buckets buckets = metadata.getBuckets();
+        if (buckets != null) {
+            buckets.getBucket().stream().forEach(b -> 
bucketsById.put(b.getIdentifier(), createBucket(b)));
+        }
+
+        return bucketsById;
+    }
+
+    private Bucket createBucket(final 
org.apache.nifi.registry.metadata.generated.Bucket jaxbBucket) {
+        final Bucket bucket = new Bucket();
+        bucket.setIdentifier(jaxbBucket.getIdentifier());
+        bucket.setName(jaxbBucket.getName());
+        bucket.setDescription(jaxbBucket.getDescription());
+        bucket.setCreatedTimestamp(jaxbBucket.getCreatedTimestamp());
+        return bucket;
+    }
+
+    private Map<String,Set<VersionedFlow>> createFlowsByBucket(final Metadata 
metadata) {
+        final Map<String,Set<VersionedFlow>> flowsByBucket = new HashMap<>();
+
+        final Flows flows = metadata.getFlows();
+        if (flows != null) {
+            flows.getFlow().stream().forEach(f -> {
+                Set<VersionedFlow> bucketFLows = 
flowsByBucket.get(f.getBucketIdentifier());
+                if (bucketFLows == null) {
+                    bucketFLows = new HashSet<>();
+                    flowsByBucket.put(f.getBucketIdentifier(), bucketFLows);
+                }
+                bucketFLows.add(createFlow(f));
+            });
+        }
+
+        return flowsByBucket;
+    }
+
+    private VersionedFlow createFlow(final Flow flow) {
+        final VersionedFlow versionedFlow = new VersionedFlow();
+        versionedFlow.setIdentifier(flow.getIdentifier());
+        versionedFlow.setName(flow.getName());
+        versionedFlow.setDescription(flow.getDescription());
+        versionedFlow.setCreatedTimestamp(flow.getCreatedTimestamp());
+        versionedFlow.setModifiedTimestamp(flow.getModifiedTimestamp());
+
+        if (flow.getSnapshot() != null) {
+            flow.getSnapshot().stream().forEach(s -> 
versionedFlow.addVersionedFlowSnapshot(createSnapshot(flow, s)));
+        }
+
+        return versionedFlow;
+    }
+
+    private VersionedFlowSnapshot createSnapshot(final Flow flow, final 
Flow.Snapshot snapshot) {
+        final VersionedFlowSnapshot versionedFlowSnapshot = new 
VersionedFlowSnapshot();
+        versionedFlowSnapshot.setFlowIdentifier(flow.getIdentifier());
+        versionedFlowSnapshot.setFlowName(flow.getName());
+        versionedFlowSnapshot.setVersion(snapshot.getVersion());
+        versionedFlowSnapshot.setComments(snapshot.getComments());
+        versionedFlowSnapshot.setTimestamp(snapshot.getCreatedTimestamp());
+        return versionedFlowSnapshot;
+    }
+
+    private Map<String,VersionedFlow> createFlowsById(final 
Map<String,Set<VersionedFlow>> flowsByBucket) {
+        final Map<String,VersionedFlow> flowsBdId = new HashMap<>();
+
+        for (final Map.Entry<String,Set<VersionedFlow>> entry : 
flowsByBucket.entrySet()) {
+            for (final VersionedFlow flow : entry.getValue()) {
+                flowsBdId.put(flow.getIdentifier(), flow);
+            }
+        }
+
+        return flowsBdId;
+    }
+
+    public Metadata getMetadata() {
+        return metadata;
+    }
+
+    public Map<String,Bucket> getBucketsBydId() {
+        return bucketsById;
+    }
+
+    public Map<String,VersionedFlow> getFlowsById() {
+        return flowsById;
+    }
+
+    public Map<String,Set<VersionedFlow>> getFlowsByBucket() {
+        return flowsByBucket;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/9eb0cef0/nifi-registry-provider-impl/src/main/xsd/metadata.xsd
----------------------------------------------------------------------
diff --git a/nifi-registry-provider-impl/src/main/xsd/metadata.xsd 
b/nifi-registry-provider-impl/src/main/xsd/metadata.xsd
new file mode 100644
index 0000000..7f71745
--- /dev/null
+++ b/nifi-registry-provider-impl/src/main/xsd/metadata.xsd
@@ -0,0 +1,139 @@
+<?xml version="1.0"?>
+<!--
+  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.
+-->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema";>
+
+    <!-- Buckets -->
+
+    <xs:complexType name="Bucket">
+        <xs:sequence>
+            <xs:element name="description" type="xs:string" minOccurs="0" 
maxOccurs="1"/>
+        </xs:sequence>
+        <xs:attribute name="identifier">
+            <xs:simpleType>
+                <xs:restriction base="xs:string">
+                    <xs:minLength value="1"/>
+                    <xs:pattern value=".*[^\s].*"/>
+                </xs:restriction>
+            </xs:simpleType>
+        </xs:attribute>
+        <xs:attribute name="name">
+            <xs:simpleType>
+                <xs:restriction base="xs:string">
+                    <xs:minLength value="1"/>
+                    <xs:pattern value=".*[^\s].*"/>
+                </xs:restriction>
+            </xs:simpleType>
+        </xs:attribute>
+        <xs:attribute name="createdTimestamp">
+            <xs:simpleType>
+                <xs:restriction base="xs:long">
+                    <xs:minInclusive value="1"/>
+                </xs:restriction>
+            </xs:simpleType>
+        </xs:attribute>
+    </xs:complexType>
+
+    <xs:complexType name="Buckets">
+        <xs:sequence>
+            <xs:element name="bucket" type="Bucket" minOccurs="0" 
maxOccurs="unbounded"/>
+        </xs:sequence>
+    </xs:complexType>
+
+    <!-- Flows -->
+
+    <xs:complexType name="Flow">
+        <xs:sequence>
+            <xs:element name="description" type="xs:string" minOccurs="0" 
maxOccurs="1"/>
+            <xs:element name="snapshot" minOccurs="0" maxOccurs="unbounded" >
+                <xs:complexType>
+                    <xs:sequence>
+                        <xs:element name="comments" type="xs:string" 
minOccurs="0" maxOccurs="1"/>
+                    </xs:sequence>
+                    <xs:attribute name="version">
+                        <xs:simpleType>
+                            <xs:restriction base="xs:int">
+                                <xs:minInclusive value="1"/>
+                            </xs:restriction>
+                        </xs:simpleType>
+                    </xs:attribute>
+                    <xs:attribute name="createdTimestamp">
+                        <xs:simpleType>
+                            <xs:restriction base="xs:long">
+                                <xs:minInclusive value="1"/>
+                            </xs:restriction>
+                        </xs:simpleType>
+                    </xs:attribute>
+                </xs:complexType>
+            </xs:element>
+        </xs:sequence>
+        <xs:attribute name="identifier">
+            <xs:simpleType>
+                <xs:restriction base="xs:string">
+                    <xs:minLength value="1"/>
+                    <xs:pattern value=".*[^\s].*"/>
+                </xs:restriction>
+            </xs:simpleType>
+        </xs:attribute>
+        <xs:attribute name="name">
+            <xs:simpleType>
+                <xs:restriction base="xs:string">
+                    <xs:minLength value="1"/>
+                    <xs:pattern value=".*[^\s].*"/>
+                </xs:restriction>
+            </xs:simpleType>
+        </xs:attribute>
+        <xs:attribute name="createdTimestamp">
+            <xs:simpleType>
+                <xs:restriction base="xs:long">
+                    <xs:minInclusive value="1"/>
+                </xs:restriction>
+            </xs:simpleType>
+        </xs:attribute>
+        <xs:attribute name="modifiedTimestamp">
+            <xs:simpleType>
+                <xs:restriction base="xs:long">
+                    <xs:minInclusive value="1"/>
+                </xs:restriction>
+            </xs:simpleType>
+        </xs:attribute>
+        <xs:attribute name="bucketIdentifier">
+            <xs:simpleType>
+                <xs:restriction base="xs:string">
+                    <xs:minLength value="1"/>
+                    <xs:pattern value=".*[^\s].*"/>
+                </xs:restriction>
+            </xs:simpleType>
+        </xs:attribute>
+    </xs:complexType>
+
+    <xs:complexType name="Flows">
+        <xs:sequence>
+            <xs:element name="flow" type="Flow" minOccurs="0" 
maxOccurs="unbounded" />
+        </xs:sequence>
+    </xs:complexType>
+
+    <!-- Metadata -->
+
+    <xs:element name="metadata">
+        <xs:complexType>
+            <xs:sequence>
+                <xs:element name="buckets" type="Buckets" minOccurs="0" 
maxOccurs="1"/>
+                <xs:element name="flows" type="Flows" minOccurs="0" 
maxOccurs="1" />
+            </xs:sequence>
+        </xs:complexType>
+    </xs:element>
+
+</xs:schema>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/9eb0cef0/nifi-registry-provider-impl/src/test/java/org/apache/nifi/registry/flow/TestFileSystemFlowPersistenceProvider.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-provider-impl/src/test/java/org/apache/nifi/registry/flow/TestFileSystemFlowPersistenceProvider.java
 
b/nifi-registry-provider-impl/src/test/java/org/apache/nifi/registry/flow/TestFileSystemFlowPersistenceProvider.java
new file mode 100644
index 0000000..aa0bf58
--- /dev/null
+++ 
b/nifi-registry-provider-impl/src/test/java/org/apache/nifi/registry/flow/TestFileSystemFlowPersistenceProvider.java
@@ -0,0 +1,202 @@
+/*
+ * 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.registry.flow;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.nifi.registry.provider.ProviderConfigurationContext;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.mockito.Mockito.when;
+
+public class TestFileSystemFlowPersistenceProvider {
+
+    static final String FLOW_STORAGE_DIR = "target/flow_storage";
+
+    static final ProviderConfigurationContext CONFIGURATION_CONTEXT = new 
ProviderConfigurationContext() {
+        @Override
+        public Map<String, String> getProperties() {
+            final Map<String,String> props = new HashMap<>();
+            props.put(FileSystemFlowPersistenceProvider.FLOW_STORAGE_DIR_PROP, 
FLOW_STORAGE_DIR);
+            return props;
+        }
+    };
+
+    private File flowStorageDir;
+    private FileSystemFlowPersistenceProvider fileSystemFlowProvider;
+
+    @Before
+    public void setup() throws IOException {
+        flowStorageDir = new File(FLOW_STORAGE_DIR);
+        if (flowStorageDir.exists()) {
+            org.apache.commons.io.FileUtils.cleanDirectory(flowStorageDir);
+            flowStorageDir.delete();
+        }
+
+        Assert.assertFalse(flowStorageDir.exists());
+
+        fileSystemFlowProvider = new FileSystemFlowPersistenceProvider();
+        fileSystemFlowProvider.onConfigured(CONFIGURATION_CONTEXT);
+        Assert.assertTrue(flowStorageDir.exists());
+    }
+
+    @Test
+    public void testSaveSuccessfully() throws IOException {
+        createAndSaveSnapshot(fileSystemFlowProvider,"bucket1", "flow1", 1, 
"flow1v1");
+        verifySnapshot(flowStorageDir, "bucket1", "flow1", 1, "flow1v1");
+
+        createAndSaveSnapshot(fileSystemFlowProvider,"bucket1", "flow1", 2, 
"flow1v2");
+        verifySnapshot(flowStorageDir, "bucket1", "flow1", 2, "flow1v2");
+
+        createAndSaveSnapshot(fileSystemFlowProvider,"bucket1", "flow2", 1, 
"flow2v1");
+        verifySnapshot(flowStorageDir, "bucket1", "flow2", 1, "flow2v1");
+
+        createAndSaveSnapshot(fileSystemFlowProvider,"bucket2", "flow3", 1, 
"flow3v1");
+        verifySnapshot(flowStorageDir, "bucket2", "flow3", 1, "flow3v1");
+    }
+
+    @Test
+    public void testSaveWithExistingVersion() throws IOException {
+        final FlowSnapshotContext context = 
Mockito.mock(FlowSnapshotContext.class);
+        when(context.getBucketId()).thenReturn("bucket1");
+        when(context.getFlowId()).thenReturn("flow1");
+        when(context.getVersion()).thenReturn(1);
+
+        final byte[] content = "flow1v1".getBytes(StandardCharsets.UTF_8);
+        fileSystemFlowProvider.saveSnapshot(context, content);
+
+        // save new content for an existing version
+        final byte[] content2 = "XXX".getBytes(StandardCharsets.UTF_8);
+        try {
+            fileSystemFlowProvider.saveSnapshot(context, content2);
+            Assert.fail("Should have thrown exception");
+        } catch (Exception e) {
+
+        }
+
+        // verify the new content wasn't written
+        final File flowSnapshotFile = new File(flowStorageDir, 
"bucket1/flow1/1/1" + FileSystemFlowPersistenceProvider.SNAPSHOT_EXTENSION);
+        try (InputStream in = new FileInputStream(flowSnapshotFile)) {
+            Assert.assertEquals("flow1v1", IOUtils.toString(in, 
StandardCharsets.UTF_8));
+        }
+    }
+
+    @Test
+    public void testSaveAndGet() throws IOException {
+        createAndSaveSnapshot(fileSystemFlowProvider,"bucket1", "flow1", 1, 
"flow1v1");
+        createAndSaveSnapshot(fileSystemFlowProvider,"bucket1", "flow1", 2, 
"flow1v2");
+
+        final byte[] flow1v1 = fileSystemFlowProvider.getSnapshot("bucket1", 
"flow1", 1);
+        Assert.assertEquals("flow1v1", new String(flow1v1, 
StandardCharsets.UTF_8));
+
+        final byte[] flow1v2 = fileSystemFlowProvider.getSnapshot("bucket1", 
"flow1", 2);
+        Assert.assertEquals("flow1v2", new String(flow1v2, 
StandardCharsets.UTF_8));
+    }
+
+    @Test
+    public void testGetWhenDoesNotExist() {
+        final byte[] flow1v1 = fileSystemFlowProvider.getSnapshot("bucket1", 
"flow1", 1);
+        Assert.assertNull(flow1v1);
+    }
+
+    @Test
+    public void testDeleteSnapshots() throws IOException {
+        final String bucketId = "bucket1";
+        final String flowId = "flow1";
+
+        createAndSaveSnapshot(fileSystemFlowProvider, bucketId, flowId, 1, 
"flow1v1");
+        createAndSaveSnapshot(fileSystemFlowProvider, bucketId, flowId, 2, 
"flow1v2");
+
+        Assert.assertNotNull(fileSystemFlowProvider.getSnapshot(bucketId, 
flowId, 1));
+        Assert.assertNotNull(fileSystemFlowProvider.getSnapshot(bucketId, 
flowId, 2));
+
+        fileSystemFlowProvider.deleteSnapshots(bucketId, flowId);
+
+        Assert.assertNull(fileSystemFlowProvider.getSnapshot(bucketId, flowId, 
1));
+        Assert.assertNull(fileSystemFlowProvider.getSnapshot(bucketId, flowId, 
2));
+
+        // delete a flow that doesn't exist
+        fileSystemFlowProvider.deleteSnapshots(bucketId, "some-other-flow");
+
+        // delete a bucket that doesn't exist
+        fileSystemFlowProvider.deleteSnapshots("some-other-bucket", flowId);
+    }
+
+    @Test
+    public void testDeleteSnapshot() throws IOException {
+        final String bucketId = "bucket1";
+        final String flowId = "flow1";
+
+        createAndSaveSnapshot(fileSystemFlowProvider, bucketId, flowId, 1, 
"flow1v1");
+        createAndSaveSnapshot(fileSystemFlowProvider, bucketId, flowId, 2, 
"flow1v2");
+
+        Assert.assertNotNull(fileSystemFlowProvider.getSnapshot(bucketId, 
flowId, 1));
+        Assert.assertNotNull(fileSystemFlowProvider.getSnapshot(bucketId, 
flowId, 2));
+
+        fileSystemFlowProvider.deleteSnapshot(bucketId, flowId, 1);
+
+        Assert.assertNull(fileSystemFlowProvider.getSnapshot(bucketId, flowId, 
1));
+        Assert.assertNotNull(fileSystemFlowProvider.getSnapshot(bucketId, 
flowId, 2));
+
+        fileSystemFlowProvider.deleteSnapshot(bucketId, flowId, 2);
+
+        Assert.assertNull(fileSystemFlowProvider.getSnapshot(bucketId, flowId, 
1));
+        Assert.assertNull(fileSystemFlowProvider.getSnapshot(bucketId, flowId, 
2));
+
+        // delete a version that doesn't exist
+        fileSystemFlowProvider.deleteSnapshot(bucketId, flowId, 3);
+
+        // delete a flow that doesn't exist
+        fileSystemFlowProvider.deleteSnapshot(bucketId, "some-other-flow", 1);
+
+        // delete a bucket that doesn't exist
+        fileSystemFlowProvider.deleteSnapshot("some-other-bucket", flowId, 1);
+    }
+
+    private void createAndSaveSnapshot(final FlowPersistenceProvider 
flowPersistenceProvider, final String bucketId, final String flowId, final int 
version,
+                                       final String contentString) throws 
IOException {
+        final FlowSnapshotContext context = 
Mockito.mock(FlowSnapshotContext.class);
+        when(context.getBucketId()).thenReturn(bucketId);
+        when(context.getFlowId()).thenReturn(flowId);
+        when(context.getVersion()).thenReturn(version);
+
+        final byte[] content = contentString.getBytes(StandardCharsets.UTF_8);
+        flowPersistenceProvider.saveSnapshot(context, content);
+    }
+
+    private void verifySnapshot(final File flowStorageDir, final String 
bucketId, final String flowId, final int version,
+                                final String contentString) throws IOException 
{
+        // verify the correct snapshot file was created
+        final File flowSnapshotFile = new File(flowStorageDir,
+                bucketId + "/" + flowId + "/" + version + "/" + version + 
FileSystemFlowPersistenceProvider.SNAPSHOT_EXTENSION);
+        Assert.assertTrue(flowSnapshotFile.exists());
+
+        try (InputStream in = new FileInputStream(flowSnapshotFile)) {
+            Assert.assertEquals(contentString, IOUtils.toString(in, 
StandardCharsets.UTF_8));
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/9eb0cef0/nifi-registry-provider-impl/src/test/java/org/apache/nifi/registry/metadata/TestFileSystemMetadataProvider.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-provider-impl/src/test/java/org/apache/nifi/registry/metadata/TestFileSystemMetadataProvider.java
 
b/nifi-registry-provider-impl/src/test/java/org/apache/nifi/registry/metadata/TestFileSystemMetadataProvider.java
new file mode 100644
index 0000000..a0d6eb5
--- /dev/null
+++ 
b/nifi-registry-provider-impl/src/test/java/org/apache/nifi/registry/metadata/TestFileSystemMetadataProvider.java
@@ -0,0 +1,454 @@
+/*
+ * 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.registry.metadata;
+
+import org.apache.nifi.registry.bucket.Bucket;
+import org.apache.nifi.registry.flow.VersionedFlow;
+import org.apache.nifi.registry.flow.VersionedFlowSnapshot;
+import org.apache.nifi.registry.provider.ProviderConfigurationContext;
+import org.apache.nifi.registry.provider.ProviderCreationException;
+import org.apache.nifi.registry.util.FileUtils;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+public class TestFileSystemMetadataProvider {
+
+    static final String METADATA_EMPTY = "metadata-empty.xml";
+    static final String METADATA_EMPTY_CONCISE = "metadata-empty-concise.xml";
+    static final String METADATA_EXISTING = "metadata-existing.xml";
+    static final String METADATA_NEW_FILE = "metadata-test.xml";
+
+    static final File METADATA_SRC_DIR = new 
File("src/test/resources/metadata");
+    static final File METADATA_DEST_DIR = new File("target/metadata");
+
+    static final File METADATA_SRC_EMPTY = new File(METADATA_SRC_DIR, 
METADATA_EMPTY);
+    static final File METADATA_SRC_EMPTY_CONCISE = new File(METADATA_SRC_DIR, 
METADATA_EMPTY_CONCISE);
+    static final File METADATA_SRC_EXISTING = new File(METADATA_SRC_DIR, 
METADATA_EXISTING);
+
+    static final File METADATA_DEST_EMPTY = new File(METADATA_DEST_DIR, 
METADATA_EMPTY);
+    static final File METADATA_DEST_EMPTY_CONCISE = new 
File(METADATA_DEST_DIR, METADATA_EMPTY_CONCISE);
+    static final File METADATA_DEST_EXISTING = new File(METADATA_DEST_DIR, 
METADATA_EXISTING);
+    static final File METADATA_DEST_NEW_FILE = new File(METADATA_DEST_DIR, 
METADATA_NEW_FILE);
+
+    private MetadataProvider metadataProvider;
+
+    @Before
+    public void setup() throws IOException {
+        FileUtils.ensureDirectoryExistAndCanReadAndWrite(METADATA_DEST_DIR);
+        org.apache.commons.io.FileUtils.cleanDirectory(METADATA_DEST_DIR);
+
+        Files.copy(METADATA_SRC_EMPTY.toPath(), METADATA_DEST_EMPTY.toPath(), 
StandardCopyOption.REPLACE_EXISTING);
+        Files.copy(METADATA_SRC_EMPTY_CONCISE.toPath(), 
METADATA_DEST_EMPTY_CONCISE.toPath(), StandardCopyOption.REPLACE_EXISTING);
+        Files.copy(METADATA_SRC_EXISTING.toPath(), 
METADATA_DEST_EXISTING.toPath(), StandardCopyOption.REPLACE_EXISTING);
+
+        metadataProvider = new FileSystemMetadataProvider();
+    }
+
+    private ProviderConfigurationContext createConfigContext(final File 
metadataFile) {
+        return () -> {
+            final Map<String,String> props = new HashMap<>();
+            props.put(FileSystemMetadataProvider.METADATA_FILE_PROP, 
metadataFile.getAbsolutePath());
+            return props;
+        };
+    }
+
+    @Test(expected = ProviderCreationException.class)
+    public void testOnConfiguredMissingMetadataFileProperty() {
+        metadataProvider.onConfigured(() -> new HashMap<>());
+    }
+
+    @Test(expected = ProviderCreationException.class)
+    public void testOnConfiguredBlankMetadataFileProperty() {
+        metadataProvider.onConfigured(() -> {
+            final Map<String,String> props = new HashMap<>();
+            props.put(FileSystemMetadataProvider.METADATA_FILE_PROP, "    ");
+            return props;
+        });
+    }
+
+    @Test
+    public void testOnConfiguredWithEmptyMetadata() {
+        
metadataProvider.onConfigured(createConfigContext(METADATA_DEST_EMPTY));
+        assertEquals(0, metadataProvider.getBuckets().size());
+        assertEquals(0, metadataProvider.getFlows().size());
+    }
+
+    @Test
+    public void testOnConfiguredWithEmptyConciseMetadata() {
+        
metadataProvider.onConfigured(createConfigContext(METADATA_DEST_EMPTY_CONCISE));
+        assertEquals(0, metadataProvider.getBuckets().size());
+        assertEquals(0, metadataProvider.getFlows().size());
+    }
+
+    @Test
+    public void testOnConfiguredWithExistingMetadata() {
+        
metadataProvider.onConfigured(createConfigContext(METADATA_DEST_EXISTING));
+        assertEquals(2, metadataProvider.getBuckets().size());
+        assertEquals(1, metadataProvider.getFlows().size());
+
+        final Bucket bucket1 = metadataProvider.getBucket("bucket1");
+        assertNotNull(bucket1);
+        assertEquals("bucket1", bucket1.getIdentifier());
+
+        final Bucket bucket2 = metadataProvider.getBucket("bucket2");
+        assertNotNull(bucket2);
+        assertEquals("bucket2", bucket2.getIdentifier());
+
+        final VersionedFlow versionedFlow = metadataProvider.getFlow("flow1");
+        assertNotNull(versionedFlow);
+        assertEquals("flow1", versionedFlow.getIdentifier());
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testCreateNullBucket() {
+        
metadataProvider.onConfigured(createConfigContext(METADATA_DEST_NEW_FILE));
+        metadataProvider.createBucket(null);
+    }
+
+    @Test
+    public void testCreateBucket() {
+        
metadataProvider.onConfigured(createConfigContext(METADATA_DEST_NEW_FILE));
+
+        final Bucket bucket = new Bucket();
+        bucket.setIdentifier("bucket1");
+        bucket.setName("My Bucket");
+        bucket.setDescription("This is my bucket");
+        bucket.setCreatedTimestamp(System.currentTimeMillis());
+
+        final Bucket returnedBucket = metadataProvider.createBucket(bucket);
+        assertNotNull(returnedBucket);
+        assertEquals(bucket.getIdentifier(), returnedBucket.getIdentifier());
+        assertEquals(bucket.getName(), returnedBucket.getName());
+        assertEquals(bucket.getDescription(), returnedBucket.getDescription());
+        assertEquals(bucket.getCreatedTimestamp(), 
returnedBucket.getCreatedTimestamp());
+    }
+
+    @Test
+    public void testGetBucketExists() {
+        
metadataProvider.onConfigured(createConfigContext(METADATA_DEST_EXISTING));
+        assertEquals(2, metadataProvider.getBuckets().size());
+
+        final Bucket bucket1 = metadataProvider.getBucket("bucket1");
+        assertNotNull(bucket1);
+        assertEquals("bucket1", bucket1.getIdentifier());
+        assertEquals("Bryan's Bucket", bucket1.getName());
+        assertEquals("The description for Bryan's Bucket.", 
bucket1.getDescription());
+        assertEquals(111111111, bucket1.getCreatedTimestamp());
+    }
+
+    @Test
+    public void testGetBucketDoesNotExist() {
+        
metadataProvider.onConfigured(createConfigContext(METADATA_DEST_EXISTING));
+        assertEquals(2, metadataProvider.getBuckets().size());
+
+        final Bucket bucket1 = 
metadataProvider.getBucket("bucket-does-not-exist");
+        assertNull(bucket1);
+    }
+
+    @Test
+    public void testUpdateBucketExists() {
+        
metadataProvider.onConfigured(createConfigContext(METADATA_DEST_EXISTING));
+        assertEquals(2, metadataProvider.getBuckets().size());
+
+        final Bucket bucket = metadataProvider.getBucket("bucket1");
+        assertNotNull(bucket);
+
+        bucket.setName("New Name");
+        bucket.setDescription("New Description");
+
+        final Bucket updatedBucket = metadataProvider.updateBucket(bucket);
+        assertNotNull(updatedBucket);
+        assertEquals("New Name", updatedBucket.getName());
+        assertEquals("New Description", updatedBucket.getDescription());
+    }
+
+    @Test
+    public void testUpdateBucketDoesNotExist() {
+        
metadataProvider.onConfigured(createConfigContext(METADATA_DEST_EXISTING));
+        assertEquals(2, metadataProvider.getBuckets().size());
+
+        final Bucket bucket = new Bucket();
+        bucket.setIdentifier("does-not-exist");
+        bucket.setName("My Bucket");
+        bucket.setDescription("This is my bucket");
+        bucket.setCreatedTimestamp(System.currentTimeMillis());
+
+        final Bucket updatedBucket = metadataProvider.updateBucket(bucket);
+        assertNull(updatedBucket);
+    }
+
+    @Test
+    public void testDeleteBucketWithFlows() {
+        
metadataProvider.onConfigured(createConfigContext(METADATA_DEST_EXISTING));
+        assertEquals(2, metadataProvider.getBuckets().size());
+
+        final String bucketId = "bucket1";
+        assertNotNull(metadataProvider.getBucket(bucketId));
+
+        final Set<VersionedFlow> bucketFlows = 
metadataProvider.getFlows(bucketId);
+        assertNotNull(bucketFlows);
+        assertEquals(1, bucketFlows.size());
+
+        metadataProvider.deleteBucket(bucketId);
+        assertNull(metadataProvider.getBucket(bucketId));
+
+        final Set<VersionedFlow> bucketFlows2 = 
metadataProvider.getFlows(bucketId);
+        assertNotNull(bucketFlows2);
+        assertEquals(0, bucketFlows2.size());
+    }
+
+    @Test
+    public void testDeleteBucketDoesNotExist() {
+        
metadataProvider.onConfigured(createConfigContext(METADATA_DEST_EXISTING));
+        assertEquals(2, metadataProvider.getBuckets().size());
+
+        final String bucketId = "bucket-does-not-exist";
+        metadataProvider.deleteBucket(bucketId);
+
+        assertEquals(2, metadataProvider.getBuckets().size());
+    }
+
+    @Test
+    public void testCreateFlow() {
+        
metadataProvider.onConfigured(createConfigContext(METADATA_DEST_EXISTING));
+        assertEquals(2, metadataProvider.getBuckets().size());
+
+        // verify bucket2 exists and has no flows
+        final Bucket bucket2 = metadataProvider.getBucket("bucket2");
+        assertNotNull(bucket2);
+        assertEquals(0, 
metadataProvider.getFlows(bucket2.getIdentifier()).size());
+
+        final VersionedFlow versionedFlow = new VersionedFlow();
+        versionedFlow.setIdentifier("flow2");
+        versionedFlow.setName("New Flow");
+        versionedFlow.setDescription("This is a new flow");
+        versionedFlow.setCreatedTimestamp(System.currentTimeMillis());
+        versionedFlow.setModifiedTimestamp(System.currentTimeMillis());
+
+        final VersionedFlow createdFlow = 
metadataProvider.createFlow(bucket2.getIdentifier(), versionedFlow);
+        assertNotNull(createdFlow);
+        assertEquals(versionedFlow.getIdentifier(), 
createdFlow.getIdentifier());
+        assertEquals(versionedFlow.getName(), createdFlow.getName());
+        assertEquals(versionedFlow.getDescription(), 
createdFlow.getDescription());
+        assertEquals(versionedFlow.getCreatedTimestamp(), 
createdFlow.getCreatedTimestamp());
+        assertEquals(versionedFlow.getModifiedTimestamp(), 
createdFlow.getModifiedTimestamp());
+
+        assertEquals(1, 
metadataProvider.getFlows(bucket2.getIdentifier()).size());
+    }
+
+    @Test
+    public void testGetFlowExists() {
+        
metadataProvider.onConfigured(createConfigContext(METADATA_DEST_EXISTING));
+
+        final VersionedFlow versionedFlow = metadataProvider.getFlow("flow1");
+        assertNotNull(versionedFlow);
+        assertEquals("flow1", versionedFlow.getIdentifier());
+        assertEquals("Bryan's Flow", versionedFlow.getName());
+        assertEquals("The description for Bryan's Flow.", 
versionedFlow.getDescription());
+        assertEquals(333333333, versionedFlow.getCreatedTimestamp());
+        assertEquals(444444444, versionedFlow.getModifiedTimestamp());
+        assertEquals(3, versionedFlow.getSnapshots().size());
+    }
+
+    @Test
+    public void testGetFlowDoesNotExist() {
+        
metadataProvider.onConfigured(createConfigContext(METADATA_DEST_EXISTING));
+
+        final VersionedFlow versionedFlow = 
metadataProvider.getFlow("flow-does-not-exist");
+        assertNull(versionedFlow);
+    }
+
+    @Test
+    public void testUpdateFlowExists() {
+        
metadataProvider.onConfigured(createConfigContext(METADATA_DEST_EXISTING));
+
+        final VersionedFlow versionedFlow = metadataProvider.getFlow("flow1");
+        assertNotNull(versionedFlow);
+
+        final String newFlowName = "New Flow Name";
+        final String newFlowDescription = "New Flow Description";
+        assertNotEquals(versionedFlow.getName(), newFlowName);
+        assertNotEquals(versionedFlow.getDescription(), newFlowDescription);
+
+        versionedFlow.setName("New Flow Name");
+        versionedFlow.setDescription("New Flow Description");
+
+        final VersionedFlow updatedFlow = 
metadataProvider.updateFlow(versionedFlow);
+        assertEquals(newFlowName, updatedFlow.getName());
+        assertEquals(newFlowDescription, updatedFlow.getDescription());
+        assertEquals(versionedFlow.getCreatedTimestamp(), 
updatedFlow.getCreatedTimestamp());
+        assertTrue(updatedFlow.getModifiedTimestamp() > 
versionedFlow.getModifiedTimestamp());
+    }
+
+    @Test
+    public void testUpdateFlowDoesNotExist() {
+        
metadataProvider.onConfigured(createConfigContext(METADATA_DEST_EXISTING));
+
+        final VersionedFlow versionedFlow = new VersionedFlow();
+        versionedFlow.setIdentifier("does-not-exist");
+        versionedFlow.setName("Does Not Exist");
+        versionedFlow.setDescription("Does Not Exist");
+        versionedFlow.setCreatedTimestamp(System.currentTimeMillis());
+        versionedFlow.setModifiedTimestamp(System.currentTimeMillis());
+
+        final VersionedFlow updatedFlow = 
metadataProvider.updateFlow(versionedFlow);
+        assertNull(updatedFlow);
+    }
+
+    @Test
+    public void testDeleteFlowWithSnapshots() {
+        
metadataProvider.onConfigured(createConfigContext(METADATA_DEST_EXISTING));
+
+        final VersionedFlow versionedFlow = metadataProvider.getFlow("flow1");
+        assertNotNull(versionedFlow);
+        assertNotNull(versionedFlow.getSnapshots());
+        assertTrue(versionedFlow.getSnapshots().size() > 0);
+
+        metadataProvider.deleteFlow(versionedFlow.getIdentifier());
+
+        final VersionedFlow deletedFlow = metadataProvider.getFlow("flow1");
+        assertNull(deletedFlow);
+    }
+
+    @Test
+    public void testDeleteFlowDoesNotExist() {
+        
metadataProvider.onConfigured(createConfigContext(METADATA_DEST_EXISTING));
+
+        assertEquals(1, metadataProvider.getFlows().size());
+        metadataProvider.deleteFlow("does-not-exist");
+        assertEquals(1, metadataProvider.getFlows().size());
+    }
+
+    @Test
+    public void testCreateFlowSnapshot() {
+        
metadataProvider.onConfigured(createConfigContext(METADATA_DEST_EXISTING));
+
+        final VersionedFlow versionedFlow = metadataProvider.getFlow("flow1");
+        assertNotNull(versionedFlow);
+        assertNotNull(versionedFlow.getSnapshots());
+
+        int lastVersion = 1;
+        for (final VersionedFlowSnapshot snapshot : 
versionedFlow.getSnapshots()) {
+            if (snapshot.getVersion() > lastVersion) {
+                lastVersion = snapshot.getVersion();
+            }
+        }
+
+        final VersionedFlowSnapshot nextSnapshot = new VersionedFlowSnapshot();
+        nextSnapshot.setFlowIdentifier(versionedFlow.getIdentifier());
+        nextSnapshot.setFlowName(versionedFlow.getName());
+        nextSnapshot.setVersion(lastVersion + 1);
+        nextSnapshot.setComments("This is the next snapshot");
+        nextSnapshot.setTimestamp(System.currentTimeMillis());
+
+        final VersionedFlowSnapshot createdSnapshot = 
metadataProvider.createFlowSnapshot(nextSnapshot);
+        assertEquals(nextSnapshot.getFlowIdentifier(), 
createdSnapshot.getFlowIdentifier());
+        assertEquals(nextSnapshot.getFlowName(), 
createdSnapshot.getFlowName());
+        assertEquals(nextSnapshot.getVersion(), createdSnapshot.getVersion());
+        assertEquals(nextSnapshot.getComments(), 
createdSnapshot.getComments());
+        assertEquals(nextSnapshot.getTimestamp(), 
createdSnapshot.getTimestamp());
+
+        final VersionedFlow updatedFlow = metadataProvider.getFlow("flow1");
+        assertNotNull(updatedFlow);
+        assertNotNull(updatedFlow.getSnapshots());
+        assertEquals(updatedFlow.getSnapshots().size(), 
versionedFlow.getSnapshots().size() + 1);
+    }
+
+    @Test
+    public void testGetFlowSnapshotExists() {
+        
metadataProvider.onConfigured(createConfigContext(METADATA_DEST_EXISTING));
+
+        final VersionedFlowSnapshot snapshot = 
metadataProvider.getFlowSnapshot("flow1", 1);
+        assertNotNull(snapshot);
+        assertEquals("flow1", snapshot.getFlowIdentifier());
+        assertEquals("Bryan's Flow", snapshot.getFlowName());
+        assertEquals(1, snapshot.getVersion());
+        assertEquals(555555555, snapshot.getTimestamp());
+        assertEquals("These are the comments for snapshot #1.", 
snapshot.getComments());
+    }
+
+    @Test
+    public void testGetFlowSnapshotNameDoesNotExist() {
+        
metadataProvider.onConfigured(createConfigContext(METADATA_DEST_EXISTING));
+
+        final VersionedFlowSnapshot snapshot = 
metadataProvider.getFlowSnapshot("does-not-exist", 1);
+        assertNull(snapshot);
+    }
+
+    @Test
+    public void testGetFlowSnapshotVersionDoesNotExist() {
+        
metadataProvider.onConfigured(createConfigContext(METADATA_DEST_EXISTING));
+
+        final VersionedFlowSnapshot snapshot = 
metadataProvider.getFlowSnapshot("flow1", Integer.MAX_VALUE);
+        assertNull(snapshot);
+    }
+
+    @Test
+    public void testDeleteSnapshotExists() {
+        
metadataProvider.onConfigured(createConfigContext(METADATA_DEST_EXISTING));
+
+        final VersionedFlow versionedFlow = metadataProvider.getFlow("flow1");
+        assertNotNull(versionedFlow);
+        assertNotNull(versionedFlow.getSnapshots());
+        assertEquals(3, versionedFlow.getSnapshots().size());
+
+        final VersionedFlowSnapshot firstSnapshot = 
versionedFlow.getSnapshots().stream().findFirst().orElse(null);
+        assertNotNull(firstSnapshot);
+
+        metadataProvider.deleteFlowSnapshot(versionedFlow.getIdentifier(), 
firstSnapshot.getVersion());
+
+        final VersionedFlow updatedFlow = metadataProvider.getFlow("flow1");
+        assertNotNull(updatedFlow);
+        assertNotNull(updatedFlow.getSnapshots());
+        assertEquals(2, updatedFlow.getSnapshots().size());
+
+        final VersionedFlowSnapshot deletedSnapshot = 
updatedFlow.getSnapshots().stream()
+                .filter(s -> s.getVersion() == 
firstSnapshot.getVersion()).findFirst().orElse(null);
+        assertNull(deletedSnapshot);
+    }
+
+    @Test
+    public void testDeleteSnapshotDoesNotExist() {
+        
metadataProvider.onConfigured(createConfigContext(METADATA_DEST_EXISTING));
+
+        final VersionedFlow versionedFlow = metadataProvider.getFlow("flow1");
+        assertNotNull(versionedFlow);
+        assertNotNull(versionedFlow.getSnapshots());
+        assertEquals(3, versionedFlow.getSnapshots().size());
+
+        metadataProvider.deleteFlowSnapshot(versionedFlow.getIdentifier(), 
Integer.MAX_VALUE);
+
+        final VersionedFlow updatedFlow = metadataProvider.getFlow("flow1");
+        assertNotNull(updatedFlow);
+        assertNotNull(updatedFlow.getSnapshots());
+        assertEquals(3, updatedFlow.getSnapshots().size());
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/9eb0cef0/nifi-registry-provider-impl/src/test/resources/metadata/metadata-empty-concise.xml
----------------------------------------------------------------------
diff --git 
a/nifi-registry-provider-impl/src/test/resources/metadata/metadata-empty-concise.xml
 
b/nifi-registry-provider-impl/src/test/resources/metadata/metadata-empty-concise.xml
new file mode 100644
index 0000000..8f4f4f9
--- /dev/null
+++ 
b/nifi-registry-provider-impl/src/test/resources/metadata/metadata-empty-concise.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0"?>
+<!--
+  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.
+-->
+<metadata />

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/9eb0cef0/nifi-registry-provider-impl/src/test/resources/metadata/metadata-empty.xml
----------------------------------------------------------------------
diff --git 
a/nifi-registry-provider-impl/src/test/resources/metadata/metadata-empty.xml 
b/nifi-registry-provider-impl/src/test/resources/metadata/metadata-empty.xml
new file mode 100644
index 0000000..5af6f87
--- /dev/null
+++ b/nifi-registry-provider-impl/src/test/resources/metadata/metadata-empty.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+<!--
+  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.
+-->
+<metadata>
+</metadata>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/9eb0cef0/nifi-registry-provider-impl/src/test/resources/metadata/metadata-existing.xml
----------------------------------------------------------------------
diff --git 
a/nifi-registry-provider-impl/src/test/resources/metadata/metadata-existing.xml 
b/nifi-registry-provider-impl/src/test/resources/metadata/metadata-existing.xml
new file mode 100644
index 0000000..29db524
--- /dev/null
+++ 
b/nifi-registry-provider-impl/src/test/resources/metadata/metadata-existing.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0"?>
+<!--
+  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.
+-->
+<metadata>
+    <buckets>
+        <bucket identifier="bucket1" name="Bryan's Bucket" 
createdTimestamp="111111111">
+            <description>The description for Bryan's Bucket.</description>
+        </bucket>
+        <bucket identifier="bucket2" name="Matt's Bucket" 
createdTimestamp="222222222" />
+    </buckets>
+    <flows>
+        <flow identifier="flow1" name="Bryan's Flow" 
createdTimestamp="333333333" modifiedTimestamp="444444444" 
bucketIdentifier="bucket1">
+            <description>The description for Bryan's Flow.</description>
+            <snapshot version="1" createdTimestamp="555555555">
+                <comments>These are the comments for snapshot #1.</comments>
+            </snapshot>
+            <snapshot version="2" createdTimestamp="666666666" />
+            <snapshot version="3" createdTimestamp="777777777">
+                <comments>These are the comments for snapshot #3.</comments>
+            </snapshot>
+        </flow>
+    </flows>
+</metadata>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/9eb0cef0/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties
----------------------------------------------------------------------
diff --git 
a/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties 
b/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties
index 156db2e..a748f2f 100644
--- a/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties
+++ b/nifi-registry-resources/src/main/resources/conf/nifi-registry.properties
@@ -32,3 +32,6 @@ 
nifi.registry.security.truststoreType=${nifi.registry.security.truststoreType}
 
nifi.registry.security.truststorePasswd=${nifi.registry.security.truststorePasswd}
 nifi.registry.security.needClientAuth=${nifi.registry.security.needClientAuth}
 
nifi.registry.security.authorized.users=${nifi.registry.security.authorized.users}
+
+# providers properties #
+nifi.registry.providers.configuration.file=${nifi.registry.providers.configuration.file}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/9eb0cef0/nifi-registry-resources/src/main/resources/conf/providers.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-resources/src/main/resources/conf/providers.xml 
b/nifi-registry-resources/src/main/resources/conf/providers.xml
new file mode 100644
index 0000000..450eecb
--- /dev/null
+++ b/nifi-registry-resources/src/main/resources/conf/providers.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  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.
+-->
+<providers>
+
+    <metadataProvider>
+        
<class>org.apache.nifi.registry.metadata.FileSystemMetadataProvider</class>
+        <property name="Metadata File">./metadata.xml</property>
+    </metadataProvider>
+
+    <flowPersistenceProvider>
+        
<class>org.apache.nifi.registry.flow.FileSystemFlowPersistenceProvider</class>
+        <property name="Flow Storage Directory">./flow_storage</property>
+    </flowPersistenceProvider>
+
+</providers>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/9eb0cef0/nifi-registry-service-api/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-service-api/pom.xml 
b/nifi-registry-service-api/pom.xml
deleted file mode 100644
index dc0e8bd..0000000
--- a/nifi-registry-service-api/pom.xml
+++ /dev/null
@@ -1,35 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-  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.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
-         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
-    <parent>
-        <groupId>org.apache.nifi.registry</groupId>
-        <artifactId>nifi-registry</artifactId>
-        <version>0.0.1-SNAPSHOT</version>
-    </parent>
-    <modelVersion>4.0.0</modelVersion>
-    <artifactId>nifi-registry-service-api</artifactId>
-    <packaging>jar</packaging>
-
-    <dependencies>
-        <dependency>
-            <groupId>junit</groupId>
-            <artifactId>junit</artifactId>
-            <version>4.12</version>
-            <scope>test</scope>
-        </dependency>
-    </dependencies>
-</project>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/9eb0cef0/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/StringUtils.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/StringUtils.java
 
b/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/StringUtils.java
new file mode 100644
index 0000000..43ab825
--- /dev/null
+++ 
b/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/StringUtils.java
@@ -0,0 +1,99 @@
+/*
+ * 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.registry.util;
+
+import java.util.Collection;
+
+public class StringUtils {
+
+    public static final String EMPTY = "";
+
+    public static boolean isBlank(final String str) {
+        if (str == null || str.isEmpty()) {
+            return true;
+        }
+        for (int i = 0; i < str.length(); i++) {
+            if (!Character.isWhitespace(str.charAt(i))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public static boolean isEmpty(final String str) {
+        return str == null || str.isEmpty();
+    }
+
+    public static boolean startsWith(final String str, final String prefix) {
+        if (str == null || prefix == null) {
+            return (str == null && prefix == null);
+        }
+        if (prefix.length() > str.length()) {
+            return false;
+        }
+        return str.regionMatches(false, 0, prefix, 0, prefix.length());
+    }
+
+    public static String substringAfter(final String str, final String 
separator) {
+        if (isEmpty(str)) {
+            return str;
+        }
+        if (separator == null) {
+            return EMPTY;
+        }
+        int pos = str.indexOf(separator);
+        if (pos == -1) {
+            return EMPTY;
+        }
+        return str.substring(pos + separator.length());
+    }
+
+    public static String join(final Collection collection, String delimiter) {
+        if (collection == null || collection.size() == 0) {
+            return EMPTY;
+        }
+        final StringBuilder sb = new StringBuilder(collection.size() * 16);
+        for (Object element : collection) {
+            sb.append((String) element);
+            sb.append(delimiter);
+        }
+        return sb.toString().substring(0, sb.lastIndexOf(delimiter));
+    }
+
+    public static String padLeft(final String source, int length, char 
padding) {
+        if (source != null) {
+            StringBuilder sb = new StringBuilder(source).reverse();
+            while (sb.length() < length) {
+                sb.append(padding);
+            }
+            return sb.reverse().toString();
+        }
+        return null;
+    }
+
+    public static String padRight(final String source, int length, char 
padding) {
+        if (source != null) {
+            StringBuilder sb = new StringBuilder(source);
+            while (sb.length() < length) {
+                sb.append(padding);
+            }
+            return sb.toString();
+        }
+        return null;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/9eb0cef0/nifi-registry-web-api/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-web-api/pom.xml b/nifi-registry-web-api/pom.xml
index b8925f5..d799c23 100644
--- a/nifi-registry-web-api/pom.xml
+++ b/nifi-registry-web-api/pom.xml
@@ -91,7 +91,12 @@
         </dependency>
         <dependency>
             <groupId>org.apache.nifi.registry</groupId>
-            <artifactId>nifi-registry-service-api</artifactId>
+            <artifactId>nifi-registry-framework</artifactId>
+            <version>0.0.1-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-provider-impl</artifactId>
             <version>0.0.1-SNAPSHOT</version>
         </dependency>
         <dependency>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/9eb0cef0/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/NiFiRegistryResourceConfig.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/NiFiRegistryResourceConfig.java
 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/NiFiRegistryResourceConfig.java
index f8295f3..e2fabdc 100644
--- 
a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/NiFiRegistryResourceConfig.java
+++ 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/NiFiRegistryResourceConfig.java
@@ -16,6 +16,11 @@
  */
 package org.apache.nifi.registry.web;
 
+import org.apache.nifi.registry.flow.FlowPersistenceProvider;
+import org.apache.nifi.registry.metadata.MetadataProvider;
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.apache.nifi.registry.provider.ProviderFactory;
+import org.apache.nifi.registry.provider.StandardProviderFactory;
 import org.apache.nifi.registry.web.api.TestResource;
 import org.apache.nifi.registry.web.mapper.IllegalArgumentExceptionMapper;
 import org.apache.nifi.registry.web.mapper.ThrowableMapper;
@@ -32,6 +37,12 @@ public class NiFiRegistryResourceConfig extends 
ResourceConfig {
     private static final Logger logger = 
LoggerFactory.getLogger(NiFiRegistryResourceConfig.class);
 
     public NiFiRegistryResourceConfig(@Context ServletContext servletContext) {
+        final NiFiRegistryProperties properties = (NiFiRegistryProperties) 
servletContext.getAttribute("nifi-registry.properties");
+
+        final ProviderFactory providerFactory = new 
StandardProviderFactory(properties);
+        final MetadataProvider metadataProvider = 
providerFactory.getMetadataProvider();
+        final FlowPersistenceProvider flowPersistenceProvider = 
providerFactory.getFlowPersistenceProvider();
+
         register(HttpMethodOverrideFilter.class);
 
         // register the exception mappers
@@ -39,6 +50,6 @@ public class NiFiRegistryResourceConfig extends 
ResourceConfig {
         register(new ThrowableMapper());
 
         // register endpoints
-        register(new TestResource());
+        register(new TestResource(metadataProvider, flowPersistenceProvider));
     }
 }

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/9eb0cef0/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/TestResource.java
----------------------------------------------------------------------
diff --git 
a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/TestResource.java
 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/TestResource.java
index 2564c8a..cc6d846 100644
--- 
a/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/TestResource.java
+++ 
b/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/TestResource.java
@@ -16,6 +16,8 @@
  */
 package org.apache.nifi.registry.web.api;
 
+import org.apache.nifi.registry.flow.FlowPersistenceProvider;
+import org.apache.nifi.registry.metadata.MetadataProvider;
 import org.apache.nifi.registry.web.response.TestEntity;
 
 import javax.ws.rs.GET;
@@ -27,6 +29,23 @@ import javax.ws.rs.core.Response;
 @Path("/test")
 public class TestResource {
 
+    private final MetadataProvider metadataProvider;
+
+    private final FlowPersistenceProvider flowPersistenceProvider;
+
+    public TestResource(final MetadataProvider metadataProvider, final 
FlowPersistenceProvider flowPersistenceProvider) {
+        this.metadataProvider = metadataProvider;
+        this.flowPersistenceProvider = flowPersistenceProvider;
+
+        if (this.metadataProvider == null) {
+            throw new IllegalStateException("MetadataProvider cannot be null");
+        }
+
+        if (this.flowPersistenceProvider == null) {
+            throw new IllegalStateException("FlowPersistenceProvider cannot be 
null");
+        }
+    }
+
     @GET
     @Produces(MediaType.APPLICATION_JSON)
     public Response getTest() {

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/9eb0cef0/pom.xml
----------------------------------------------------------------------
diff --git a/pom.xml b/pom.xml
index 4185344..6538337 100644
--- a/pom.xml
+++ b/pom.xml
@@ -30,16 +30,18 @@
     <modules>
         <module>nifi-registry-properties</module>
         <module>nifi-registry-utils</module>
-       <module>nifi-registry-flow-data-model</module>
+           <module>nifi-registry-data-model</module>
         <module>nifi-registry-jetty</module>
         <module>nifi-registry-resources</module>
         <module>nifi-registry-runtime</module>
         <module>nifi-registry-security</module>
-        <module>nifi-registry-service-api</module>
+        <module>nifi-registry-framework</module>
+           <module>nifi-registry-provider-api</module>
+           <module>nifi-registry-provider-impl</module>
         <module>nifi-registry-web-api</module>
         <module>nifi-registry-web-ui</module>
         <module>nifi-registry-bootstrap</module>
-       <module>nifi-registry-assembly</module>
+           <module>nifi-registry-assembly</module>
     </modules>
     <url>https://nifi.apache.org/registry.html</url>
     <organization>
@@ -246,6 +248,11 @@
                 <artifactId>commons-lang3</artifactId>
                 <version>3.5</version>
             </dependency>
+            <dependency>
+                <groupId>commons-io</groupId>
+                <artifactId>commons-io</artifactId>
+                <version>2.5</version>
+            </dependency>
         </dependencies>
     </dependencyManagement>
 

Reply via email to