http://git-wip-us.apache.org/repos/asf/nifi/blob/9cf9e866/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/NiFiClientConfig.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/NiFiClientConfig.java
 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/NiFiClientConfig.java
new file mode 100644
index 0000000..d5f4981
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/NiFiClientConfig.java
@@ -0,0 +1,271 @@
+/*
+ * 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.toolkit.cli.impl.client.nifi;
+
+import org.apache.nifi.registry.security.util.KeyStoreUtils;
+import org.apache.nifi.registry.security.util.KeystoreType;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.security.KeyStore;
+import java.security.SecureRandom;
+
+/**
+ * Configuration for a NiFiClient.
+ */
+public class NiFiClientConfig {
+
+    public static final String DEFAULT_PROTOCOL = "TLSv1.2";
+
+    private final String baseUrl;
+    private final SSLContext sslContext;
+    private final String keystoreFilename;
+    private final String keystorePass;
+    private final String keyPass;
+    private final KeystoreType keystoreType;
+    private final String truststoreFilename;
+    private final String truststorePass;
+    private final KeystoreType truststoreType;
+    private final String protocol;
+    private final HostnameVerifier hostnameVerifier;
+    private final Integer readTimeout;
+    private final Integer connectTimeout;
+
+
+    private NiFiClientConfig(final NiFiClientConfig.Builder builder) {
+        this.baseUrl = builder.baseUrl;
+        this.sslContext = builder.sslContext;
+        this.keystoreFilename = builder.keystoreFilename;
+        this.keystorePass = builder.keystorePass;
+        this.keyPass = builder.keyPass;
+        this.keystoreType = builder.keystoreType;
+        this.truststoreFilename = builder.truststoreFilename;
+        this.truststorePass = builder.truststorePass;
+        this.truststoreType = builder.truststoreType;
+        this.protocol = builder.protocol == null ? DEFAULT_PROTOCOL : 
builder.protocol;
+        this.hostnameVerifier = builder.hostnameVerifier;
+        this.readTimeout = builder.readTimeout;
+        this.connectTimeout = builder.connectTimeout;
+    }
+
+    public String getBaseUrl() {
+        return baseUrl;
+    }
+
+    public SSLContext getSslContext() {
+        if (sslContext != null) {
+            return sslContext;
+        }
+
+        final KeyManagerFactory keyManagerFactory;
+        if (keystoreFilename != null && keystorePass != null && keystoreType 
!= null) {
+            try {
+                // prepare the keystore
+                final KeyStore keyStore = 
KeyStoreUtils.getKeyStore(keystoreType.name());
+                try (final InputStream keyStoreStream = new 
FileInputStream(new File(keystoreFilename))) {
+                    keyStore.load(keyStoreStream, keystorePass.toCharArray());
+                }
+                keyManagerFactory = 
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+
+                if (keyPass == null) {
+                    keyManagerFactory.init(keyStore, 
keystorePass.toCharArray());
+                } else {
+                    keyManagerFactory.init(keyStore, keyPass.toCharArray());
+                }
+            } catch (final Exception e) {
+                throw new IllegalStateException("Failed to load Keystore", e);
+            }
+        } else {
+            keyManagerFactory = null;
+        }
+
+        final TrustManagerFactory trustManagerFactory;
+        if (truststoreFilename != null && truststorePass != null && 
truststoreType != null) {
+            try {
+                // prepare the truststore
+                final KeyStore trustStore = 
KeyStoreUtils.getTrustStore(truststoreType.name());
+                try (final InputStream trustStoreStream = new 
FileInputStream(new File(truststoreFilename))) {
+                    trustStore.load(trustStoreStream, 
truststorePass.toCharArray());
+                }
+                trustManagerFactory = 
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+                trustManagerFactory.init(trustStore);
+            } catch (final Exception e) {
+                throw new IllegalStateException("Failed to load Truststore", 
e);
+            }
+        } else {
+            trustManagerFactory = null;
+        }
+
+        if (keyManagerFactory != null || trustManagerFactory != null) {
+            try {
+                // initialize the ssl context
+                KeyManager[] keyManagers = keyManagerFactory != null ? 
keyManagerFactory.getKeyManagers() : null;
+                TrustManager[] trustManagers = trustManagerFactory != null ? 
trustManagerFactory.getTrustManagers() : null;
+                final SSLContext sslContext = 
SSLContext.getInstance(getProtocol());
+                sslContext.init(keyManagers, trustManagers, new 
SecureRandom());
+                sslContext.getDefaultSSLParameters().setNeedClientAuth(true);
+
+                return sslContext;
+            } catch (final Exception e) {
+                throw new IllegalStateException("Created keystore and 
truststore but failed to initialize SSLContext", e);
+            }
+        } else {
+            return null;
+        }
+    }
+
+    public String getKeystoreFilename() {
+        return keystoreFilename;
+    }
+
+    public String getKeystorePass() {
+        return keystorePass;
+    }
+
+    public String getKeyPass() {
+        return keyPass;
+    }
+
+    public KeystoreType getKeystoreType() {
+        return keystoreType;
+    }
+
+    public String getTruststoreFilename() {
+        return truststoreFilename;
+    }
+
+    public String getTruststorePass() {
+        return truststorePass;
+    }
+
+    public KeystoreType getTruststoreType() {
+        return truststoreType;
+    }
+
+    public String getProtocol() {
+        return protocol;
+    }
+
+    public HostnameVerifier getHostnameVerifier() {
+        return hostnameVerifier;
+    }
+
+    public Integer getReadTimeout() {
+        return readTimeout;
+    }
+
+    public Integer getConnectTimeout() {
+        return connectTimeout;
+    }
+
+    /**
+     * Builder for client configuration.
+     */
+    public static class Builder {
+
+        private String baseUrl;
+        private SSLContext sslContext;
+        private String keystoreFilename;
+        private String keystorePass;
+        private String keyPass;
+        private KeystoreType keystoreType;
+        private String truststoreFilename;
+        private String truststorePass;
+        private KeystoreType truststoreType;
+        private String protocol;
+        private HostnameVerifier hostnameVerifier;
+        private Integer readTimeout;
+        private Integer connectTimeout;
+
+        public NiFiClientConfig.Builder baseUrl(final String baseUrl) {
+            this.baseUrl = baseUrl;
+            return this;
+        }
+
+        public NiFiClientConfig.Builder sslContext(final SSLContext 
sslContext) {
+            this.sslContext = sslContext;
+            return this;
+        }
+
+        public NiFiClientConfig.Builder keystoreFilename(final String 
keystoreFilename) {
+            this.keystoreFilename = keystoreFilename;
+            return this;
+        }
+
+        public NiFiClientConfig.Builder keystorePassword(final String 
keystorePass) {
+            this.keystorePass = keystorePass;
+            return this;
+        }
+
+        public NiFiClientConfig.Builder keyPassword(final String keyPass) {
+            this.keyPass = keyPass;
+            return this;
+        }
+
+        public NiFiClientConfig.Builder keystoreType(final KeystoreType 
keystoreType) {
+            this.keystoreType = keystoreType;
+            return this;
+        }
+
+        public NiFiClientConfig.Builder truststoreFilename(final String 
truststoreFilename) {
+            this.truststoreFilename = truststoreFilename;
+            return this;
+        }
+
+        public NiFiClientConfig.Builder truststorePassword(final String 
truststorePass) {
+            this.truststorePass = truststorePass;
+            return this;
+        }
+
+        public NiFiClientConfig.Builder truststoreType(final KeystoreType 
truststoreType) {
+            this.truststoreType = truststoreType;
+            return this;
+        }
+
+        public NiFiClientConfig.Builder protocol(final String protocol) {
+            this.protocol = protocol;
+            return this;
+        }
+
+        public NiFiClientConfig.Builder hostnameVerifier(final 
HostnameVerifier hostnameVerifier) {
+            this.hostnameVerifier = hostnameVerifier;
+            return this;
+        }
+
+        public NiFiClientConfig.Builder readTimeout(final Integer readTimeout) 
{
+            this.readTimeout = readTimeout;
+            return this;
+        }
+
+        public NiFiClientConfig.Builder connectTimeout(final Integer 
connectTimeout) {
+            this.connectTimeout = connectTimeout;
+            return this;
+        }
+
+        public NiFiClientConfig build() {
+            return new NiFiClientConfig(this);
+        }
+
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/9cf9e866/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/NiFiClientException.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/NiFiClientException.java
 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/NiFiClientException.java
new file mode 100644
index 0000000..8b392e8
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/NiFiClientException.java
@@ -0,0 +1,29 @@
+/*
+ * 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.toolkit.cli.impl.client.nifi;
+
+public class NiFiClientException extends Exception {
+
+    public NiFiClientException(final String message) {
+        super(message);
+    }
+
+    public NiFiClientException(final String message, final Throwable cause) {
+        super(message, cause);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/9cf9e866/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/ProcessGroupClient.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/ProcessGroupClient.java
 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/ProcessGroupClient.java
new file mode 100644
index 0000000..037c310
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/ProcessGroupClient.java
@@ -0,0 +1,38 @@
+/*
+ * 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.toolkit.cli.impl.client.nifi;
+
+import org.apache.nifi.web.api.entity.ProcessGroupEntity;
+import org.apache.nifi.web.api.entity.VariableRegistryEntity;
+
+import java.io.IOException;
+
+/**
+ * Client for ProcessGroupResource.
+ */
+public interface ProcessGroupClient {
+
+    ProcessGroupEntity createProcessGroup(String parentGroupdId, 
ProcessGroupEntity entity)
+            throws NiFiClientException, IOException;
+
+    ProcessGroupEntity getProcessGroup(String processGroupId) throws 
NiFiClientException, IOException;
+
+    ProcessGroupEntity updateProcessGroup(ProcessGroupEntity entity) throws 
NiFiClientException, IOException;
+
+    VariableRegistryEntity getVariables(String processGroupId) throws 
NiFiClientException, IOException;
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/9cf9e866/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/impl/AbstractJerseyClient.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/impl/AbstractJerseyClient.java
 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/impl/AbstractJerseyClient.java
new file mode 100644
index 0000000..7767a64
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/impl/AbstractJerseyClient.java
@@ -0,0 +1,120 @@
+/*
+ * 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.toolkit.cli.impl.client.nifi.impl;
+
+import org.apache.nifi.toolkit.cli.impl.client.nifi.NiFiClientException;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.client.Invocation;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Response;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Base class for the client operations to share exception handling.
+ *
+ * Sub-classes should always execute a request from getRequestBuilder(target) 
to ensure proper headers are sent.
+ */
+public class AbstractJerseyClient {
+
+    private final Map<String,String> headers;
+
+    public AbstractJerseyClient(final Map<String, String> headers) {
+        this.headers = headers == null ? Collections.emptyMap() : 
Collections.unmodifiableMap(new HashMap<>(headers));
+    }
+
+    protected Map<String,String> getHeaders() {
+        return headers;
+    }
+
+    /**
+     * Creates a new Invocation.Builder for the given WebTarget with the 
headers added to the builder.
+     *
+     * @param webTarget the target for the request
+     * @return the builder for the target with the headers added
+     */
+    protected Invocation.Builder getRequestBuilder(final WebTarget webTarget) {
+        final Invocation.Builder requestBuilder = webTarget.request();
+        headers.entrySet().stream().forEach(e -> 
requestBuilder.header(e.getKey(), e.getValue()));
+        return requestBuilder;
+    }
+
+    /**
+     * Executes the given action and returns the result.
+     *
+     * @param action the action to execute
+     * @param errorMessage the message to use if a NiFiRegistryException is 
thrown
+     * @param <T> the return type of the action
+     * @return the result of the action
+     * @throws NiFiClientException if any exception other than IOException is 
encountered
+     * @throws IOException if an I/O error occurs communicating with the 
registry
+     */
+    protected <T> T executeAction(final String errorMessage, final 
NiFiAction<T> action) throws NiFiClientException, IOException {
+        try {
+            return action.execute();
+        } catch (final Exception e) {
+            final Throwable ioeCause = getIOExceptionCause(e);
+
+            if (ioeCause == null) {
+                final StringBuilder errorMessageBuilder = new 
StringBuilder(errorMessage);
+
+                // see if we have a WebApplicationException, and if so add the 
response body to the error message
+                if (e instanceof WebApplicationException) {
+                    final Response response = ((WebApplicationException) 
e).getResponse();
+                    final String responseBody = 
response.readEntity(String.class);
+                    errorMessageBuilder.append(": ").append(responseBody);
+                }
+
+                throw new NiFiClientException(errorMessageBuilder.toString(), 
e);
+            } else {
+                throw (IOException) ioeCause;
+            }
+        }
+    }
+
+
+    /**
+     * An action to execute with the given return type.
+     *
+     * @param <T> the return type of the action
+     */
+    protected interface NiFiAction<T> {
+
+        T execute();
+
+    }
+
+    /**
+     * @param e an exception that was encountered interacting with the registry
+     * @return the IOException that caused this exception, or null if the an 
IOException did not cause this exception
+     */
+    protected Throwable getIOExceptionCause(final Throwable e) {
+        if (e == null) {
+            return null;
+        }
+
+        if (e instanceof IOException) {
+            return e;
+        }
+
+        return getIOExceptionCause(e.getCause());
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/9cf9e866/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/impl/JerseyControllerClient.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/impl/JerseyControllerClient.java
 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/impl/JerseyControllerClient.java
new file mode 100644
index 0000000..9c9ffc4
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/impl/JerseyControllerClient.java
@@ -0,0 +1,107 @@
+/*
+ * 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.toolkit.cli.impl.client.nifi.impl;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.toolkit.cli.impl.client.nifi.ControllerClient;
+import org.apache.nifi.toolkit.cli.impl.client.nifi.NiFiClientException;
+import org.apache.nifi.web.api.entity.RegistryClientEntity;
+import org.apache.nifi.web.api.entity.RegistryClientsEntity;
+
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.MediaType;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Jersey implementation of ControllerClient.
+ */
+public class JerseyControllerClient extends AbstractJerseyClient implements 
ControllerClient {
+
+    private final WebTarget controllerTarget;
+
+    public JerseyControllerClient(final WebTarget baseTarget) {
+        this(baseTarget, Collections.emptyMap());
+    }
+
+    public JerseyControllerClient(final WebTarget baseTarget, final 
Map<String,String> headers) {
+        super(headers);
+        this.controllerTarget = baseTarget.path("/controller");
+    }
+
+    @Override
+    public RegistryClientsEntity getRegistryClients() throws 
NiFiClientException, IOException {
+        return executeAction("Error retrieving registry clients", () -> {
+            final WebTarget target = controllerTarget.path("registry-clients");
+            return getRequestBuilder(target).get(RegistryClientsEntity.class);
+        });
+    }
+
+    @Override
+    public RegistryClientEntity getRegistryClient(final String id) throws 
NiFiClientException, IOException {
+        if (StringUtils.isBlank(id)) {
+            throw new IllegalArgumentException("Registry client id cannot be 
null");
+        }
+
+        final WebTarget target = controllerTarget
+                .path("registry-clients/{id}")
+                .resolveTemplate("id", id);
+
+        return getRequestBuilder(target).get(RegistryClientEntity.class);
+    }
+
+    @Override
+    public RegistryClientEntity createRegistryClient(final 
RegistryClientEntity registryClient) throws NiFiClientException, IOException {
+        if (registryClient == null) {
+            throw new IllegalArgumentException("Registry client entity cannot 
be null");
+        }
+
+        return executeAction("Error creating registry client", () -> {
+            final WebTarget target = controllerTarget.path("registry-clients");
+
+            return getRequestBuilder(target).post(
+                    Entity.entity(registryClient, MediaType.APPLICATION_JSON),
+                    RegistryClientEntity.class
+            );
+        });
+    }
+
+    @Override
+    public RegistryClientEntity updateRegistryClient(final 
RegistryClientEntity registryClient) throws NiFiClientException, IOException {
+        if (registryClient == null) {
+            throw new IllegalArgumentException("Registry client entity cannot 
be null");
+        }
+
+        if (StringUtils.isBlank(registryClient.getId())) {
+            throw new IllegalArgumentException("Registry client entity must 
contain an id");
+        }
+
+        return executeAction("Error updating registry client", () -> {
+            final WebTarget target = controllerTarget
+                    .path("registry-clients/{id}")
+                    .resolveTemplate("id", registryClient.getId());
+
+            return getRequestBuilder(target).put(
+                    Entity.entity(registryClient, MediaType.APPLICATION_JSON),
+                    RegistryClientEntity.class
+            );
+        });
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/9cf9e866/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/impl/JerseyFlowClient.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/impl/JerseyFlowClient.java
 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/impl/JerseyFlowClient.java
new file mode 100644
index 0000000..5af34be
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/impl/JerseyFlowClient.java
@@ -0,0 +1,105 @@
+/*
+ * 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.toolkit.cli.impl.client.nifi.impl;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.toolkit.cli.impl.client.nifi.FlowClient;
+import org.apache.nifi.toolkit.cli.impl.client.nifi.NiFiClientException;
+import org.apache.nifi.web.api.entity.CurrentUserEntity;
+import org.apache.nifi.web.api.entity.ProcessGroupFlowEntity;
+import org.apache.nifi.web.api.entity.ScheduleComponentsEntity;
+
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.MediaType;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Jersey implementation of FlowClient.
+ */
+public class JerseyFlowClient extends AbstractJerseyClient implements 
FlowClient {
+
+    static final String ROOT = "root";
+
+    private final WebTarget flowTarget;
+
+    public JerseyFlowClient(final WebTarget baseTarget) {
+        this(baseTarget, Collections.emptyMap());
+    }
+
+    public JerseyFlowClient(final WebTarget baseTarget, final 
Map<String,String> headers) {
+        super(headers);
+        this.flowTarget = baseTarget.path("/flow");
+    }
+
+    @Override
+    public CurrentUserEntity getCurrentUser() throws NiFiClientException, 
IOException {
+        return executeAction("Error retrieving current", () -> {
+            final WebTarget target = flowTarget.path("current-user");
+            return getRequestBuilder(target).get(CurrentUserEntity.class);
+        });
+    }
+
+    @Override
+    public String getRootGroupId() throws NiFiClientException, IOException {
+        final ProcessGroupFlowEntity entity = getProcessGroup(ROOT);
+        return entity.getProcessGroupFlow().getId();
+    }
+
+    @Override
+    public ProcessGroupFlowEntity getProcessGroup(final String id) throws 
NiFiClientException, IOException {
+        if (StringUtils.isBlank(id)) {
+            throw new IllegalArgumentException("Process group id cannot be 
null");
+        }
+
+        return executeAction("Error retrieving process group flow", () -> {
+            final WebTarget target = flowTarget
+                    .path("process-groups/{id}")
+                    .resolveTemplate("id", id);
+
+            return getRequestBuilder(target).get(ProcessGroupFlowEntity.class);
+        });
+    }
+
+    @Override
+    public ScheduleComponentsEntity scheduleProcessGroupComponents(
+            final String processGroupId, final ScheduleComponentsEntity 
scheduleComponentsEntity)
+            throws NiFiClientException, IOException {
+
+        if (StringUtils.isBlank(processGroupId)) {
+            throw new IllegalArgumentException("Process group id cannot be 
null");
+        }
+
+        if (scheduleComponentsEntity == null) {
+            throw new IllegalArgumentException("ScheduleComponentsEntity 
cannot be null");
+        }
+
+        scheduleComponentsEntity.setId(processGroupId);
+
+        return executeAction("Error scheduling components", () -> {
+            final WebTarget target = flowTarget
+                    .path("process-groups/{id}")
+                    .resolveTemplate("id", processGroupId);
+
+            return getRequestBuilder(target).put(
+                    Entity.entity(scheduleComponentsEntity, 
MediaType.APPLICATION_JSON_TYPE),
+                    ScheduleComponentsEntity.class);
+        });
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/9cf9e866/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/impl/JerseyNiFiClient.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/impl/JerseyNiFiClient.java
 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/impl/JerseyNiFiClient.java
new file mode 100644
index 0000000..091f5db
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/impl/JerseyNiFiClient.java
@@ -0,0 +1,240 @@
+/*
+ * 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.toolkit.cli.impl.client.nifi.impl;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.registry.security.util.ProxiedEntitiesUtils;
+import org.apache.nifi.toolkit.cli.impl.client.nifi.ControllerClient;
+import org.apache.nifi.toolkit.cli.impl.client.nifi.FlowClient;
+import org.apache.nifi.toolkit.cli.impl.client.nifi.NiFiClient;
+import org.apache.nifi.toolkit.cli.impl.client.nifi.NiFiClientConfig;
+import org.apache.nifi.toolkit.cli.impl.client.nifi.ProcessGroupClient;
+import org.glassfish.jersey.client.ClientConfig;
+import org.glassfish.jersey.client.ClientProperties;
+import 
org.glassfish.jersey.jackson.internal.jackson.jaxrs.json.JacksonJaxbJsonProvider;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.WebTarget;
+import java.io.IOException;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * Jersey implementation of NiFiClient.
+ */
+public class JerseyNiFiClient implements NiFiClient {
+
+    static final String NIFI_CONTEXT = "nifi-api";
+    static final int DEFAULT_CONNECT_TIMEOUT = 10000;
+    static final int DEFAULT_READ_TIMEOUT = 10000;
+
+    static final String AUTHORIZATION_HEADER = "Authorization";
+    static final String BEARER = "Bearer";
+
+    private final Client client;
+    private final WebTarget baseTarget;
+
+    private JerseyNiFiClient(final Builder builder) {
+        final NiFiClientConfig clientConfig = builder.getConfig();
+        if (clientConfig == null) {
+            throw new IllegalArgumentException("NiFiClientConfig cannot be 
null");
+        }
+
+        String baseUrl = clientConfig.getBaseUrl();
+        if (StringUtils.isBlank(baseUrl)) {
+            throw new IllegalArgumentException("Base URL cannot be blank");
+        }
+
+        if (baseUrl.endsWith("/")) {
+            baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
+        }
+
+        if (!baseUrl.endsWith(NIFI_CONTEXT)) {
+            baseUrl = baseUrl + "/" + NIFI_CONTEXT;
+        }
+
+        try {
+            new URI(baseUrl);
+        } catch (final Exception e) {
+            throw new IllegalArgumentException("Invalid base URL: " + 
e.getMessage(), e);
+        }
+
+        final SSLContext sslContext = clientConfig.getSslContext();
+        final HostnameVerifier hostnameVerifier = 
clientConfig.getHostnameVerifier();
+
+        final ClientBuilder clientBuilder = ClientBuilder.newBuilder();
+        if (sslContext != null) {
+            clientBuilder.sslContext(sslContext);
+        }
+        if (hostnameVerifier != null) {
+            clientBuilder.hostnameVerifier(hostnameVerifier);
+        }
+
+        final int connectTimeout = clientConfig.getConnectTimeout() == null ? 
DEFAULT_CONNECT_TIMEOUT : clientConfig.getConnectTimeout();
+        final int readTimeout = clientConfig.getReadTimeout() == null ? 
DEFAULT_READ_TIMEOUT : clientConfig.getReadTimeout();
+
+        final ClientConfig jerseyClientConfig = new ClientConfig();
+        jerseyClientConfig.property(ClientProperties.CONNECT_TIMEOUT, 
connectTimeout);
+        jerseyClientConfig.property(ClientProperties.READ_TIMEOUT, 
readTimeout);
+        jerseyClientConfig.register(jacksonJaxbJsonProvider());
+        clientBuilder.withConfig(jerseyClientConfig);
+        this.client = clientBuilder.build();
+
+        this.baseTarget = client.target(baseUrl);
+    }
+
+    @Override
+    public ControllerClient getControllerClient() {
+        return new JerseyControllerClient(baseTarget);
+    }
+
+    @Override
+    public ControllerClient getControllerClientForProxiedEntities(final 
String... proxiedEntity) {
+        final Map<String,String> headers = getHeaders(proxiedEntity);
+        return new JerseyControllerClient(baseTarget, headers);
+    }
+
+    @Override
+    public ControllerClient getControllerClientForToken(final String 
base64token) {
+        final Map<String,String> headers = getHeadersWithToken(base64token);
+        return new JerseyControllerClient(baseTarget, headers);
+    }
+
+    @Override
+    public FlowClient getFlowClient() {
+        return new JerseyFlowClient(baseTarget);
+    }
+
+    @Override
+    public FlowClient getFlowClientForProxiedEntities(String... proxiedEntity) 
{
+        final Map<String,String> headers = getHeaders(proxiedEntity);
+        return new JerseyFlowClient(baseTarget, headers);
+    }
+
+    @Override
+    public FlowClient getFlowClientForToken(String base64token) {
+        final Map<String,String> headers = getHeadersWithToken(base64token);
+        return new JerseyFlowClient(baseTarget, headers);
+    }
+
+    @Override
+    public ProcessGroupClient getProcessGroupClient() {
+        return new JerseyProcessGroupClient(baseTarget);
+    }
+
+    @Override
+    public ProcessGroupClient 
getProcessGroupClientForProxiedEntities(String... proxiedEntity) {
+        final Map<String,String> headers = getHeaders(proxiedEntity);
+        return new JerseyProcessGroupClient(baseTarget, headers);
+    }
+
+    @Override
+    public ProcessGroupClient getProcessGroupClientForToken(String 
base64token) {
+        final Map<String,String> headers = getHeadersWithToken(base64token);
+        return new JerseyProcessGroupClient(baseTarget, headers);
+    }
+
+    @Override
+    public void close() throws IOException {
+        if (this.client != null) {
+            try {
+                this.client.close();
+            } catch (Exception e) {
+
+            }
+        }
+    }
+
+    private Map<String,String> getHeadersWithToken(final String base64token) {
+        if (StringUtils.isBlank(base64token)) {
+            throw new IllegalArgumentException("Token cannot be null");
+        }
+
+        final Map<String,String> headers = new HashMap<>();
+        headers.put(AUTHORIZATION_HEADER, BEARER + " " + base64token);
+        return headers;
+    }
+
+    private Map<String,String> getHeaders(final String[] proxiedEntities) {
+        final String proxiedEntitiesValue = 
getProxiedEntitesValue(proxiedEntities);
+
+        final Map<String,String> headers = new HashMap<>();
+        if (proxiedEntitiesValue != null) {
+            headers.put(ProxiedEntitiesUtils.PROXY_ENTITIES_CHAIN, 
proxiedEntitiesValue);
+        }
+        return headers;
+    }
+
+    private String getProxiedEntitesValue(final String[] proxiedEntities) {
+        if (proxiedEntities == null) {
+            return null;
+        }
+
+        final List<String> proxiedEntityChain = Arrays.stream(proxiedEntities)
+                
.map(ProxiedEntitiesUtils::formatProxyDn).collect(Collectors.toList());
+        return StringUtils.join(proxiedEntityChain, "");
+    }
+
+    /**
+     * Builder for creating a JerseyNiFiClient.
+     */
+    public static class Builder implements NiFiClient.Builder {
+
+        private NiFiClientConfig clientConfig;
+
+        @Override
+        public JerseyNiFiClient.Builder config(final NiFiClientConfig 
clientConfig) {
+            this.clientConfig = clientConfig;
+            return this;
+        }
+
+        @Override
+        public NiFiClientConfig getConfig() {
+            return clientConfig;
+        }
+
+        @Override
+        public NiFiClient build() {
+            return new JerseyNiFiClient(this);
+        }
+
+    }
+
+    private static JacksonJaxbJsonProvider jacksonJaxbJsonProvider() {
+        JacksonJaxbJsonProvider jacksonJaxbJsonProvider = new 
JacksonJaxbJsonProvider();
+
+        ObjectMapper mapper = new ObjectMapper();
+        
mapper.setDefaultPropertyInclusion(JsonInclude.Value.construct(JsonInclude.Include.NON_NULL,
 JsonInclude.Include.NON_NULL));
+        mapper.setAnnotationIntrospector(new 
JaxbAnnotationIntrospector(mapper.getTypeFactory()));
+        // Ignore unknown properties so that deployed client remain compatible 
with future versions of NiFi that add new fields
+        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, 
false);
+
+        jacksonJaxbJsonProvider.setMapper(mapper);
+        return jacksonJaxbJsonProvider;
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/9cf9e866/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/impl/JerseyProcessGroupClient.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/impl/JerseyProcessGroupClient.java
 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/impl/JerseyProcessGroupClient.java
new file mode 100644
index 0000000..65bac4f
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/client/nifi/impl/JerseyProcessGroupClient.java
@@ -0,0 +1,121 @@
+/*
+ * 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.toolkit.cli.impl.client.nifi.impl;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.toolkit.cli.impl.client.nifi.NiFiClientException;
+import org.apache.nifi.toolkit.cli.impl.client.nifi.ProcessGroupClient;
+import org.apache.nifi.web.api.entity.ProcessGroupEntity;
+import org.apache.nifi.web.api.entity.VariableRegistryEntity;
+
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.MediaType;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Jersey implementation of ProcessGroupClient.
+ */
+public class JerseyProcessGroupClient extends AbstractJerseyClient implements 
ProcessGroupClient {
+
+    private final WebTarget processGroupsTarget;
+
+    public JerseyProcessGroupClient(final WebTarget baseTarget) {
+        this(baseTarget, Collections.emptyMap());
+    }
+
+    public JerseyProcessGroupClient(final WebTarget baseTarget, final 
Map<String,String> headers) {
+        super(headers);
+        this.processGroupsTarget = baseTarget.path("/process-groups");
+    }
+
+    @Override
+    public ProcessGroupEntity createProcessGroup(final String parentGroupdId, 
final ProcessGroupEntity entity)
+            throws NiFiClientException, IOException {
+
+        if (StringUtils.isBlank(parentGroupdId)) {
+            throw new IllegalArgumentException("Parent process group id cannot 
be null or blank");
+        }
+
+        if (entity == null){
+            throw new IllegalArgumentException("Process group entity cannot be 
null");
+        }
+
+        return executeAction("Error creating process group", () -> {
+            final WebTarget target = processGroupsTarget
+                    .path("{id}/process-groups")
+                    .resolveTemplate("id", parentGroupdId);
+
+            return getRequestBuilder(target).post(
+                    Entity.entity(entity, MediaType.APPLICATION_JSON_TYPE),
+                    ProcessGroupEntity.class
+            );
+        });
+    }
+
+    @Override
+    public ProcessGroupEntity getProcessGroup(final String processGroupId) 
throws NiFiClientException, IOException {
+        if (StringUtils.isBlank(processGroupId)) {
+            throw new IllegalArgumentException("Process group id cannot be 
null or blank");
+        }
+
+        return executeAction("Error getting process group", () -> {
+            final WebTarget target = processGroupsTarget
+                    .path("{id}")
+                    .resolveTemplate("id", processGroupId);
+
+            return getRequestBuilder(target).get(ProcessGroupEntity.class);
+        });
+    }
+
+    @Override
+    public ProcessGroupEntity updateProcessGroup(final ProcessGroupEntity 
entity)
+            throws NiFiClientException, IOException {
+
+        if (entity == null){
+            throw new IllegalArgumentException("Process group entity cannot be 
null");
+        }
+
+        return executeAction("Error updating process group", () -> {
+            final WebTarget target = processGroupsTarget
+                    .path("{id}")
+                    .resolveTemplate("id", entity.getId());
+
+            return getRequestBuilder(target).put(
+                    Entity.entity(entity, MediaType.APPLICATION_JSON_TYPE),
+                    ProcessGroupEntity.class
+            );
+        });
+    }
+
+    @Override
+    public VariableRegistryEntity getVariables(final String processGroupId) 
throws NiFiClientException, IOException {
+        if (StringUtils.isBlank(processGroupId)) {
+            throw new IllegalArgumentException("Parent process group id cannot 
be null or blank");
+        }
+
+        return executeAction("Error getting variables for process group", () 
-> {
+            final WebTarget target = processGroupsTarget
+                    .path("{id}/variable-registry")
+                    .resolveTemplate("id", processGroupId);
+
+            return getRequestBuilder(target).get(VariableRegistryEntity.class);
+        });
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/9cf9e866/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/AbstractCommand.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/AbstractCommand.java
 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/AbstractCommand.java
new file mode 100644
index 0000000..0b82947
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/AbstractCommand.java
@@ -0,0 +1,220 @@
+/*
+ * 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.toolkit.cli.impl.command;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectWriter;
+import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.MissingOptionException;
+import org.apache.commons.cli.Option;
+import org.apache.commons.cli.Options;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.Validate;
+import org.apache.nifi.toolkit.cli.api.Command;
+import org.apache.nifi.toolkit.cli.api.Context;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.util.Properties;
+
+/**
+ * Base class for all commands.
+ */
+public abstract class AbstractCommand implements Command {
+
+    protected static final ObjectMapper MAPPER = new ObjectMapper();
+    static {
+        MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+        
MAPPER.setDefaultPropertyInclusion(JsonInclude.Value.construct(JsonInclude.Include.NON_NULL,
 JsonInclude.Include.NON_NULL));
+        MAPPER.setAnnotationIntrospector(new 
JaxbAnnotationIntrospector(MAPPER.getTypeFactory()));
+        MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, 
false);
+    }
+
+    protected static final ObjectWriter OBJECT_WRITER = 
MAPPER.writerWithDefaultPrettyPrinter();
+
+    private final String name;
+    private final Options options;
+
+    private Context context;
+    private PrintStream output;
+
+    public AbstractCommand(final String name) {
+        this.name = name;
+        Validate.notNull(this.name);
+
+        this.options = new Options();
+
+        this.options.addOption(CommandOption.URL.createOption());
+        this.options.addOption(CommandOption.PROPERTIES.createOption());
+
+        this.options.addOption(CommandOption.KEYSTORE.createOption());
+        this.options.addOption(CommandOption.KEYSTORE_TYPE.createOption());
+        this.options.addOption(CommandOption.KEYSTORE_PASSWORD.createOption());
+        this.options.addOption(CommandOption.KEY_PASSWORD.createOption());
+
+        this.options.addOption(CommandOption.TRUSTSTORE.createOption());
+        this.options.addOption(CommandOption.TRUSTSTORE_TYPE.createOption());
+        
this.options.addOption(CommandOption.TRUSTSTORE_PASSWORD.createOption());
+
+        this.options.addOption(CommandOption.PROXIED_ENTITY.createOption());
+
+        this.options.addOption(CommandOption.VERBOSE.createOption());
+        this.options.addOption(CommandOption.HELP.createOption());
+    }
+
+    @Override
+    public final void initialize(final Context context) {
+        Validate.notNull(context);
+        Validate.notNull(context.getOutput());
+        this.context = context;
+        this.output = context.getOutput();
+        this.doInitialize(context);
+    }
+
+    protected void doInitialize(final Context context) {
+        // sub-classes can override to do additional things like add options
+    }
+
+    protected void addOption(final Option option) {
+        this.options.addOption(option);
+    }
+
+    protected Context getContext() {
+        return this.context;
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public Options getOptions() {
+        return options;
+    }
+
+    @Override
+    public void printUsage(String errorMessage) {
+        output.println();
+
+        if (errorMessage != null) {
+            output.println("ERROR: " + errorMessage);
+            output.println();
+        }
+
+        final PrintWriter printWriter = new PrintWriter(output);
+
+        final HelpFormatter hf = new HelpFormatter();
+        hf.setWidth(160);
+        hf.printHelp(printWriter, hf.getWidth(), getName(), null, getOptions(),
+                hf.getLeftPadding(), hf.getDescPadding(), null, false);
+
+        printWriter.println();
+        printWriter.flush();
+    }
+
+
+    protected void print(final String val) {
+        output.print(val);
+    }
+
+    protected void println(final String val) {
+        output.println(val);
+    }
+
+    protected void println() {
+        output.println();
+    }
+
+    protected void writeResult(final Properties properties, final Object 
result) throws IOException {
+        if (properties.containsKey(CommandOption.OUTPUT_FILE.getLongName())) {
+            final String outputFile = 
properties.getProperty(CommandOption.OUTPUT_FILE.getLongName());
+            try (final OutputStream resultOut = new 
FileOutputStream(outputFile)) {
+                OBJECT_WRITER.writeValue(resultOut, result);
+            }
+        } else {
+            OBJECT_WRITER.writeValue(new OutputStream() {
+                @Override
+                public void write(byte[] b) throws IOException {
+                    output.write(b);
+                }
+
+                @Override
+                public void write(byte[] b, int off, int len) throws 
IOException {
+                    output.write(b, off, len);
+                }
+
+                @Override
+                public void write(int b) throws IOException {
+                    output.write(b);
+                }
+
+                @Override
+                public void close() throws IOException {
+                    // DON'T close the output stream here
+                    output.flush();
+                }
+            }, result);
+        }
+
+    }
+
+    protected String getArg(final Properties properties, final CommandOption 
option) {
+        return properties.getProperty(option.getLongName());
+    }
+
+    protected String getRequiredArg(final Properties properties, final 
CommandOption option) throws MissingOptionException {
+        final String argValue = properties.getProperty(option.getLongName());
+        if (StringUtils.isBlank(argValue)) {
+            throw new MissingOptionException("Missing required option '" + 
option.getLongName() + "'");
+        }
+        return argValue;
+    }
+
+    protected Integer getIntArg(final Properties properties, final 
CommandOption option) throws MissingOptionException {
+        final String argValue = properties.getProperty(option.getLongName());
+        if (StringUtils.isBlank(argValue)) {
+            return null;
+        }
+
+        try {
+            return Integer.valueOf(argValue);
+        } catch (Exception e) {
+            throw new MissingOptionException("Version must be numeric: " + 
argValue);
+        }
+    }
+
+    protected Integer getRequiredIntArg(final Properties properties, final 
CommandOption option) throws MissingOptionException {
+        final String argValue = properties.getProperty(option.getLongName());
+        if (StringUtils.isBlank(argValue)) {
+            throw new MissingOptionException("Missing required option '" + 
option.getLongName() + "'");
+        }
+
+        try {
+            return Integer.valueOf(argValue);
+        } catch (Exception e) {
+            throw new MissingOptionException("Version must be numeric: " + 
argValue);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/9cf9e866/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/AbstractCommandGroup.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/AbstractCommandGroup.java
 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/AbstractCommandGroup.java
new file mode 100644
index 0000000..f597365
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/AbstractCommandGroup.java
@@ -0,0 +1,73 @@
+/*
+ * 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.toolkit.cli.impl.command;
+
+import org.apache.commons.lang3.Validate;
+import org.apache.nifi.toolkit.cli.api.Command;
+import org.apache.nifi.toolkit.cli.api.CommandGroup;
+import org.apache.nifi.toolkit.cli.api.Context;
+
+import java.io.PrintStream;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Base class for CommandGroups to extend from.
+ */
+public abstract class AbstractCommandGroup implements CommandGroup {
+
+    private final String name;
+    private PrintStream output;
+    private List<Command> commands;
+
+    public AbstractCommandGroup(final String name) {
+        this.name = name;
+        Validate.notBlank(this.name);
+    }
+
+    @Override
+    public final void initialize(final Context context) {
+        Validate.notNull(context);
+        this.output = context.getOutput();
+        this.commands = Collections.unmodifiableList(createCommands());
+        this.commands.stream().forEach(c -> c.initialize(context));
+    }
+
+    /**
+     * Sub-classes override to provide the appropriate commands for the given 
group.
+     *
+     * @return the list of commands for this group
+     */
+    protected abstract List<Command> createCommands();
+
+    @Override
+    public String getName() {
+        return this.name;
+    }
+
+    @Override
+    public List<Command> getCommands() {
+        return this.commands;
+    }
+
+    @Override
+    public void printUsage() {
+        commands.stream().forEach(c -> output.println("\t" + getName() + " " + 
c.getName()));
+        output.flush();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/9cf9e866/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/AbstractPropertyCommand.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/AbstractPropertyCommand.java
 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/AbstractPropertyCommand.java
new file mode 100644
index 0000000..fb4dc7f
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/AbstractPropertyCommand.java
@@ -0,0 +1,95 @@
+/*
+ * 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.toolkit.cli.impl.command;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.Option;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.nifi.toolkit.cli.api.CommandException;
+import org.apache.nifi.toolkit.cli.api.Session;
+import org.apache.nifi.toolkit.cli.impl.session.SessionVariables;
+
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.util.Properties;
+
+/**
+ * Base class for commands that support loading properties from the session or 
an argument.
+ */
+public abstract class AbstractPropertyCommand extends AbstractCommand {
+
+    public AbstractPropertyCommand(String name) {
+        super(name);
+    }
+
+    @Override
+    public void execute(final CommandLine commandLine) throws CommandException 
{
+        try {
+            final Properties properties = new Properties();
+
+            // start by loading the properties file if it was specified
+            if (commandLine.hasOption(CommandOption.PROPERTIES.getLongName())) 
{
+                final String propertiesFile = 
commandLine.getOptionValue(CommandOption.PROPERTIES.getLongName());
+                if (!StringUtils.isBlank(propertiesFile)) {
+                    try (final InputStream in = new 
FileInputStream(propertiesFile)) {
+                        properties.load(in);
+                    }
+                }
+            } else {
+                // no properties file was specified so see if there is 
anything in the session
+                final SessionVariables sessionVariable = 
getPropertiesSessionVariable();
+                if (sessionVariable != null) {
+                    final Session session = getContext().getSession();
+                    final String sessionPropsFiles = 
session.get(sessionVariable.getVariableName());
+                    if (!StringUtils.isBlank(sessionPropsFiles)) {
+                        try (final InputStream in = new 
FileInputStream(sessionPropsFiles)) {
+                            properties.load(in);
+                        }
+                    }
+                }
+            }
+
+            // add in anything specified on command line, and override 
anything that was already there
+            for (final Option option : commandLine.getOptions()) {
+                final String optValue = option.getValue() == null ? "" : 
option.getValue();
+                properties.setProperty(option.getLongOpt(), optValue);
+            }
+
+            // delegate to sub-classes
+            doExecute(properties);
+
+        } catch (CommandException ce) {
+            throw ce;
+        } catch (Exception e) {
+            throw new CommandException("Error executing command '" + getName() 
+ "' : " + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * @return the SessionVariables that specifies the properties file for 
this command, or null if not supported
+     */
+    protected abstract SessionVariables getPropertiesSessionVariable();
+
+    /**
+     * Sub-classes implement specific command logic.
+     *
+     * @param properties the properties which represent the arguments
+     * @throws CommandException if an error occurrs
+     */
+    protected abstract void doExecute(final Properties properties) throws 
CommandException;
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/9cf9e866/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/CommandFactory.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/CommandFactory.java
 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/CommandFactory.java
new file mode 100644
index 0000000..aec0ae4
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/CommandFactory.java
@@ -0,0 +1,67 @@
+/*
+ * 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.toolkit.cli.impl.command;
+
+import org.apache.nifi.toolkit.cli.api.Command;
+import org.apache.nifi.toolkit.cli.api.CommandGroup;
+import org.apache.nifi.toolkit.cli.api.Context;
+import org.apache.nifi.toolkit.cli.impl.command.misc.Exit;
+import org.apache.nifi.toolkit.cli.impl.command.misc.Help;
+import org.apache.nifi.toolkit.cli.impl.command.nifi.NiFiCommandGroup;
+import 
org.apache.nifi.toolkit.cli.impl.command.registry.NiFiRegistryCommandGroup;
+import org.apache.nifi.toolkit.cli.impl.command.session.SessionCommandGroup;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * Creates and initializes all of the available commands.
+ */
+public class CommandFactory {
+
+    public static Map<String,Command> createTopLevelCommands(final Context 
context) {
+        final List<Command> commandList = new ArrayList<>();
+        commandList.add(new Help());
+        commandList.add(new Exit());
+
+        final Map<String,Command> commandMap = new TreeMap<>();
+        commandList.stream().forEach(cmd -> {
+            cmd.initialize(context);
+            commandMap.put(cmd.getName(), cmd);
+        });
+
+        return Collections.unmodifiableMap(commandMap);
+    }
+
+    public static Map<String,CommandGroup> createCommandGroups(final Context 
context) {
+
+        final List<CommandGroup> groups = new ArrayList<>();
+        groups.add(new NiFiRegistryCommandGroup());
+        groups.add(new NiFiCommandGroup());
+        groups.add(new SessionCommandGroup());
+
+        final Map<String,CommandGroup> groupMap = new TreeMap<>();
+        groups.stream().forEach(g -> {
+            g.initialize(context);
+            groupMap.put(g.getName(), g);
+        });
+        return Collections.unmodifiableMap(groupMap);
+    }
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/9cf9e866/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/CommandOption.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/CommandOption.java
 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/CommandOption.java
new file mode 100644
index 0000000..0b14ee3
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/CommandOption.java
@@ -0,0 +1,102 @@
+/*
+ * 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.toolkit.cli.impl.command;
+
+import org.apache.commons.cli.Option;
+
+/**
+ * All possible options for commands.
+ */
+public enum CommandOption {
+
+    // General
+    URL("u", "baseUrl", "The URL to execute the command against", true),
+    INPUT_FILE("i", "inputFile", "A file to read as input, must contain full 
path and filename", true),
+    OUTPUT_FILE("o", "outputFile", "A file to write output to, must contain 
full path and filename", true),
+    PROPERTIES("p", "properties", "A properties file to load arguments from, " 
+
+            "command line values will override anything in the properties 
file, must contain full path to file", true),
+
+    // Registry - Buckets
+    BUCKET_ID("b", "bucketIdentifier", "A bucket identifier", true),
+    BUCKET_NAME("bn", "bucketName", "A bucket name", true),
+    BUCKET_DESC("bd", "bucketDesc", "A bucket description", true),
+
+    // Registry - Flows
+    FLOW_ID("f", "flowIdentifier", "A flow identifier", true),
+    FLOW_NAME("fn", "flowName", "A flow name", true),
+    FLOW_DESC("fd", "flowDesc", "A flow description", true),
+    FLOW_VERSION("fv", "flowVersion", "A version of a flow", true),
+
+    // NiFi - Registries
+    REGISTRY_CLIENT_ID("rcid", "registryClientId", "The id of a registry 
client", true),
+    REGISTRY_CLIENT_NAME("rcn", "registryClientName", "The name of the 
registry client", true),
+    REGISTRY_CLIENT_URL("rcu", "registryClientUrl", "The url of the registry 
client", true),
+    REGISTRY_CLIENT_DESC("rcd", "registryClientDesc", "The description of the 
registry client", true),
+
+    // NiFi - PGs
+    PG_ID("pgid", "processGroupId", "The id of a process group", true),
+    PG_NAME("pgn", "processGroupName", "The name of a process group", true),
+
+    // NiFi - Pos
+    POS_X("px", "posX", "The X coordinate", true),
+    POS_Y("py", "posY", "The Y coordinate", true),
+
+    // Security related
+    KEYSTORE("ks", "keystore", "A keystore to use for TLS/SSL connections", 
true),
+    KEYSTORE_TYPE("kst", "keystoreType", "The type of key store being used 
(JKS or PKCS12)", true),
+    KEYSTORE_PASSWORD("ksp", "keystorePasswd", "The password of the keystore 
being used", true),
+    KEY_PASSWORD("kp", "keyPasswd", "The key password of the keystore being 
used", true),
+    TRUSTSTORE("ts", "truststore", "A truststore to use for TLS/SSL 
connections", true),
+    TRUSTSTORE_TYPE("tst", "truststoreType", "The type of trust store being 
used (JKS or PKCS12)", true),
+    TRUSTSTORE_PASSWORD("tsp", "truststorePasswd", "The password of the 
truststore being used", true),
+    PROXIED_ENTITY("pe", "proxiedEntity", "The identity of an entity to 
proxy", true),
+    PROTOCOL("pro", "protocol", "The security protocol to use, such as 
TLSv.1.2", true),
+
+    // Miscellaneous
+    VERBOSE("verbose", "verbose", "Indicates that verbose output should be 
provided", false),
+    HELP("h", "help", "Help", false)
+    ;
+
+    private final String shortName;
+    private final String longName;
+    private final String description;
+    private final boolean hasArg;
+
+    CommandOption(final String shortName, final String longName, final String 
description, final boolean hasArg) {
+        this.shortName = shortName;
+        this.longName = longName;
+        this.description = description;
+        this.hasArg = hasArg;
+    }
+
+    public String getShortName() {
+        return shortName;
+    }
+
+    public String getLongName() {
+        return longName;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+
+    public Option createOption() {
+        return 
Option.builder(shortName).longOpt(longName).desc(description).hasArg(hasArg).build();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/9cf9e866/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/CommandProcessor.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/CommandProcessor.java
 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/CommandProcessor.java
new file mode 100644
index 0000000..b87b80a
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/CommandProcessor.java
@@ -0,0 +1,180 @@
+/*
+ * 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.toolkit.cli.impl.command;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.CommandLineParser;
+import org.apache.commons.cli.DefaultParser;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.apache.commons.lang3.Validate;
+import org.apache.nifi.toolkit.cli.api.Command;
+import org.apache.nifi.toolkit.cli.api.CommandGroup;
+import org.apache.nifi.toolkit.cli.api.Context;
+
+import java.io.PrintStream;
+import java.util.Arrays;
+import java.util.Map;
+
+/**
+ * Takes the arguments from the shell and executes the appropriate command, or 
prints appropriate usage.
+ */
+public class CommandProcessor {
+
+    private final Map<String,Command> topLevelCommands;
+    private final Map<String,CommandGroup> commandGroups;
+    private final Context context;
+    private final PrintStream out;
+
+    public CommandProcessor(final Map<String,Command> topLevelCommands, final 
Map<String,CommandGroup> commandGroups, final Context context) {
+        this.topLevelCommands = topLevelCommands;
+        this.commandGroups = commandGroups;
+        this.context = context;
+        this.out = context.getOutput();
+        Validate.notNull(this.topLevelCommands);
+        Validate.notNull(this.commandGroups);
+        Validate.notNull(this.context);
+        Validate.notNull(this.out);
+    }
+
+    public void printBasicUsage(String errorMessage) {
+        out.println();
+
+        if (errorMessage != null) {
+            out.println("ERROR: " + errorMessage);
+            out.println();
+        }
+
+        out.println("commands:");
+        out.println();
+
+        commandGroups.entrySet().stream().forEach(e -> 
e.getValue().printUsage());
+        topLevelCommands.keySet().stream().forEach(k -> out.println("\t" + k));
+        out.println();
+    }
+
+    private CommandLine parseCli(Command command, String[] args) throws 
ParseException {
+        final Options options = command.getOptions();
+        final CommandLineParser parser = new DefaultParser();
+        final CommandLine commandLine = parser.parse(options, args);
+
+        if (commandLine.hasOption(CommandOption.HELP.getLongName())) {
+            command.printUsage(null);
+            return null;
+        }
+
+        return commandLine;
+    }
+
+    public void process(String[] args) {
+        if (args == null || args.length == 0
+                || (args.length == 1 && 
CommandOption.HELP.getLongName().equalsIgnoreCase(args[0]))) {
+            printBasicUsage(null);
+            return;
+        }
+
+        final String commandStr = args[0];
+        if (topLevelCommands.containsKey(commandStr)) {
+            processTopLevelCommand(commandStr, args);
+        } else if (commandGroups.containsKey(commandStr)) {
+            processGroupCommand(commandStr, args);
+        } else {
+            printBasicUsage("Unknown command '" + commandStr + "'");
+            return;
+        }
+    }
+
+    private void processTopLevelCommand(final String commandStr, final 
String[] args) {
+        try {
+            final Command command = topLevelCommands.get(commandStr);
+
+            final String[] otherArgs = Arrays.copyOfRange(args, 1, 
args.length, String[].class);
+            final CommandLine commandLine = parseCli(command, otherArgs);
+            if (commandLine == null) {
+                out.println("Unable to parse command line");
+                return;
+            }
+
+            try {
+                if (otherArgs.length == 1 && 
CommandOption.HELP.getLongName().equalsIgnoreCase(otherArgs[0])) {
+                    command.printUsage(null);
+                } else {
+                    command.execute(commandLine);
+                }
+            } catch (Exception e) {
+                command.printUsage(e.getMessage());
+                if 
(commandLine.hasOption(CommandOption.VERBOSE.getLongName())) {
+                    out.println();
+                    e.printStackTrace(out);
+                    out.println();
+                }
+            }
+
+        } catch (Exception e) {
+            out.println();
+            e.printStackTrace(out);
+            out.println();
+        }
+    }
+
+    private void processGroupCommand(final String commandGroupStr, final 
String[] args) {
+        if (args.length <= 1) {
+            printBasicUsage("No command provided to " + commandGroupStr);
+            return;
+        }
+
+        final String commandStr = args[1];
+        final CommandGroup commandGroup = commandGroups.get(commandGroupStr);
+        final Command command = commandGroup.getCommands().stream().filter(c 
-> c.getName().equals(commandStr)).findFirst().orElse(null);
+
+        if (command == null) {
+            printBasicUsage("Unknown command '" + commandGroupStr + " " + 
commandStr + "'");
+            return;
+        }
+
+        try {
+            final String[] otherArgs = Arrays.copyOfRange(args, 2, 
args.length, String[].class);
+            final CommandLine commandLine = parseCli(command, otherArgs);
+            if (commandLine == null) {
+                out.println("Unable to parse command line");
+                return;
+            }
+
+            try {
+                if (otherArgs.length == 1 && 
CommandOption.HELP.getLongName().equalsIgnoreCase(otherArgs[0])) {
+                    command.printUsage(null);
+                } else {
+                    command.execute(commandLine);
+                }
+            } catch (Exception e) {
+                command.printUsage(e.getMessage());
+                if 
(commandLine.hasOption(CommandOption.VERBOSE.getLongName())) {
+                    out.println();
+                    e.printStackTrace(out);
+                    out.println();
+                }
+            }
+
+        } catch (Exception e) {
+            out.println();
+            e.printStackTrace(out);
+            out.println();
+        }
+    }
+
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/9cf9e866/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/misc/Exit.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/misc/Exit.java
 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/misc/Exit.java
new file mode 100644
index 0000000..b10d17a
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/misc/Exit.java
@@ -0,0 +1,55 @@
+/*
+ * 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.toolkit.cli.impl.command.misc;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.Options;
+import org.apache.nifi.toolkit.cli.api.Command;
+import org.apache.nifi.toolkit.cli.api.CommandException;
+import org.apache.nifi.toolkit.cli.api.Context;
+
+/**
+ * Command for exiting the shell.
+ */
+public class Exit implements Command {
+
+    @Override
+    public void initialize(final Context context) {
+
+    }
+
+    @Override
+    public String getName() {
+        return "exit";
+    }
+
+    @Override
+    public Options getOptions() {
+        return new Options();
+    }
+
+    @Override
+    public void printUsage(String errorMessage) {
+
+    }
+
+    @Override
+    public void execute(final CommandLine cli) throws CommandException {
+        System.exit(0);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/9cf9e866/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/misc/Help.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/misc/Help.java
 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/misc/Help.java
new file mode 100644
index 0000000..cf3c558
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/misc/Help.java
@@ -0,0 +1,55 @@
+/*
+ * 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.toolkit.cli.impl.command.misc;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.Options;
+import org.apache.nifi.toolkit.cli.api.Command;
+import org.apache.nifi.toolkit.cli.api.CommandException;
+import org.apache.nifi.toolkit.cli.api.Context;
+
+/**
+ * Place-holder so "help" shows up in top-level commands.
+ */
+public class Help implements Command {
+
+    @Override
+    public void initialize(final Context context) {
+
+    }
+
+    @Override
+    public String getName() {
+        return "help";
+    }
+
+    @Override
+    public Options getOptions() {
+        return new Options();
+    }
+
+    @Override
+    public void printUsage(String errorMessage) {
+
+    }
+
+    @Override
+    public void execute(final CommandLine cli) throws CommandException {
+        // nothing to do
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/9cf9e866/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/nifi/AbstractNiFiCommand.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/nifi/AbstractNiFiCommand.java
 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/nifi/AbstractNiFiCommand.java
new file mode 100644
index 0000000..cfe3e53
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/nifi/AbstractNiFiCommand.java
@@ -0,0 +1,74 @@
+/*
+ * 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.toolkit.cli.impl.command.nifi;
+
+import org.apache.commons.cli.MissingOptionException;
+import org.apache.nifi.toolkit.cli.api.ClientFactory;
+import org.apache.nifi.toolkit.cli.api.CommandException;
+import org.apache.nifi.toolkit.cli.impl.client.nifi.NiFiClient;
+import org.apache.nifi.toolkit.cli.impl.client.nifi.NiFiClientException;
+import org.apache.nifi.toolkit.cli.impl.command.AbstractPropertyCommand;
+import org.apache.nifi.toolkit.cli.impl.session.SessionVariables;
+import org.apache.nifi.web.api.dto.RevisionDTO;
+
+import java.io.IOException;
+import java.util.Properties;
+
+/**
+ * Base class for all NiFi commands.
+ */
+public abstract class AbstractNiFiCommand extends AbstractPropertyCommand {
+
+    public AbstractNiFiCommand(final String name) {
+        super(name);
+    }
+
+    @Override
+    protected SessionVariables getPropertiesSessionVariable() {
+        return SessionVariables.NIFI_CLIENT_PROPS;
+    }
+
+    @Override
+    protected void doExecute(final Properties properties) throws 
CommandException {
+        final ClientFactory<NiFiClient> clientFactory = 
getContext().getNiFiClientFactory();
+        try (final NiFiClient client = clientFactory.createClient(properties)) 
{
+            doExecute(client, properties);
+        } catch (Exception e) {
+            throw new CommandException("Error executing command '" + getName() 
+ "' : " + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * Sub-classes implement to perform the desired action using the provided 
client and properties.
+     *
+     * @param client a NiFi client
+     * @param properties properties for the command
+     */
+    protected abstract void doExecute(final NiFiClient client, final 
Properties properties)
+            throws NiFiClientException, IOException, MissingOptionException, 
CommandException;
+
+
+    protected RevisionDTO getInitialRevisionDTO() {
+        final String clientId = getContext().getSession().getNiFiClientID();
+
+        final RevisionDTO revisionDTO = new RevisionDTO();
+        revisionDTO.setVersion(new Long(0));
+        revisionDTO.setClientId(clientId);
+        return revisionDTO;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/nifi/blob/9cf9e866/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/nifi/NiFiCommandGroup.java
----------------------------------------------------------------------
diff --git 
a/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/nifi/NiFiCommandGroup.java
 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/nifi/NiFiCommandGroup.java
new file mode 100644
index 0000000..ab9f011
--- /dev/null
+++ 
b/nifi-toolkit/nifi-toolkit-cli/src/main/java/org/apache/nifi/toolkit/cli/impl/command/nifi/NiFiCommandGroup.java
@@ -0,0 +1,57 @@
+/*
+ * 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.toolkit.cli.impl.command.nifi;
+
+import org.apache.nifi.toolkit.cli.api.Command;
+import org.apache.nifi.toolkit.cli.impl.command.AbstractCommandGroup;
+import org.apache.nifi.toolkit.cli.impl.command.nifi.flow.CurrentUser;
+import org.apache.nifi.toolkit.cli.impl.command.nifi.flow.GetRootId;
+import org.apache.nifi.toolkit.cli.impl.command.nifi.pg.PGGetVars;
+import org.apache.nifi.toolkit.cli.impl.command.nifi.pg.PGImport;
+import org.apache.nifi.toolkit.cli.impl.command.nifi.pg.PGStart;
+import org.apache.nifi.toolkit.cli.impl.command.nifi.pg.PGStop;
+import 
org.apache.nifi.toolkit.cli.impl.command.nifi.registry.CreateRegistryClient;
+import 
org.apache.nifi.toolkit.cli.impl.command.nifi.registry.ListRegistryClients;
+import 
org.apache.nifi.toolkit.cli.impl.command.nifi.registry.UpdateRegistryClient;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * CommandGroup for NiFi commands.
+ */
+public class NiFiCommandGroup extends AbstractCommandGroup {
+
+    public NiFiCommandGroup() {
+        super("nifi");
+    }
+
+    @Override
+    protected List<Command> createCommands() {
+        final List<AbstractNiFiCommand> commands = new ArrayList<>();
+        commands.add(new CurrentUser());
+        commands.add(new GetRootId());
+        commands.add(new ListRegistryClients());
+        commands.add(new CreateRegistryClient());
+        commands.add(new UpdateRegistryClient());
+        commands.add(new PGImport());
+        commands.add(new PGStart());
+        commands.add(new PGStop());
+        commands.add(new PGGetVars());
+        return new ArrayList<>(commands);
+    }
+}

Reply via email to