You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by kd...@apache.org on 2018/09/22 02:11:17 UTC

[18/51] [partial] nifi-registry git commit: NIFIREG-201 Refactoring project structure to better isolate extensions

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/FileUtils.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/FileUtils.java b/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/FileUtils.java
new file mode 100644
index 0000000..5abaf7e
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/FileUtils.java
@@ -0,0 +1,426 @@
+/*
+ * 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.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileLock;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collection;
+
+import org.slf4j.Logger;
+
+/**
+ * A utility class containing a few useful static methods to do typical IO
+ * operations.
+ *
+ */
+public class FileUtils {
+
+    public static final long TRANSFER_CHUNK_SIZE_BYTES = 1024 * 1024 * 8; //8 MB chunks
+    public static final long MILLIS_BETWEEN_ATTEMPTS = 50L;
+
+    /**
+     * Closes the given closeable quietly - no logging, no exceptions...
+     *
+     * @param closeable the thing to close
+     */
+    public static void closeQuietly(final Closeable closeable) {
+        if (null != closeable) {
+            try {
+                closeable.close();
+            } catch (final IOException io) {/*IGNORE*/
+
+            }
+        }
+    }
+
+    /**
+     * Releases the given lock quietly no logging, no exception
+     *
+     * @param lock the lock to release
+     */
+    public static void releaseQuietly(final FileLock lock) {
+        if (null != lock) {
+            try {
+                lock.release();
+            } catch (final IOException io) {
+                /*IGNORE*/
+            }
+        }
+    }
+
+    /* Superseded by renamed class bellow */
+    @Deprecated
+    public static void ensureDirectoryExistAndCanAccess(final File dir) throws IOException {
+        ensureDirectoryExistAndCanReadAndWrite(dir);
+    }
+
+    public static void ensureDirectoryExistAndCanReadAndWrite(final File dir) throws IOException {
+        if (dir.exists() && !dir.isDirectory()) {
+            throw new IOException(dir.getAbsolutePath() + " is not a directory");
+        } else if (!dir.exists()) {
+            final boolean made = dir.mkdirs();
+            if (!made) {
+                throw new IOException(dir.getAbsolutePath() + " could not be created");
+            }
+        }
+        if (!(dir.canRead() && dir.canWrite())) {
+            throw new IOException(dir.getAbsolutePath() + " directory does not have read/write privilege");
+        }
+    }
+
+    public static void ensureDirectoryExistAndCanRead(final File dir) throws IOException {
+        if (dir.exists() && !dir.isDirectory()) {
+            throw new IOException(dir.getAbsolutePath() + " is not a directory");
+        } else if (!dir.exists()) {
+            final boolean made = dir.mkdirs();
+            if (!made) {
+                throw new IOException(dir.getAbsolutePath() + " could not be created");
+            }
+        }
+        if (!dir.canRead()) {
+            throw new IOException(dir.getAbsolutePath() + " directory does not have read privilege");
+        }
+    }
+
+    /**
+     * Copies the given source file to the given destination file. The given destination will be overwritten if it already exists.
+     *
+     * @param source the file to copy
+     * @param destination the file to copy to
+     * @param lockInputFile if true will lock input file during copy; if false will not
+     * @param lockOutputFile if true will lock output file during copy; if false will not
+     * @param move if true will perform what is effectively a move operation rather than a pure copy. This allows for potentially highly efficient movement of the file but if not possible this will
+     * revert to a copy then delete behavior. If false, then the file is copied and the source file is retained. If a true rename/move occurs then no lock is held during that time.
+     * @param logger if failures occur, they will be logged to this logger if possible. If this logger is null, an IOException will instead be thrown, indicating the problem.
+     * @return long number of bytes copied
+     * @throws FileNotFoundException if the source file could not be found
+     * @throws IOException if unable to read or write the underlying streams
+     * @throws SecurityException if a security manager denies the needed file operations
+     */
+    public static long copyFile(final File source, final File destination, final boolean lockInputFile, final boolean lockOutputFile, final boolean move, final Logger logger)
+            throws FileNotFoundException, IOException {
+
+        FileInputStream fis = null;
+        FileOutputStream fos = null;
+        FileLock inLock = null;
+        FileLock outLock = null;
+        long fileSize = 0L;
+        if (!source.canRead()) {
+            throw new IOException("Must at least have read permission");
+
+        }
+        if (move && source.renameTo(destination)) {
+            fileSize = destination.length();
+        } else {
+            try {
+                fis = new FileInputStream(source);
+                fos = new FileOutputStream(destination);
+                final FileChannel in = fis.getChannel();
+                final FileChannel out = fos.getChannel();
+                if (lockInputFile) {
+                    inLock = in.tryLock(0, Long.MAX_VALUE, true);
+                    if (null == inLock) {
+                        throw new IOException("Unable to obtain shared file lock for: " + source.getAbsolutePath());
+                    }
+                }
+                if (lockOutputFile) {
+                    outLock = out.tryLock(0, Long.MAX_VALUE, false);
+                    if (null == outLock) {
+                        throw new IOException("Unable to obtain exclusive file lock for: " + destination.getAbsolutePath());
+                    }
+                }
+                long bytesWritten = 0;
+                do {
+                    bytesWritten += out.transferFrom(in, bytesWritten, TRANSFER_CHUNK_SIZE_BYTES);
+                    fileSize = in.size();
+                } while (bytesWritten < fileSize);
+                out.force(false);
+                FileUtils.closeQuietly(fos);
+                FileUtils.closeQuietly(fis);
+                fos = null;
+                fis = null;
+                if (move && !FileUtils.deleteFile(source, null, 5)) {
+                    if (logger == null) {
+                        FileUtils.deleteFile(destination, null, 5);
+                        throw new IOException("Could not remove file " + source.getAbsolutePath());
+                    } else {
+                        logger.warn("Configured to delete source file when renaming/move not successful.  However, unable to delete file at: " + source.getAbsolutePath());
+                    }
+                }
+            } finally {
+                FileUtils.releaseQuietly(inLock);
+                FileUtils.releaseQuietly(outLock);
+                FileUtils.closeQuietly(fos);
+                FileUtils.closeQuietly(fis);
+            }
+        }
+        return fileSize;
+    }
+
+    /**
+     * Copies the given source file to the given destination file. The given destination will be overwritten if it already exists.
+     *
+     * @param source the file to copy from
+     * @param destination the file to copy to
+     * @param lockInputFile if true will lock input file during copy; if false will not
+     * @param lockOutputFile if true will lock output file during copy; if false will not
+     * @param logger the logger to use
+     * @return long number of bytes copied
+     * @throws FileNotFoundException if the source file could not be found
+     * @throws IOException if unable to read or write to file
+     * @throws SecurityException if a security manager denies the needed file operations
+     */
+    public static long copyFile(final File source, final File destination, final boolean lockInputFile, final boolean lockOutputFile, final Logger logger) throws FileNotFoundException, IOException {
+        return FileUtils.copyFile(source, destination, lockInputFile, lockOutputFile, false, logger);
+    }
+
+    public static long copyFile(final File source, final OutputStream stream, final boolean closeOutputStream, final boolean lockInputFile) throws FileNotFoundException, IOException {
+        FileInputStream fis = null;
+        FileLock inLock = null;
+        long fileSize = 0L;
+        try {
+            fis = new FileInputStream(source);
+            final FileChannel in = fis.getChannel();
+            if (lockInputFile) {
+                inLock = in.tryLock(0, Long.MAX_VALUE, true);
+                if (inLock == null) {
+                    throw new IOException("Unable to obtain exclusive file lock for: " + source.getAbsolutePath());
+                }
+
+            }
+
+            byte[] buffer = new byte[1 << 18]; //256 KB
+            int bytesRead = -1;
+            while ((bytesRead = fis.read(buffer)) != -1) {
+                stream.write(buffer, 0, bytesRead);
+            }
+            in.force(false);
+            stream.flush();
+            fileSize = in.size();
+        } finally {
+            FileUtils.releaseQuietly(inLock);
+            FileUtils.closeQuietly(fis);
+            if (closeOutputStream) {
+                FileUtils.closeQuietly(stream);
+            }
+        }
+        return fileSize;
+    }
+
+    public static long copyFile(final InputStream stream, final File destination, final boolean closeInputStream, final boolean lockOutputFile) throws FileNotFoundException, IOException {
+        final Path destPath = destination.toPath();
+        final long size = Files.copy(stream, destPath);
+        if (closeInputStream) {
+            stream.close();
+        }
+        return size;
+    }
+
+    /**
+     * Deletes the given file. If the given file exists but could not be deleted
+     * this will be printed as a warning to the given logger
+     *
+     * @param file to delete
+     * @param logger to notify
+     * @return true if deleted
+     */
+    public static boolean deleteFile(final File file, final Logger logger) {
+        return FileUtils.deleteFile(file, logger, 1);
+    }
+
+    /**
+     * Deletes the given file. If the given file exists but could not be deleted
+     * this will be printed as a warning to the given logger
+     *
+     * @param file to delete
+     * @param logger to notify
+     * @param attempts indicates how many times an attempt to delete should be
+     * made
+     * @return true if given file no longer exists
+     */
+    public static boolean deleteFile(final File file, final Logger logger, final int attempts) {
+        if (file == null) {
+            return false;
+        }
+        boolean isGone = false;
+        try {
+            if (file.exists()) {
+                final int effectiveAttempts = Math.max(1, attempts);
+                for (int i = 0; i < effectiveAttempts && !isGone; i++) {
+                    isGone = file.delete() || !file.exists();
+                    if (!isGone && (effectiveAttempts - i) > 1) {
+                        FileUtils.sleepQuietly(MILLIS_BETWEEN_ATTEMPTS);
+                    }
+                }
+                if (!isGone && logger != null) {
+                    logger.warn("File appears to exist but unable to delete file: " + file.getAbsolutePath());
+                }
+            }
+        } catch (final Throwable t) {
+            if (logger != null) {
+                logger.warn("Unable to delete file: '" + file.getAbsolutePath() + "' due to " + t);
+            }
+        }
+        return isGone;
+    }
+
+    /**
+     * Deletes all files (not directories..) in the given directory (non
+     * recursive) that match the given filename filter. If any file cannot be
+     * deleted then this is printed at warn to the given logger.
+     *
+     * @param directory to delete contents of
+     * @param filter if null then no filter is used
+     * @param logger to notify
+     * @throws IOException if abstract pathname does not denote a directory, or
+     * if an I/O error occurs
+     */
+    public static void deleteFilesInDirectory(final File directory, final FilenameFilter filter, final Logger logger) throws IOException {
+        FileUtils.deleteFilesInDirectory(directory, filter, logger, false);
+    }
+
+    /**
+     * Deletes all files (not directories) in the given directory (recursive)
+     * that match the given filename filter. If any file cannot be deleted then
+     * this is printed at warn to the given logger.
+     *
+     * @param directory to delete contents of
+     * @param filter if null then no filter is used
+     * @param logger to notify
+     * @param recurse true if should recurse
+     * @throws IOException if abstract pathname does not denote a directory, or
+     * if an I/O error occurs
+     */
+    public static void deleteFilesInDirectory(final File directory, final FilenameFilter filter, final Logger logger, final boolean recurse) throws IOException {
+        FileUtils.deleteFilesInDirectory(directory, filter, logger, recurse, false);
+    }
+
+    /**
+     * Deletes all files (not directories) in the given directory (recursive)
+     * that match the given filename filter. If any file cannot be deleted then
+     * this is printed at warn to the given logger.
+     *
+     * @param directory to delete contents of
+     * @param filter if null then no filter is used
+     * @param logger to notify
+     * @param recurse will look for contents of sub directories.
+     * @param deleteEmptyDirectories default is false; if true will delete
+     * directories found that are empty
+     * @throws IOException if abstract pathname does not denote a directory, or
+     * if an I/O error occurs
+     */
+    public static void deleteFilesInDirectory(final File directory, final FilenameFilter filter, final Logger logger, final boolean recurse, final boolean deleteEmptyDirectories) throws IOException {
+        // ensure the specified directory is actually a directory and that it exists
+        if (null != directory && directory.isDirectory()) {
+            final File ingestFiles[] = directory.listFiles();
+            if (ingestFiles == null) {
+                // null if abstract pathname does not denote a directory, or if an I/O error occurs
+                throw new IOException("Unable to list directory content in: " + directory.getAbsolutePath());
+            }
+            for (File ingestFile : ingestFiles) {
+                boolean process = (filter == null) ? true : filter.accept(directory, ingestFile.getName());
+                if (ingestFile.isFile() && process) {
+                    FileUtils.deleteFile(ingestFile, logger, 3);
+                }
+                if (ingestFile.isDirectory() && recurse) {
+                    FileUtils.deleteFilesInDirectory(ingestFile, filter, logger, recurse, deleteEmptyDirectories);
+                    if (deleteEmptyDirectories && ingestFile.list().length == 0) {
+                        FileUtils.deleteFile(ingestFile, logger, 3);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Deletes given files.
+     *
+     * @param files to delete
+     * @param recurse will recurse
+     * @throws IOException if issues deleting files
+     */
+    public static void deleteFiles(final Collection<File> files, final boolean recurse) throws IOException {
+        for (final File file : files) {
+            FileUtils.deleteFile(file, recurse);
+        }
+    }
+
+    public static void deleteFile(final File file, final boolean recurse) throws IOException {
+        final File[] list = file.listFiles();
+        if (file.isDirectory() && recurse && list != null) {
+            FileUtils.deleteFiles(Arrays.asList(list), recurse);
+        }
+        //now delete the file itself regardless of whether it is plain file or a directory
+        if (!FileUtils.deleteFile(file, null, 5)) {
+            throw new IOException("Unable to delete " + file.getAbsolutePath());
+        }
+    }
+
+    public static void sleepQuietly(final long millis) {
+        try {
+            Thread.sleep(millis);
+        } catch (final InterruptedException ex) {
+            /* do nothing */
+        }
+    }
+
+
+    // The invalid character list is derived from this Stackoverflow page.
+    // https://stackoverflow.com/questions/1155107/is-there-a-cross-platform-java-method-to-remove-filename-special-chars
+    private final static int[] INVALID_CHARS = {34, 60, 62, 124, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
+            16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 58, 42, 63, 92, 47, 32};
+
+    static {
+        Arrays.sort(INVALID_CHARS);
+    }
+
+    /**
+     * Replaces invalid characters for a file system name within a given filename string to underscore '_'.
+     * Be careful not to pass a file path as this method replaces path delimiter characters (i.e forward/back slashes).
+     * @param filename The filename to clean
+     * @return sanitized filename
+     */
+    public static String sanitizeFilename(String filename) {
+        if (filename == null || filename.isEmpty()) {
+            return filename;
+        }
+        int codePointCount = filename.codePointCount(0, filename.length());
+
+        final StringBuilder cleanName = new StringBuilder();
+        for (int i = 0; i < codePointCount; i++) {
+            int c = filename.codePointAt(i);
+            if (Arrays.binarySearch(INVALID_CHARS, c) < 0) {
+                cleanName.appendCodePoint(c);
+            } else {
+                cleanName.append('_');
+            }
+        }
+        return cleanName.toString();
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/FormatUtils.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/FormatUtils.java b/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/FormatUtils.java
new file mode 100644
index 0000000..c1e353d
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/FormatUtils.java
@@ -0,0 +1,261 @@
+/*
+ * 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.text.NumberFormat;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class FormatUtils {
+
+    private static final String UNION = "|";
+
+    // for Data Sizes
+    private static final double BYTES_IN_KILOBYTE = 1024;
+    private static final double BYTES_IN_MEGABYTE = BYTES_IN_KILOBYTE * 1024;
+    private static final double BYTES_IN_GIGABYTE = BYTES_IN_MEGABYTE * 1024;
+    private static final double BYTES_IN_TERABYTE = BYTES_IN_GIGABYTE * 1024;
+
+    // for Time Durations
+    private static final String NANOS = join(UNION, "ns", "nano", "nanos", "nanosecond", "nanoseconds");
+    private static final String MILLIS = join(UNION, "ms", "milli", "millis", "millisecond", "milliseconds");
+    private static final String SECS = join(UNION, "s", "sec", "secs", "second", "seconds");
+    private static final String MINS = join(UNION, "m", "min", "mins", "minute", "minutes");
+    private static final String HOURS = join(UNION, "h", "hr", "hrs", "hour", "hours");
+    private static final String DAYS = join(UNION, "d", "day", "days");
+    private static final String WEEKS = join(UNION, "w", "wk", "wks", "week", "weeks");
+
+    private static final String VALID_TIME_UNITS = join(UNION, NANOS, MILLIS, SECS, MINS, HOURS, DAYS, WEEKS);
+    public static final String TIME_DURATION_REGEX = "(\\d+)\\s*(" + VALID_TIME_UNITS + ")";
+    public static final Pattern TIME_DURATION_PATTERN = Pattern.compile(TIME_DURATION_REGEX);
+
+    /**
+     * Formats the specified count by adding commas.
+     *
+     * @param count the value to add commas to
+     * @return the string representation of the given value with commas included
+     */
+    public static String formatCount(final long count) {
+        return NumberFormat.getIntegerInstance().format(count);
+    }
+
+    /**
+     * Formats the specified duration in 'mm:ss.SSS' format.
+     *
+     * @param sourceDuration the duration to format
+     * @param sourceUnit the unit to interpret the duration
+     * @return representation of the given time data in minutes/seconds
+     */
+    public static String formatMinutesSeconds(final long sourceDuration, final TimeUnit sourceUnit) {
+        final long millis = TimeUnit.MILLISECONDS.convert(sourceDuration, sourceUnit);
+
+        final long millisInMinute = TimeUnit.MILLISECONDS.convert(1, TimeUnit.MINUTES);
+        final int minutes = (int) (millis / millisInMinute);
+        final long secondsMillisLeft = millis - minutes * millisInMinute;
+
+        final long millisInSecond = TimeUnit.MILLISECONDS.convert(1, TimeUnit.SECONDS);
+        final int seconds = (int) (secondsMillisLeft / millisInSecond);
+        final long millisLeft = secondsMillisLeft - seconds * millisInSecond;
+
+        return pad2Places(minutes) + ":" + pad2Places(seconds) + "." + pad3Places(millisLeft);
+    }
+
+    /**
+     * Formats the specified duration in 'HH:mm:ss.SSS' format.
+     *
+     * @param sourceDuration the duration to format
+     * @param sourceUnit the unit to interpret the duration
+     * @return representation of the given time data in hours/minutes/seconds
+     */
+    public static String formatHoursMinutesSeconds(final long sourceDuration, final TimeUnit sourceUnit) {
+        final long millis = TimeUnit.MILLISECONDS.convert(sourceDuration, sourceUnit);
+
+        final long millisInHour = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS);
+        final int hours = (int) (millis / millisInHour);
+        final long minutesSecondsMillisLeft = millis - hours * millisInHour;
+
+        return pad2Places(hours) + ":" + formatMinutesSeconds(minutesSecondsMillisLeft, TimeUnit.MILLISECONDS);
+    }
+
+    private static String pad2Places(final long val) {
+        return (val < 10) ? "0" + val : String.valueOf(val);
+    }
+
+    private static String pad3Places(final long val) {
+        return (val < 100) ? "0" + pad2Places(val) : String.valueOf(val);
+    }
+
+    /**
+     * Formats the specified data size in human readable format.
+     *
+     * @param dataSize Data size in bytes
+     * @return Human readable format
+     */
+    public static String formatDataSize(final double dataSize) {
+        // initialize the formatter
+        final NumberFormat format = NumberFormat.getNumberInstance();
+        format.setMaximumFractionDigits(2);
+
+        // check terabytes
+        double dataSizeToFormat = dataSize / BYTES_IN_TERABYTE;
+        if (dataSizeToFormat > 1) {
+            return format.format(dataSizeToFormat) + " TB";
+        }
+
+        // check gigabytes
+        dataSizeToFormat = dataSize / BYTES_IN_GIGABYTE;
+        if (dataSizeToFormat > 1) {
+            return format.format(dataSizeToFormat) + " GB";
+        }
+
+        // check megabytes
+        dataSizeToFormat = dataSize / BYTES_IN_MEGABYTE;
+        if (dataSizeToFormat > 1) {
+            return format.format(dataSizeToFormat) + " MB";
+        }
+
+        // check kilobytes
+        dataSizeToFormat = dataSize / BYTES_IN_KILOBYTE;
+        if (dataSizeToFormat > 1) {
+            return format.format(dataSizeToFormat) + " KB";
+        }
+
+        // default to bytes
+        return format.format(dataSize) + " bytes";
+    }
+
+    public static long getTimeDuration(final String value, final TimeUnit desiredUnit) {
+        final Matcher matcher = TIME_DURATION_PATTERN.matcher(value.toLowerCase());
+        if (!matcher.matches()) {
+            throw new IllegalArgumentException("Value '" + value + "' is not a valid Time Duration");
+        }
+
+        final String duration = matcher.group(1);
+        final String units = matcher.group(2);
+        TimeUnit specifiedTimeUnit = null;
+        switch (units.toLowerCase()) {
+            case "ns":
+            case "nano":
+            case "nanos":
+            case "nanoseconds":
+                specifiedTimeUnit = TimeUnit.NANOSECONDS;
+                break;
+            case "ms":
+            case "milli":
+            case "millis":
+            case "milliseconds":
+                specifiedTimeUnit = TimeUnit.MILLISECONDS;
+                break;
+            case "s":
+            case "sec":
+            case "secs":
+            case "second":
+            case "seconds":
+                specifiedTimeUnit = TimeUnit.SECONDS;
+                break;
+            case "m":
+            case "min":
+            case "mins":
+            case "minute":
+            case "minutes":
+                specifiedTimeUnit = TimeUnit.MINUTES;
+                break;
+            case "h":
+            case "hr":
+            case "hrs":
+            case "hour":
+            case "hours":
+                specifiedTimeUnit = TimeUnit.HOURS;
+                break;
+            case "d":
+            case "day":
+            case "days":
+                specifiedTimeUnit = TimeUnit.DAYS;
+                break;
+            case "w":
+            case "wk":
+            case "wks":
+            case "week":
+            case "weeks":
+                final long durationVal = Long.parseLong(duration);
+                return desiredUnit.convert(durationVal, TimeUnit.DAYS)*7;
+        }
+
+        final long durationVal = Long.parseLong(duration);
+        return desiredUnit.convert(durationVal, specifiedTimeUnit);
+    }
+
+    public static String formatUtilization(final double utilization) {
+        return utilization + "%";
+    }
+
+    private static String join(final String delimiter, final String... values) {
+        if (values.length == 0) {
+            return "";
+        } else if (values.length == 1) {
+            return values[0];
+        }
+
+        final StringBuilder sb = new StringBuilder();
+        sb.append(values[0]);
+        for (int i = 1; i < values.length; i++) {
+            sb.append(delimiter).append(values[i]);
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     * Formats nanoseconds in the format:
+     * 3 seconds, 8 millis, 3 nanos - if includeTotalNanos = false,
+     * 3 seconds, 8 millis, 3 nanos (3008000003 nanos) - if includeTotalNanos = true
+     *
+     * @param nanos the number of nanoseconds to format
+     * @param includeTotalNanos whether or not to include the total number of nanoseconds in parentheses in the returned value
+     * @return a human-readable String that is a formatted representation of the given number of nanoseconds.
+     */
+    public static String formatNanos(final long nanos, final boolean includeTotalNanos) {
+        final StringBuilder sb = new StringBuilder();
+
+        final long seconds = nanos > 1000000000L ? nanos / 1000000000L : 0L;
+        long millis = nanos > 1000000L ? nanos / 1000000L : 0L;
+        final long nanosLeft = nanos % 1000000L;
+
+        if (seconds > 0) {
+            sb.append(seconds).append(" seconds");
+        }
+        if (millis > 0) {
+            if (seconds > 0) {
+                sb.append(", ");
+                millis -= seconds * 1000L;
+            }
+
+            sb.append(millis).append(" millis");
+        }
+        if (seconds > 0 || millis > 0) {
+            sb.append(", ");
+        }
+        sb.append(nanosLeft).append(" nanos");
+
+        if (includeTotalNanos) {
+            sb.append(" (").append(nanos).append(" nanos)");
+        }
+
+        return sb.toString();
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/PropertyValue.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/PropertyValue.java b/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/PropertyValue.java
new file mode 100644
index 0000000..4950772
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/PropertyValue.java
@@ -0,0 +1,91 @@
+/*
+ * 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.concurrent.TimeUnit;
+
+/**
+ * <p>
+ * A PropertyValue provides a mechanism whereby the currently configured value
+ * can be obtained in different forms.
+ * </p>
+ */
+public interface PropertyValue {
+
+    /**
+     * @return the raw property value as a string
+     */
+    String getValue();
+
+    /**
+     * @return an integer representation of the property value, or
+     * <code>null</code> if not set
+     * @throws NumberFormatException if not able to parse
+     */
+    Integer asInteger();
+
+    /**
+     * @return a Long representation of the property value, or <code>null</code>
+     * if not set
+     * @throws NumberFormatException if not able to parse
+     */
+    Long asLong();
+
+    /**
+     * @return a Boolean representation of the property value, or
+     * <code>null</code> if not set
+     */
+    Boolean asBoolean();
+
+    /**
+     * @return a Float representation of the property value, or
+     * <code>null</code> if not set
+     * @throws NumberFormatException if not able to parse
+     */
+    Float asFloat();
+
+    /**
+     * @return a Double representation of the property value, of
+     * <code>null</code> if not set
+     * @throws NumberFormatException if not able to parse
+     */
+    Double asDouble();
+
+    /**
+     * @param timeUnit specifies the TimeUnit to convert the time duration into
+     * @return a Long value representing the value of the configured time period
+     * in terms of the specified TimeUnit; if the property is not set, returns
+     * <code>null</code>
+     */
+    Long asTimePeriod(TimeUnit timeUnit);
+
+    /**
+     *
+     * @param dataUnit specifies the DataUnit to convert the data size into
+     * @return a Long value representing the value of the configured data size
+     * in terms of the specified DataUnit; if hte property is not set, returns
+     * <code>null</code>
+     */
+    Double asDataSize(DataUnit dataUnit);
+
+    /**
+     * @return <code>true</code> if the user has configured a value, or if the
+     * PropertyDescriptor for the associated property has a default
+     * value, <code>false</code> otherwise
+     */
+    boolean isSet();
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/StandardPropertyValue.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/StandardPropertyValue.java b/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/StandardPropertyValue.java
new file mode 100644
index 0000000..b185fad
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-utils/src/main/java/org/apache/nifi/registry/util/StandardPropertyValue.java
@@ -0,0 +1,79 @@
+/*
+ * 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.concurrent.TimeUnit;
+
+public class StandardPropertyValue implements PropertyValue {
+
+    private final String rawValue;
+
+    public StandardPropertyValue(final String rawValue) {
+        this.rawValue = rawValue;
+    }
+
+    @Override
+    public String getValue() {
+        return rawValue;
+    }
+
+    @Override
+    public Integer asInteger() {
+        return (rawValue == null) ? null : Integer.parseInt(rawValue.trim());
+    }
+
+    @Override
+    public Long asLong() {
+        return (rawValue == null) ? null : Long.parseLong(rawValue.trim());
+    }
+
+    @Override
+    public Boolean asBoolean() {
+        return (rawValue == null) ? null : Boolean.parseBoolean(rawValue.trim());
+    }
+
+    @Override
+    public Float asFloat() {
+        return (rawValue == null) ? null : Float.parseFloat(rawValue.trim());
+    }
+
+    @Override
+    public Double asDouble() {
+        return (rawValue == null) ? null : Double.parseDouble(rawValue.trim());
+    }
+
+    @Override
+    public Long asTimePeriod(final TimeUnit timeUnit) {
+        return (rawValue == null) ? null : FormatUtils.getTimeDuration(rawValue.trim(), timeUnit);
+    }
+
+    @Override
+    public Double asDataSize(final DataUnit dataUnit) {
+        return rawValue == null ? null : DataUnit.parseDataSize(rawValue.trim(), dataUnit);
+    }
+
+    @Override
+    public boolean isSet() {
+        return rawValue != null;
+    }
+
+    @Override
+    public String toString() {
+        return rawValue;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-utils/src/test/java/org/apache/nifi/registry/util/TestFileUtils.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-utils/src/test/java/org/apache/nifi/registry/util/TestFileUtils.java b/nifi-registry-core/nifi-registry-utils/src/test/java/org/apache/nifi/registry/util/TestFileUtils.java
new file mode 100644
index 0000000..d4bc963
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-utils/src/test/java/org/apache/nifi/registry/util/TestFileUtils.java
@@ -0,0 +1,31 @@
+/*
+ * 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 org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public class TestFileUtils {
+    @Test
+    public void testSanitizeFilename() {
+        String filename = "This / is / a test";
+        final String sanitizedFilename = FileUtils.sanitizeFilename(filename);
+        assertEquals("This___is___a_test", sanitizedFilename);
+    }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/pom.xml
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/pom.xml b/nifi-registry-core/nifi-registry-web-api/pom.xml
new file mode 100644
index 0000000..65ec265
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/pom.xml
@@ -0,0 +1,359 @@
+<?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">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.nifi.registry</groupId>
+        <artifactId>nifi-registry-core</artifactId>
+        <version>0.3.0-SNAPSHOT</version>
+    </parent>
+    <artifactId>nifi-registry-web-api</artifactId>
+    <version>0.3.0-SNAPSHOT</version>
+    <packaging>war</packaging>
+
+    <build>
+        <resources>
+            <resource>
+                <directory>src/main/resources</directory>
+            </resource>
+        </resources>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>${spring.boot.version}</version>
+            </plugin>
+            <plugin>
+                <artifactId>maven-war-plugin</artifactId>
+                <configuration>
+                    <failOnMissingWebXml>false</failOnMissingWebXml>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>com.github.kongchen</groupId>
+                <artifactId>swagger-maven-plugin</artifactId>
+                <version>3.1.6</version>
+                <executions>
+                    <execution>
+                        <phase>compile</phase>
+                        <goals>
+                            <goal>generate</goal>
+                        </goals>
+                        <configuration>
+                            <apiSources>
+                                <apiSource>
+                                    <locations>
+                                        <location>org.apache.nifi.registry.web.api</location>
+                                    </locations>
+                                    <schemes>
+                                        <scheme>http</scheme>
+                                        <scheme>https</scheme>
+                                    </schemes>
+                                    <basePath>/nifi-registry-api</basePath>
+                                    <info>
+                                        <title>NiFi Registry REST API</title>
+                                        <version>${project.version}</version>
+                                        <description>
+                                            The REST API provides an interface to a registry with operations for saving, versioning, reading NiFi flows and components.
+                                        </description>
+                                        <contact>
+                                            <name>Apache NiFi Registry</name>
+                                            <email>dev@nifi.apache.org</email>
+                                            <url>https://nifi.apache.org</url>
+                                        </contact>
+                                        <license>
+                                            <url>http://www.apache.org/licenses/LICENSE-2.0.html</url>
+                                            <name>Apache 2.0 License</name>
+                                        </license>
+                                    </info>
+                                    <securityDefinitions>
+                                        <securityDefinition>
+                                            <jsonPath>${project.basedir}/src/main/resources/swagger/security-definitions.json</jsonPath>
+                                        </securityDefinition>
+                                    </securityDefinitions>
+                                    <templatePath>classpath:/templates/index.html.hbs</templatePath>
+                                    <outputPath>
+                                        ${project.build.directory}/${project.artifactId}-${project.version}/docs/rest-api/index.html
+                                    </outputPath>
+                                    <swaggerDirectory>${project.build.directory}/swagger</swaggerDirectory>
+                                </apiSource>
+                            </apiSources>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <artifactId>maven-resources-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>copy-resources</id>
+                        <phase>validate</phase>
+                        <goals>
+                            <goal>copy-resources</goal>
+                        </goals>
+                        <configuration>
+                            <outputDirectory>${project.build.directory}/${project.artifactId}-${project.version}/docs/rest-api/images</outputDirectory>
+                            <resources>
+                                <resource>
+                                    <directory>src/main/resources/images</directory>
+                                </resource>
+                            </resources>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>com.googlecode.maven-download-plugin</groupId>
+                <artifactId>download-maven-plugin</artifactId>
+                <version>1.2.1</version>
+                <executions>
+                    <execution>
+                        <id>download-swagger-ui</id>
+                        <!-- This plugin downloads swagger UI static assets during the build, to be
+                             served by the web app to render the dynamically generated Swagger spec.
+                             For offline development, or to build without the swagger UI, activate
+                             the "no-swagger-ui" maven profile during the build with the "-P" flag -->
+                        <goals>
+                            <goal>wget</goal>
+                        </goals>
+                        <configuration>
+                            <url>
+                                https://github.com/swagger-api/swagger-ui/archive/v${swagger.ui.version}.tar.gz
+                            </url>
+                            <unpack>true</unpack>
+                            <outputDirectory>${project.build.directory}
+                            </outputDirectory>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-antrun-plugin</artifactId>
+                <version>1.8</version>
+                <executions>
+                    <execution>
+                        <id>bundle-swagger-ui</id>
+                        <phase>prepare-package</phase>
+                        <goals>
+                            <goal>run</goal>
+                        </goals>
+                        <configuration>
+                            <target>
+                                <sequential>
+                                    <echo>Copy static Swagger UI files to target</echo>
+                                    <copy todir="${project.build.directory}/classes/static/swagger">
+                                        <fileset dir="${project.build.directory}/swagger-ui-${swagger.ui.version}/dist">
+                                            <include name="**" />
+                                        </fileset>
+                                    </copy>
+                                    <echo>Disable schema validation by removing validatorUrl</echo>
+                                    <replace token="https://online.swagger.io/validator" value="" dir="${project.build.directory}/classes/static/swagger">
+                                        <include name="swagger-ui-bundle.js" />
+                                        <include name="swagger-ui-standalone-preset.js" />
+                                    </replace>
+                                    <echo>Rename 'index.html' to 'ui.html'</echo>
+                                    <move file="${project.build.directory}/classes/static/swagger/index.html" tofile="${project.build.directory}/classes/static/swagger/ui.html" />
+                                    <echo>Replace default swagger.json location</echo>
+                                    <replace token="http://petstore.swagger.io/v2/swagger.json" value="/nifi-registry-api/swagger/swagger.json" dir="${project.build.directory}/classes/static/swagger">
+                                        <include name="ui.html" />
+                                    </replace>
+                                    <echo>Copy swagger.json into static assets folder</echo>
+                                    <copy todir="${project.build.directory}/classes/static/swagger">
+                                        <fileset dir="${project.build.directory}/swagger">
+                                            <include name="*.json" />
+                                        </fileset>
+                                    </copy>
+                                </sequential>
+                            </target>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+    <profiles>
+        <profile>
+            <id>no-swagger-ui</id>
+            <!-- Activate this profile with "-P no-swagger-ui" to disable the Swagger UI
+                 static assets from being downloaded and bundled with the web api WAR. -->
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>com.googlecode.maven-download-plugin</groupId>
+                        <artifactId>download-maven-plugin</artifactId>
+                        <version>1.2.1</version>
+                        <executions>
+                            <execution>
+                                <id>download-swagger-ui</id>
+                                <phase>none</phase>
+                            </execution>
+                        </executions>
+                    </plugin>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-antrun-plugin</artifactId>
+                        <version>1.8</version>
+                        <executions>
+                            <execution>
+                                <id>bundle-swagger-ui</id>
+                                <phase>none</phase>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+    </profiles>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+            <version>${spring.boot.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-jersey</artifactId>
+            <version>${spring.boot.version}</version>
+        </dependency>
+        <!-- Exclude micrometer-core because it creates a class cast issue with logback, revisit later -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+            <version>${spring.boot.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>io.micrometer</groupId>
+                    <artifactId>micrometer-core</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.security.kerberos</groupId>
+            <artifactId>spring-security-kerberos-core</artifactId>
+            <version>1.0.1.RELEASE</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.springframework</groupId>
+                    <artifactId>spring-core</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.springframework.security</groupId>
+                    <artifactId>spring-security-core</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <!-- Must be marked provided in order to produce a correct WAR -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-tomcat</artifactId>
+            <version>${spring.boot.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-framework</artifactId>
+            <version>0.3.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-properties</artifactId>
+            <version>0.3.0-SNAPSHOT</version>
+            <scope>provided</scope> <!-- This will be in the lib directory -->
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-security-api</artifactId>
+            <version>0.3.0-SNAPSHOT</version>
+            <scope>provided</scope> <!-- This will be in lib directory -->
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-provider-api</artifactId>
+            <version>0.3.0-SNAPSHOT</version>
+            <scope>provided</scope> <!-- This will be in lib directory -->
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-security-utils</artifactId>
+            <version>0.3.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>javax.servlet-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>io.swagger</groupId>
+            <artifactId>swagger-annotations</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt</artifactId>
+            <version>0.7.0</version>
+        </dependency>
+        <!-- Test Dependencies -->
+        <dependency>
+            <groupId>org.apache.nifi.registry</groupId>
+            <artifactId>nifi-registry-client</artifactId>
+            <version>0.3.0-SNAPSHOT</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <version>${spring.boot.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-jetty</artifactId>
+            <version>${spring.boot.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.unboundid</groupId>
+            <artifactId>unboundid-ldapsdk</artifactId>
+            <version>3.2.1</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.spockframework</groupId>
+            <artifactId>spock-core</artifactId>
+            <version>1.0-groovy-2.4</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.codehaus.groovy</groupId>
+            <artifactId>groovy-all</artifactId>
+            <version>2.4.12</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>cglib</groupId>
+            <artifactId>cglib-nodep</artifactId>
+            <version>2.2.2</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/NiFiRegistryApiApplication.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/NiFiRegistryApiApplication.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/NiFiRegistryApiApplication.java
new file mode 100644
index 0000000..d06555d
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/NiFiRegistryApiApplication.java
@@ -0,0 +1,85 @@
+/*
+ * 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;
+
+import org.apache.nifi.registry.event.EventService;
+import org.apache.nifi.registry.event.StandardEvent;
+import org.apache.nifi.registry.hook.Event;
+import org.apache.nifi.registry.hook.EventType;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.context.event.ApplicationReadyEvent;
+import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
+import org.springframework.context.ApplicationListener;
+import org.springframework.stereotype.Component;
+
+import java.util.Properties;
+
+/**
+ * Main class for starting the NiFi Registry Web API as a Spring Boot application.
+ *
+ * This class is purposely in the org.apache.nifi.registry package since that is the common base
+ * package across other modules. This is done because spring-boot will use the package of this
+ * class to automatically scan for beans/config/entities/etc. and would otherwise require
+ * configuring custom packages to scan in several different places.
+ *
+ * WebMvcAutoConfiguration is excluded because our web app is using Jersey in place of SpringMVC
+ */
+@SpringBootApplication
+public class NiFiRegistryApiApplication extends SpringBootServletInitializer {
+
+    public static final String NIFI_REGISTRY_PROPERTIES_ATTRIBUTE = "nifi-registry.properties";
+    public static final String NIFI_REGISTRY_MASTER_KEY_ATTRIBUTE = "nifi-registry.key";
+
+    @Autowired
+    private EventService eventService;
+
+    @Override
+    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
+        final Properties defaultProperties = new Properties();
+
+        // Enable Actuator Endpoints
+        defaultProperties.setProperty("management.endpoints.web.expose", "*");
+
+        // Run Jersey as a filter instead of a servlet so that requests can be forwarded to other handlers (e.g., actuator)
+        defaultProperties.setProperty("spring.jersey.type", "filter");
+
+        return application
+                .sources(NiFiRegistryApiApplication.class)
+                .properties(defaultProperties);
+    }
+
+    @Component
+    private class OnApplicationReadyEventing
+            implements ApplicationListener<ApplicationReadyEvent> {
+
+        @Override
+        public void onApplicationEvent(final ApplicationReadyEvent event) {
+            Event registryStartEvent = new StandardEvent.Builder()
+                    .eventType(EventType.REGISTRY_START)
+                    .build();
+            eventService.publish(registryStartEvent);
+        }
+    }
+
+    public static void main(String[] args) {
+        SpringApplication.run(NiFiRegistryApiApplication.class, args);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/NiFiRegistryPropertiesFactory.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/NiFiRegistryPropertiesFactory.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/NiFiRegistryPropertiesFactory.java
new file mode 100644
index 0000000..b7865bb
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/NiFiRegistryPropertiesFactory.java
@@ -0,0 +1,47 @@
+/*
+ * 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;
+
+import org.apache.nifi.registry.properties.NiFiRegistryProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.context.ServletContextAware;
+
+import javax.servlet.ServletContext;
+
+/**
+ * The JettyServer puts an instance of NiFiRegistryProperties into the ServletContext, this class
+ * obtains that instance and makes it available to inject to all other places.
+ *
+ */
+@Configuration
+public class NiFiRegistryPropertiesFactory implements ServletContextAware {
+
+    private NiFiRegistryProperties properties;
+
+    @Override
+    public void setServletContext(ServletContext servletContext) {
+        properties = (NiFiRegistryProperties) servletContext.getAttribute(
+                NiFiRegistryApiApplication.NIFI_REGISTRY_PROPERTIES_ATTRIBUTE);
+    }
+
+    @Bean
+    public NiFiRegistryProperties getNiFiRegistryProperties() {
+        return properties;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/NiFiRegistryResourceConfig.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/NiFiRegistryResourceConfig.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/NiFiRegistryResourceConfig.java
new file mode 100644
index 0000000..a5ab5ef
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/NiFiRegistryResourceConfig.java
@@ -0,0 +1,78 @@
+/*
+ * 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.web;
+
+import org.apache.nifi.registry.web.api.AccessPolicyResource;
+import org.apache.nifi.registry.web.api.AccessResource;
+import org.apache.nifi.registry.web.api.BucketFlowResource;
+import org.apache.nifi.registry.web.api.BucketResource;
+import org.apache.nifi.registry.web.api.ConfigResource;
+import org.apache.nifi.registry.web.api.FlowResource;
+import org.apache.nifi.registry.web.api.ItemResource;
+import org.apache.nifi.registry.web.api.TenantResource;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.server.ServerProperties;
+import org.glassfish.jersey.server.filter.HttpMethodOverrideFilter;
+import org.glassfish.jersey.servlet.ServletProperties;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.Configuration;
+
+import javax.servlet.ServletContext;
+import javax.ws.rs.core.Context;
+
+/**
+ * This is the main Jersey configuration for the application.
+ *
+ *  NOTE: Don't set @ApplicationPath here because it has already been set to 'nifi-registry-api' in JettyServer
+ */
+@Configuration
+public class NiFiRegistryResourceConfig extends ResourceConfig {
+
+    private static final Logger logger = LoggerFactory.getLogger(NiFiRegistryResourceConfig.class);
+
+    public NiFiRegistryResourceConfig(@Context ServletContext servletContext) {
+        // register filters
+        register(HttpMethodOverrideFilter.class);
+
+        // register the exception mappers & jackson object mapper resolver
+        packages("org.apache.nifi.registry.web.mapper");
+
+        // register endpoints
+        register(AccessPolicyResource.class);
+        register(AccessResource.class);
+        register(BucketResource.class);
+        register(BucketFlowResource.class);
+        register(FlowResource.class);
+        register(ItemResource.class);
+        register(TenantResource.class);
+        register(ConfigResource.class);
+
+        // include bean validation errors in response
+        property(ServerProperties.BV_SEND_ERROR_IN_RESPONSE, true);
+
+        // this is necessary for the /access/token/kerberos endpoint to work correctly
+        // when sending 401 Unauthorized with a WWW-Authenticate: Negotiate header.
+        // if this value needs to be changed, kerberos authentication needs to move to filter chain
+        // so it can directly set the HttpServletResponse instead of indirectly through a JAX-RS Response
+        property(ServerProperties.RESPONSE_SET_STATUS_OVER_SEND_ERROR, true);
+
+        // configure jersey to ignore resource paths for actuator and swagger-ui
+        property(ServletProperties.FILTER_STATIC_CONTENT_REGEX, "/(actuator|swagger/).*");
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi-registry/blob/6f26290d/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessPolicyResource.java
----------------------------------------------------------------------
diff --git a/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessPolicyResource.java b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessPolicyResource.java
new file mode 100644
index 0000000..713b369
--- /dev/null
+++ b/nifi-registry-core/nifi-registry-web-api/src/main/java/org/apache/nifi/registry/web/api/AccessPolicyResource.java
@@ -0,0 +1,407 @@
+/*
+ * 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.web.api;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import io.swagger.annotations.Authorization;
+import io.swagger.annotations.Extension;
+import io.swagger.annotations.ExtensionProperty;
+import org.apache.nifi.registry.authorization.AccessPolicy;
+import org.apache.nifi.registry.authorization.AccessPolicySummary;
+import org.apache.nifi.registry.authorization.Resource;
+import org.apache.nifi.registry.event.EventService;
+import org.apache.nifi.registry.exception.ResourceNotFoundException;
+import org.apache.nifi.registry.security.authorization.Authorizer;
+import org.apache.nifi.registry.security.authorization.AuthorizerCapabilityDetection;
+import org.apache.nifi.registry.security.authorization.RequestAction;
+import org.apache.nifi.registry.security.authorization.resource.Authorizable;
+import org.apache.nifi.registry.service.AuthorizationService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.net.URI;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * RESTful endpoint for managing access policies.
+ */
+@Component
+@Path("/policies")
+@Api(
+        value = "policies",
+        description = "Endpoint for managing access policies.",
+        authorizations = { @Authorization("Authorization") }
+)
+public class AccessPolicyResource extends AuthorizableApplicationResource {
+
+    private static final Logger logger = LoggerFactory.getLogger(AccessPolicyResource.class);
+
+    private Authorizer authorizer;
+
+    @Autowired
+    public AccessPolicyResource(
+            Authorizer authorizer,
+            AuthorizationService authorizationService,
+            EventService eventService) {
+        super(authorizationService, eventService);
+        this.authorizer = authorizer;
+    }
+
+    /**
+     * Create a new access policy.
+     *
+     * @param httpServletRequest request
+     * @param requestAccessPolicy the access policy to create.
+     * @return The created access policy.
+     */
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(
+            value = "Creates an access policy",
+            response = AccessPolicy.class,
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "write"),
+                            @ExtensionProperty(name = "resource", value = "/policies") })
+            }
+    )
+    @ApiResponses({
+            @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+            @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+            @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+            @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry might not be configured to use a ConfigurableAccessPolicyProvider.") })
+    public Response createAccessPolicy(
+            @Context final HttpServletRequest httpServletRequest,
+            @ApiParam(value = "The access policy configuration details.", required = true)
+            final AccessPolicy requestAccessPolicy) {
+
+        verifyAuthorizerSupportsConfigurablePolicies();
+        authorizeAccess(RequestAction.WRITE);
+
+        if (requestAccessPolicy == null) {
+            throw new IllegalArgumentException("Access policy details must be specified when creating a new policy.");
+        }
+        if (requestAccessPolicy.getIdentifier() != null) {
+            throw new IllegalArgumentException("Access policy ID cannot be specified when creating a new policy.");
+        }
+        if (requestAccessPolicy.getResource() == null) {
+            throw new IllegalArgumentException("Resource must be specified when creating a new access policy.");
+        }
+        RequestAction.valueOfValue(requestAccessPolicy.getAction());
+
+        AccessPolicy createdPolicy = authorizationService.createAccessPolicy(requestAccessPolicy);
+
+        String locationUri = generateAccessPolicyUri(createdPolicy);
+        return generateCreatedResponse(URI.create(locationUri), createdPolicy).build();
+    }
+
+    /**
+     * Retrieves all access policies
+     *
+     * @return A list of access policies
+     */
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(
+            value = "Gets all access policies",
+            response = AccessPolicy.class,
+            responseContainer = "List",
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "read"),
+                            @ExtensionProperty(name = "resource", value = "/policies") })
+            }
+    )
+    @ApiResponses({
+            @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+            @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+            @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+    public Response getAccessPolicies() {
+
+        verifyAuthorizerIsManaged();
+        authorizeAccess(RequestAction.READ);
+
+        List<AccessPolicy> accessPolicies = authorizationService.getAccessPolicies();
+        if (accessPolicies == null) {
+            accessPolicies = Collections.emptyList();
+        }
+
+        return generateOkResponse(accessPolicies).build();
+    }
+
+    /**
+     * Retrieves the specified access policy.
+     *
+     * @param identifier The id of the access policy to retrieve
+     * @return An accessPolicyEntity.
+     */
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("{id}")
+    @ApiOperation(
+            value = "Gets an access policy",
+            response = AccessPolicy.class,
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "read"),
+                            @ExtensionProperty(name = "resource", value = "/policies") })
+            }
+    )
+    @ApiResponses({
+            @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+            @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+            @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+            @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+    public Response getAccessPolicy(
+            @ApiParam(value = "The access policy id.", required = true)
+            @PathParam("id") final String identifier) {
+
+        verifyAuthorizerIsManaged();
+        authorizeAccess(RequestAction.READ);
+
+        final AccessPolicy accessPolicy = authorizationService.getAccessPolicy(identifier);
+        if (accessPolicy == null) {
+            logger.warn("The specified access policy id [{}] does not exist.", identifier);
+
+            throw new ResourceNotFoundException("The specified policy does not exist in this registry.");
+        }
+
+        return generateOkResponse(accessPolicy).build();
+    }
+
+
+    /**
+     * Retrieve a specified access policy for a given (action, resource) pair.
+     *
+     * @param action the action, i.e. "read", "write"
+     * @param rawResource the name of the resource as a raw string
+     * @return An access policy.
+     */
+    @GET
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("{action}/{resource: .+}")
+    @ApiOperation(
+            value = "Gets an access policy for the specified action and resource",
+            response = AccessPolicy.class,
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "read"),
+                            @ExtensionProperty(name = "resource", value = "/policies") })
+            }
+    )
+    @ApiResponses({
+            @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+            @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+            @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+            @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+            @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409) })
+    public Response getAccessPolicyForResource(
+            @ApiParam(value = "The request action.", allowableValues = "read, write, delete", required = true)
+            @PathParam("action")
+            final String action,
+            @ApiParam(value = "The resource of the policy.", required = true)
+            @PathParam("resource")
+            final String rawResource) {
+
+        verifyAuthorizerIsManaged();
+        authorizeAccess(RequestAction.READ);
+
+        // parse the action and resource type
+        final RequestAction requestAction = RequestAction.valueOfValue(action);
+        final String resource = "/" + rawResource;
+
+        AccessPolicy accessPolicy = authorizationService.getAccessPolicy(resource, requestAction);
+        if (accessPolicy == null) {
+            throw new ResourceNotFoundException("No policy found for action='" + action + "', resource='" + resource + "'");
+        }
+        return generateOkResponse(accessPolicy).build();
+    }
+
+
+    /**
+     * Update an access policy.
+     *
+     * @param httpServletRequest request
+     * @param identifier         The id of the access policy to update.
+     * @param requestAccessPolicy An access policy.
+     * @return the updated access policy.
+     */
+    @PUT
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("{id}")
+    @ApiOperation(
+            value = "Updates a access policy",
+            response = AccessPolicy.class,
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "write"),
+                            @ExtensionProperty(name = "resource", value = "/policies") })
+            }
+    )
+    @ApiResponses({
+            @ApiResponse(code = 400, message = HttpStatusMessages.MESSAGE_400),
+            @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+            @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+            @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+            @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry might not be configured to use a ConfigurableAccessPolicyProvider.") })
+    public Response updateAccessPolicy(
+            @Context
+            final HttpServletRequest httpServletRequest,
+            @ApiParam(value = "The access policy id.", required = true)
+            @PathParam("id")
+            final String identifier,
+            @ApiParam(value = "The access policy configuration details.", required = true)
+            final AccessPolicy requestAccessPolicy) {
+
+        verifyAuthorizerSupportsConfigurablePolicies();
+        authorizeAccess(RequestAction.WRITE);
+
+        if (requestAccessPolicy == null) {
+            throw new IllegalArgumentException("Access policy details must be specified when updating a policy.");
+        }
+        if (!identifier.equals(requestAccessPolicy.getIdentifier())) {
+            throw new IllegalArgumentException(String.format("The policy id in the request body (%s) does not equal the "
+                    + "policy id of the requested resource (%s).", requestAccessPolicy.getIdentifier(), identifier));
+        }
+
+        AccessPolicy createdPolicy = authorizationService.updateAccessPolicy(requestAccessPolicy);
+
+        String locationUri = generateAccessPolicyUri(createdPolicy);
+        return generateOkResponse(createdPolicy).build();
+    }
+
+
+    /**
+     * Remove a specified access policy.
+     *
+     * @param httpServletRequest request
+     * @param identifier         The id of the access policy to remove.
+     * @return The deleted access policy
+     */
+    @DELETE
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("{id}")
+    @ApiOperation(
+            value = "Deletes an access policy",
+            response = AccessPolicy.class,
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "delete"),
+                            @ExtensionProperty(name = "resource", value = "/policies") })
+            }
+    )
+    @ApiResponses({
+            @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+            @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403),
+            @ApiResponse(code = 404, message = HttpStatusMessages.MESSAGE_404),
+            @ApiResponse(code = 409, message = HttpStatusMessages.MESSAGE_409 + " The NiFi Registry might not be configured to use a ConfigurableAccessPolicyProvider.") })
+    public Response removeAccessPolicy(
+            @Context final HttpServletRequest httpServletRequest,
+            @ApiParam(value = "The access policy id.", required = true)
+            @PathParam("id")
+            final String identifier) {
+
+        verifyAuthorizerSupportsConfigurablePolicies();
+        authorizeAccess(RequestAction.DELETE);
+        AccessPolicy deletedPolicy = authorizationService.deleteAccessPolicy(identifier);
+        if (deletedPolicy == null) {
+            logger.warn("The specified access policy id [{}] does not exist.", identifier);
+
+            throw new ResourceNotFoundException("The specified policy does not exist in this registry.");
+        }
+        return generateOkResponse(deletedPolicy).build();
+    }
+
+    /**
+     * Gets the available resources that support access/authorization policies.
+     *
+     * @return A resourcesEntity.
+     */
+    @GET
+    @Path("/resources")
+    @Consumes(MediaType.WILDCARD)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(
+            value = "Gets the available resources that support access/authorization policies",
+            response = Resource.class,
+            responseContainer = "List",
+            extensions = {
+                    @Extension(name = "access-policy", properties = {
+                            @ExtensionProperty(name = "action", value = "read"),
+                            @ExtensionProperty(name = "resource", value = "/policies") })
+            }
+    )
+    @ApiResponses({
+            @ApiResponse(code = 401, message = HttpStatusMessages.MESSAGE_401),
+            @ApiResponse(code = 403, message = HttpStatusMessages.MESSAGE_403) })
+    public Response getResources() {
+        authorizeAccess(RequestAction.READ);
+
+        final List<Resource> resources = authorizationService.getResources();
+
+        return generateOkResponse(resources).build();
+    }
+
+
+    private void verifyAuthorizerIsManaged() {
+        if (!AuthorizerCapabilityDetection.isManagedAuthorizer(authorizer)) {
+            throw new IllegalStateException(AuthorizationService.MSG_NON_MANAGED_AUTHORIZER);
+        }
+    }
+
+    private void verifyAuthorizerSupportsConfigurablePolicies() {
+        if (!AuthorizerCapabilityDetection.isConfigurableAccessPolicyProvider(authorizer)) {
+            verifyAuthorizerIsManaged();
+            throw new IllegalStateException(AuthorizationService.MSG_NON_CONFIGURABLE_POLICIES);
+        }
+    }
+
+    private void authorizeAccess(RequestAction actionType) {
+        final Authorizable policiesAuthorizable = authorizableLookup.getPoliciesAuthorizable();
+        authorizationService.authorize(policiesAuthorizable, actionType);
+    }
+
+    private String generateAccessPolicyUri(final AccessPolicySummary accessPolicy) {
+        return generateResourceUri("policies", accessPolicy.getIdentifier());
+    }
+
+}