From b1970ecc1046955bcaa8ad7e4cfd9c3f92a8a306 Mon Sep 17 00:00:00 2001 From: Brandon DeVries Date: Tue, 7 Apr 2015 16:05:55 -0400 Subject: [PATCH 1/2] updating properties to support multiple nar libraries --- .../org/apache/nifi/util/NiFiProperties.java | 35 ++++- .../apache/nifi/util/NiFiPropertiesTest.java | 90 ++++++++++++ .../NiFiProperties/conf/nifi.blank.properties | 128 ++++++++++++++++++ .../conf/nifi.missing.properties | 126 +++++++++++++++++ .../NiFiProperties/conf/nifi.properties | 128 ++++++++++++++++++ 5 files changed, 501 insertions(+), 6 deletions(-) create mode 100644 nifi/nifi-commons/nifi-properties/src/test/java/org/apache/nifi/util/NiFiPropertiesTest.java create mode 100644 nifi/nifi-commons/nifi-properties/src/test/resources/NiFiProperties/conf/nifi.blank.properties create mode 100644 nifi/nifi-commons/nifi-properties/src/test/resources/NiFiProperties/conf/nifi.missing.properties create mode 100644 nifi/nifi-commons/nifi-properties/src/test/resources/NiFiProperties/conf/nifi.properties diff --git a/nifi/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java b/nifi/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java index 3b427a77ae..1704483e90 100644 --- a/nifi/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java +++ b/nifi/nifi-commons/nifi-properties/src/main/java/org/apache/nifi/util/NiFiProperties.java @@ -16,9 +16,6 @@ */ package org.apache.nifi.util; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; @@ -27,10 +24,15 @@ import java.net.InetSocketAddress; import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Properties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + public class NiFiProperties extends Properties { private static final long serialVersionUID = 2119177359005492702L; @@ -50,6 +52,7 @@ public class NiFiProperties extends Properties { public static final String AUTO_RESUME_STATE = "nifi.flowcontroller.autoResumeState"; public static final String FLOW_CONTROLLER_GRACEFUL_SHUTDOWN_PERIOD = "nifi.flowcontroller.graceful.shutdown.period"; public static final String NAR_LIBRARY_DIRECTORY = "nifi.nar.library.directory"; + public static final String NAR_LIBRARY_DIRECTORY_PREFIX = "nifi.nar.library.directory."; public static final String NAR_WORKING_DIRECTORY = "nifi.nar.working.directory"; public static final String COMPONENT_DOCS_DIRECTORY = "nifi.documentation.working.directory"; public static final String SENSITIVE_PROPS_KEY = "nifi.sensitive.props.key"; @@ -525,9 +528,29 @@ public class NiFiProperties extends Properties { return new File(getNarWorkingDirectory(), "extensions"); } - public File getNarLibraryDirectory() { - return new File(getProperty(NAR_LIBRARY_DIRECTORY, DEFAULT_NAR_LIBRARY_DIR)); - } + public List getNarLibraryDirectories() { + + List narLibraryPaths = new ArrayList<>(); + + // go through each property + for (String propertyName : stringPropertyNames()) { + // determine if the property is a nar library path + if (StringUtils.startsWith(propertyName, NAR_LIBRARY_DIRECTORY_PREFIX) || NAR_LIBRARY_DIRECTORY.equals(propertyName)) { + // attempt to resolve the path specified + String narLib = getProperty(propertyName); + if (!StringUtils.isBlank(narLib)){ + narLibraryPaths.add(Paths.get(narLib)); + } + } + } + + if (narLibraryPaths.isEmpty()){ + narLibraryPaths.add(Paths.get(DEFAULT_NAR_LIBRARY_DIR)); + } + + + return narLibraryPaths; + } // getters for ui properties // /** diff --git a/nifi/nifi-commons/nifi-properties/src/test/java/org/apache/nifi/util/NiFiPropertiesTest.java b/nifi/nifi-commons/nifi-properties/src/test/java/org/apache/nifi/util/NiFiPropertiesTest.java new file mode 100644 index 0000000000..3f1ff20bb8 --- /dev/null +++ b/nifi/nifi-commons/nifi-properties/src/test/java/org/apache/nifi/util/NiFiPropertiesTest.java @@ -0,0 +1,90 @@ +package org.apache.nifi.util; + +import static org.junit.Assert.assertEquals; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.nio.file.Path; +import java.util.List; + +import org.junit.Test; + +public class NiFiPropertiesTest { + + @Test + public void testProperties() { + + NiFiProperties properties = loadSpecifiedProperties("/NiFiProperties/conf/nifi.properties"); + + assertEquals("UI Banner Text", properties.getBannerText()); + + List directories = properties.getNarLibraryDirectories(); + + assertEquals(new File("./target/resources/NiFiProperties/lib/").getPath(), directories.get(0).toString()); + assertEquals(new File("./target/resources/NiFiProperties/lib2/").getPath(), directories.get(1).toString()); + + } + + @Test + public void testMissingProperties() { + + NiFiProperties properties = loadSpecifiedProperties("/NiFiProperties/conf/nifi.missing.properties"); + + List directories = properties.getNarLibraryDirectories(); + + assertEquals(1, directories.size()); + + assertEquals(new File(NiFiProperties.DEFAULT_NAR_LIBRARY_DIR).getPath(), directories.get(0).toString()); + + } + + @Test + public void testBlankProperties() { + + NiFiProperties properties = loadSpecifiedProperties("/NiFiProperties/conf/nifi.blank.properties"); + + List directories = properties.getNarLibraryDirectories(); + + assertEquals(1, directories.size()); + + assertEquals(new File(NiFiProperties.DEFAULT_NAR_LIBRARY_DIR).getPath(), directories.get(0).toString()); + + } + + private NiFiProperties loadSpecifiedProperties(String propertiesFile) { + + String file = NiFiPropertiesTest.class.getResource(propertiesFile).getFile(); + + System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, file); + + NiFiProperties properties = NiFiProperties.getInstance(); + + // clear out existing properties + for (String prop : properties.stringPropertyNames()) { + properties.remove(prop); + } + + InputStream inStream = null; + try { + inStream = new BufferedInputStream(new FileInputStream(file)); + properties.load(inStream); + } catch (final Exception ex) { + throw new RuntimeException("Cannot load properties file due to " + ex.getLocalizedMessage(), ex); + } finally { + if (null != inStream) { + try { + inStream.close(); + } catch (final Exception ex) { + /** + * do nothing * + */ + } + } + } + + return properties; + } + +} diff --git a/nifi/nifi-commons/nifi-properties/src/test/resources/NiFiProperties/conf/nifi.blank.properties b/nifi/nifi-commons/nifi-properties/src/test/resources/NiFiProperties/conf/nifi.blank.properties new file mode 100644 index 0000000000..1f853b97ec --- /dev/null +++ b/nifi/nifi-commons/nifi-properties/src/test/resources/NiFiProperties/conf/nifi.blank.properties @@ -0,0 +1,128 @@ +# 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. + +# Core Properties # +nifi.version=nifi-test 3.0.0 +nifi.flow.configuration.file=./target/flow.xml.gz +nifi.flow.configuration.archive.dir=./target/archive/ +nifi.flowcontroller.autoResumeState=true +nifi.flowcontroller.graceful.shutdown.period=10 sec +nifi.flowservice.writedelay.interval=2 sec +nifi.administrative.yield.duration=30 sec + +nifi.reporting.task.configuration.file=./target/reporting-tasks.xml +nifi.controller.service.configuration.file=./target/controller-services.xml +nifi.templates.directory=./target/templates +nifi.ui.banner.text=UI Banner Text +nifi.ui.autorefresh.interval=30 sec +nifi.nar.library.directory= +nifi.custom.nar.library.directory.alt= +nifi.nar.working.directory=./target/work/nar/ + +# H2 Settings +nifi.database.directory=./target/database_repository +nifi.h2.url.append=;LOCK_TIMEOUT=25000;WRITE_DELAY=0;AUTO_SERVER=FALSE + +# FlowFile Repository +nifi.flowfile.repository.directory=./target/test-repo +nifi.flowfile.repository.partitions=1 +nifi.flowfile.repository.checkpoint.interval=2 mins +nifi.queue.swap.threshold=20000 +nifi.swap.storage.directory=./target/test-repo/swap +nifi.swap.in.period=5 sec +nifi.swap.in.threads=1 +nifi.swap.out.period=5 sec +nifi.swap.out.threads=4 + +# Content Repository +nifi.content.claim.max.appendable.size=10 MB +nifi.content.claim.max.flow.files=100 +nifi.content.repository.directory.default=./target/content_repository + +# Provenance Repository Properties +nifi.provenance.repository.storage.directory=./target/provenance_repository +nifi.provenance.repository.max.storage.time=24 hours +nifi.provenance.repository.max.storage.size=1 GB +nifi.provenance.repository.rollover.time=5 mins +nifi.provenance.repository.rollover.size=100 MB + +# Site to Site properties +nifi.remote.input.socket.port=9990 +nifi.remote.input.secure=true + +# web properties # +nifi.web.war.directory=./target/lib +nifi.web.http.host= +nifi.web.http.port=8080 +nifi.web.https.host= +nifi.web.https.port= +nifi.web.jetty.working.directory=./target/work/jetty + +# security properties # +nifi.sensitive.props.key=key +nifi.sensitive.props.algorithm=PBEWITHMD5AND256BITAES-CBC-OPENSSL +nifi.sensitive.props.provider=BC + +nifi.security.keystore= +nifi.security.keystoreType= +nifi.security.keystorePasswd= +nifi.security.keyPasswd= +nifi.security.truststore= +nifi.security.truststoreType= +nifi.security.truststorePasswd= +nifi.security.needClientAuth= +nifi.security.authorizedUsers.file=./target/conf/authorized-users.xml +nifi.security.user.credential.cache.duration=24 hours +nifi.security.user.authority.provider=nifi.authorization.FileAuthorizationProvider +nifi.security.support.new.account.requests= +nifi.security.default.user.roles= + +# cluster common properties (cluster manager and nodes must have same values) # +nifi.cluster.protocol.heartbeat.interval=5 sec +nifi.cluster.protocol.is.secure=false +nifi.cluster.protocol.socket.timeout=30 sec +nifi.cluster.protocol.connection.handshake.timeout=45 sec +# if multicast is used, then nifi.cluster.protocol.multicast.xxx properties must be configured # +nifi.cluster.protocol.use.multicast=false +nifi.cluster.protocol.multicast.address= +nifi.cluster.protocol.multicast.port= +nifi.cluster.protocol.multicast.service.broadcast.delay=500 ms +nifi.cluster.protocol.multicast.service.locator.attempts=3 +nifi.cluster.protocol.multicast.service.locator.attempts.delay=1 sec + +# cluster node properties (only configure for cluster nodes) # +nifi.cluster.is.node=false +nifi.cluster.node.address= +nifi.cluster.node.protocol.port= +nifi.cluster.node.protocol.threads=2 +# if multicast is not used, nifi.cluster.node.unicast.xxx must have same values as nifi.cluster.manager.xxx # +nifi.cluster.node.unicast.manager.address= +nifi.cluster.node.unicast.manager.protocol.port= +nifi.cluster.node.unicast.manager.authority.provider.port= + +# cluster manager properties (only configure for cluster manager) # +nifi.cluster.is.manager=false +nifi.cluster.manager.address= +nifi.cluster.manager.protocol.port= +nifi.cluster.manager.authority.provider.port= +nifi.cluster.manager.authority.provider.threads=10 +nifi.cluster.manager.node.firewall.file= +nifi.cluster.manager.node.event.history.size=10 +nifi.cluster.manager.node.api.connection.timeout=30 sec +nifi.cluster.manager.node.api.read.timeout=30 sec +nifi.cluster.manager.node.api.request.threads=10 +nifi.cluster.manager.flow.retrieval.delay=5 sec +nifi.cluster.manager.protocol.threads=10 +nifi.cluster.manager.safemode.duration=0 sec diff --git a/nifi/nifi-commons/nifi-properties/src/test/resources/NiFiProperties/conf/nifi.missing.properties b/nifi/nifi-commons/nifi-properties/src/test/resources/NiFiProperties/conf/nifi.missing.properties new file mode 100644 index 0000000000..dbb8978c6b --- /dev/null +++ b/nifi/nifi-commons/nifi-properties/src/test/resources/NiFiProperties/conf/nifi.missing.properties @@ -0,0 +1,126 @@ +# 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. + +# Core Properties # +nifi.version=nifi-test 3.0.0 +nifi.flow.configuration.file=./target/flow.xml.gz +nifi.flow.configuration.archive.dir=./target/archive/ +nifi.flowcontroller.autoResumeState=true +nifi.flowcontroller.graceful.shutdown.period=10 sec +nifi.flowservice.writedelay.interval=2 sec +nifi.administrative.yield.duration=30 sec + +nifi.reporting.task.configuration.file=./target/reporting-tasks.xml +nifi.controller.service.configuration.file=./target/controller-services.xml +nifi.templates.directory=./target/templates +nifi.ui.banner.text=UI Banner Text +nifi.ui.autorefresh.interval=30 sec +nifi.nar.working.directory=./target/work/nar/ + +# H2 Settings +nifi.database.directory=./target/database_repository +nifi.h2.url.append=;LOCK_TIMEOUT=25000;WRITE_DELAY=0;AUTO_SERVER=FALSE + +# FlowFile Repository +nifi.flowfile.repository.directory=./target/test-repo +nifi.flowfile.repository.partitions=1 +nifi.flowfile.repository.checkpoint.interval=2 mins +nifi.queue.swap.threshold=20000 +nifi.swap.storage.directory=./target/test-repo/swap +nifi.swap.in.period=5 sec +nifi.swap.in.threads=1 +nifi.swap.out.period=5 sec +nifi.swap.out.threads=4 + +# Content Repository +nifi.content.claim.max.appendable.size=10 MB +nifi.content.claim.max.flow.files=100 +nifi.content.repository.directory.default=./target/content_repository + +# Provenance Repository Properties +nifi.provenance.repository.storage.directory=./target/provenance_repository +nifi.provenance.repository.max.storage.time=24 hours +nifi.provenance.repository.max.storage.size=1 GB +nifi.provenance.repository.rollover.time=5 mins +nifi.provenance.repository.rollover.size=100 MB + +# Site to Site properties +nifi.remote.input.socket.port=9990 +nifi.remote.input.secure=true + +# web properties # +nifi.web.war.directory=./target/lib +nifi.web.http.host= +nifi.web.http.port=8080 +nifi.web.https.host= +nifi.web.https.port= +nifi.web.jetty.working.directory=./target/work/jetty + +# security properties # +nifi.sensitive.props.key=key +nifi.sensitive.props.algorithm=PBEWITHMD5AND256BITAES-CBC-OPENSSL +nifi.sensitive.props.provider=BC + +nifi.security.keystore= +nifi.security.keystoreType= +nifi.security.keystorePasswd= +nifi.security.keyPasswd= +nifi.security.truststore= +nifi.security.truststoreType= +nifi.security.truststorePasswd= +nifi.security.needClientAuth= +nifi.security.authorizedUsers.file=./target/conf/authorized-users.xml +nifi.security.user.credential.cache.duration=24 hours +nifi.security.user.authority.provider=nifi.authorization.FileAuthorizationProvider +nifi.security.support.new.account.requests= +nifi.security.default.user.roles= + +# cluster common properties (cluster manager and nodes must have same values) # +nifi.cluster.protocol.heartbeat.interval=5 sec +nifi.cluster.protocol.is.secure=false +nifi.cluster.protocol.socket.timeout=30 sec +nifi.cluster.protocol.connection.handshake.timeout=45 sec +# if multicast is used, then nifi.cluster.protocol.multicast.xxx properties must be configured # +nifi.cluster.protocol.use.multicast=false +nifi.cluster.protocol.multicast.address= +nifi.cluster.protocol.multicast.port= +nifi.cluster.protocol.multicast.service.broadcast.delay=500 ms +nifi.cluster.protocol.multicast.service.locator.attempts=3 +nifi.cluster.protocol.multicast.service.locator.attempts.delay=1 sec + +# cluster node properties (only configure for cluster nodes) # +nifi.cluster.is.node=false +nifi.cluster.node.address= +nifi.cluster.node.protocol.port= +nifi.cluster.node.protocol.threads=2 +# if multicast is not used, nifi.cluster.node.unicast.xxx must have same values as nifi.cluster.manager.xxx # +nifi.cluster.node.unicast.manager.address= +nifi.cluster.node.unicast.manager.protocol.port= +nifi.cluster.node.unicast.manager.authority.provider.port= + +# cluster manager properties (only configure for cluster manager) # +nifi.cluster.is.manager=false +nifi.cluster.manager.address= +nifi.cluster.manager.protocol.port= +nifi.cluster.manager.authority.provider.port= +nifi.cluster.manager.authority.provider.threads=10 +nifi.cluster.manager.node.firewall.file= +nifi.cluster.manager.node.event.history.size=10 +nifi.cluster.manager.node.api.connection.timeout=30 sec +nifi.cluster.manager.node.api.read.timeout=30 sec +nifi.cluster.manager.node.api.request.threads=10 +nifi.cluster.manager.flow.retrieval.delay=5 sec +nifi.cluster.manager.protocol.threads=10 +nifi.cluster.manager.safemode.duration=0 sec diff --git a/nifi/nifi-commons/nifi-properties/src/test/resources/NiFiProperties/conf/nifi.properties b/nifi/nifi-commons/nifi-properties/src/test/resources/NiFiProperties/conf/nifi.properties new file mode 100644 index 0000000000..ed44b88ddf --- /dev/null +++ b/nifi/nifi-commons/nifi-properties/src/test/resources/NiFiProperties/conf/nifi.properties @@ -0,0 +1,128 @@ +# 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. + +# Core Properties # +nifi.version=nifi-test 3.0.0 +nifi.flow.configuration.file=./target/flow.xml.gz +nifi.flow.configuration.archive.dir=./target/archive/ +nifi.flowcontroller.autoResumeState=true +nifi.flowcontroller.graceful.shutdown.period=10 sec +nifi.flowservice.writedelay.interval=2 sec +nifi.administrative.yield.duration=30 sec + +nifi.reporting.task.configuration.file=./target/reporting-tasks.xml +nifi.controller.service.configuration.file=./target/controller-services.xml +nifi.templates.directory=./target/templates +nifi.ui.banner.text=UI Banner Text +nifi.ui.autorefresh.interval=30 sec +nifi.nar.library.directory=./target/resources/NiFiProperties/lib/ +nifi.nar.library.directory.alt=./target/resources/NiFiProperties/lib2/ +nifi.nar.working.directory=./target/work/nar/ + +# H2 Settings +nifi.database.directory=./target/database_repository +nifi.h2.url.append=;LOCK_TIMEOUT=25000;WRITE_DELAY=0;AUTO_SERVER=FALSE + +# FlowFile Repository +nifi.flowfile.repository.directory=./target/test-repo +nifi.flowfile.repository.partitions=1 +nifi.flowfile.repository.checkpoint.interval=2 mins +nifi.queue.swap.threshold=20000 +nifi.swap.storage.directory=./target/test-repo/swap +nifi.swap.in.period=5 sec +nifi.swap.in.threads=1 +nifi.swap.out.period=5 sec +nifi.swap.out.threads=4 + +# Content Repository +nifi.content.claim.max.appendable.size=10 MB +nifi.content.claim.max.flow.files=100 +nifi.content.repository.directory.default=./target/content_repository + +# Provenance Repository Properties +nifi.provenance.repository.storage.directory=./target/provenance_repository +nifi.provenance.repository.max.storage.time=24 hours +nifi.provenance.repository.max.storage.size=1 GB +nifi.provenance.repository.rollover.time=5 mins +nifi.provenance.repository.rollover.size=100 MB + +# Site to Site properties +nifi.remote.input.socket.port=9990 +nifi.remote.input.secure=true + +# web properties # +nifi.web.war.directory=./target/lib +nifi.web.http.host= +nifi.web.http.port=8080 +nifi.web.https.host= +nifi.web.https.port= +nifi.web.jetty.working.directory=./target/work/jetty + +# security properties # +nifi.sensitive.props.key=key +nifi.sensitive.props.algorithm=PBEWITHMD5AND256BITAES-CBC-OPENSSL +nifi.sensitive.props.provider=BC + +nifi.security.keystore= +nifi.security.keystoreType= +nifi.security.keystorePasswd= +nifi.security.keyPasswd= +nifi.security.truststore= +nifi.security.truststoreType= +nifi.security.truststorePasswd= +nifi.security.needClientAuth= +nifi.security.authorizedUsers.file=./target/conf/authorized-users.xml +nifi.security.user.credential.cache.duration=24 hours +nifi.security.user.authority.provider=nifi.authorization.FileAuthorizationProvider +nifi.security.support.new.account.requests= +nifi.security.default.user.roles= + +# cluster common properties (cluster manager and nodes must have same values) # +nifi.cluster.protocol.heartbeat.interval=5 sec +nifi.cluster.protocol.is.secure=false +nifi.cluster.protocol.socket.timeout=30 sec +nifi.cluster.protocol.connection.handshake.timeout=45 sec +# if multicast is used, then nifi.cluster.protocol.multicast.xxx properties must be configured # +nifi.cluster.protocol.use.multicast=false +nifi.cluster.protocol.multicast.address= +nifi.cluster.protocol.multicast.port= +nifi.cluster.protocol.multicast.service.broadcast.delay=500 ms +nifi.cluster.protocol.multicast.service.locator.attempts=3 +nifi.cluster.protocol.multicast.service.locator.attempts.delay=1 sec + +# cluster node properties (only configure for cluster nodes) # +nifi.cluster.is.node=false +nifi.cluster.node.address= +nifi.cluster.node.protocol.port= +nifi.cluster.node.protocol.threads=2 +# if multicast is not used, nifi.cluster.node.unicast.xxx must have same values as nifi.cluster.manager.xxx # +nifi.cluster.node.unicast.manager.address= +nifi.cluster.node.unicast.manager.protocol.port= +nifi.cluster.node.unicast.manager.authority.provider.port= + +# cluster manager properties (only configure for cluster manager) # +nifi.cluster.is.manager=false +nifi.cluster.manager.address= +nifi.cluster.manager.protocol.port= +nifi.cluster.manager.authority.provider.port= +nifi.cluster.manager.authority.provider.threads=10 +nifi.cluster.manager.node.firewall.file= +nifi.cluster.manager.node.event.history.size=10 +nifi.cluster.manager.node.api.connection.timeout=30 sec +nifi.cluster.manager.node.api.read.timeout=30 sec +nifi.cluster.manager.node.api.request.threads=10 +nifi.cluster.manager.flow.retrieval.delay=5 sec +nifi.cluster.manager.protocol.threads=10 +nifi.cluster.manager.safemode.duration=0 sec From 275735648aa97a59eee42d0b343411e01f71a7fb Mon Sep 17 00:00:00 2001 From: Brandon DeVries Date: Tue, 7 Apr 2015 16:18:48 -0400 Subject: [PATCH 2/2] updating NarUnpacker to use multiple library directories --- .../java/org/apache/nifi/nar/NarUnpacker.java | 29 ++- .../org/apache/nifi/nar/NarUnpackerTest.java | 177 ++++++++++++++++++ .../NarUnpacker/conf/nifi.properties | 129 +++++++++++++ .../resources/NarUnpacker/lib/dummy-one.nar | Bin 0 -> 1749 bytes .../NarUnpacker/lib/nifi-framework-nar.nar | Bin 0 -> 406 bytes .../resources/NarUnpacker/lib2/dummy-two.nar | Bin 0 -> 1751 bytes 6 files changed, 327 insertions(+), 8 deletions(-) create mode 100644 nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-nar-utils/src/test/java/org/apache/nifi/nar/NarUnpackerTest.java create mode 100644 nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-nar-utils/src/test/resources/NarUnpacker/conf/nifi.properties create mode 100644 nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-nar-utils/src/test/resources/NarUnpacker/lib/dummy-one.nar create mode 100644 nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-nar-utils/src/test/resources/NarUnpacker/lib/nifi-framework-nar.nar create mode 100644 nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-nar-utils/src/test/resources/NarUnpacker/lib2/dummy-two.nar diff --git a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-nar-utils/src/main/java/org/apache/nifi/nar/NarUnpacker.java b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-nar-utils/src/main/java/org/apache/nifi/nar/NarUnpacker.java index 3aaf83985b..f05b1f8220 100644 --- a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-nar-utils/src/main/java/org/apache/nifi/nar/NarUnpacker.java +++ b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-nar-utils/src/main/java/org/apache/nifi/nar/NarUnpacker.java @@ -25,6 +25,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.file.Files; +import java.nio.file.Path; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; @@ -41,7 +42,6 @@ import java.util.jar.Manifest; import org.apache.nifi.util.FileUtils; import org.apache.nifi.util.NiFiProperties; import org.apache.nifi.util.StringUtils; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -61,22 +61,35 @@ public final class NarUnpacker { }; public static ExtensionMapping unpackNars(final NiFiProperties props) { - final File narLibraryDir = props.getNarLibraryDirectory(); + final List narLibraryDirs = props.getNarLibraryDirectories(); final File frameworkWorkingDir = props.getFrameworkWorkingDirectory(); final File extensionsWorkingDir = props.getExtensionsWorkingDirectory(); final File docsWorkingDir = props.getComponentDocumentationWorkingDirectory(); try { - // make sure the nar directories are there and accessible - FileUtils.ensureDirectoryExistAndCanAccess(narLibraryDir); + File unpackedFramework = null; + final Set unpackedExtensions = new HashSet<>(); + final List narFiles = new ArrayList<>(); + + // make sure the nar directories are there and accessible FileUtils.ensureDirectoryExistAndCanAccess(frameworkWorkingDir); FileUtils.ensureDirectoryExistAndCanAccess(extensionsWorkingDir); FileUtils.ensureDirectoryExistAndCanAccess(docsWorkingDir); - File unpackedFramework = null; - final Set unpackedExtensions = new HashSet<>(); - final File[] narFiles = narLibraryDir.listFiles(NAR_FILTER); - if (narFiles != null) { + for (Path narLibraryDir : narLibraryDirs) { + + File narDir = narLibraryDir.toFile(); + FileUtils.ensureDirectoryExistAndCanAccess(narDir); + + File[] dirFiles = narDir.listFiles(NAR_FILTER); + if (dirFiles != null){ + List fileList = Arrays.asList(dirFiles); + narFiles.addAll(fileList); + } + } + + + if (!narFiles.isEmpty()) { for (File narFile : narFiles) { logger.debug("Expanding NAR file: " + narFile.getAbsolutePath()); diff --git a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-nar-utils/src/test/java/org/apache/nifi/nar/NarUnpackerTest.java b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-nar-utils/src/test/java/org/apache/nifi/nar/NarUnpackerTest.java new file mode 100644 index 0000000000..e44f081ec7 --- /dev/null +++ b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-nar-utils/src/test/java/org/apache/nifi/nar/NarUnpackerTest.java @@ -0,0 +1,177 @@ +package org.apache.nifi.nar; + +import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; + +import org.apache.nifi.util.NiFiProperties; +import org.junit.BeforeClass; +import org.junit.Test; + +public class NarUnpackerTest { + + @BeforeClass + public static void copyResources() throws IOException { + + final Path sourcePath = Paths.get("./src/test/resources"); + final Path targetPath = Paths.get("./target"); + + Files.walkFileTree(sourcePath, new SimpleFileVisitor() { + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + + Path relativeSource = sourcePath.relativize(dir); + Path target = targetPath.resolve(relativeSource); + + Files.createDirectories(target); + + return FileVisitResult.CONTINUE; + + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + + Path relativeSource = sourcePath.relativize(file); + Path target = targetPath.resolve(relativeSource); + + Files.copy(file, target, REPLACE_EXISTING); + + return FileVisitResult.CONTINUE; + } + }); + } + + @Test + public void testUnpackNars() { + + NiFiProperties properties = loadSpecifiedProperties("/NarUnpacker/conf/nifi.properties"); + + assertEquals("./target/NarUnpacker/lib/", properties.getProperty("nifi.nar.library.directory")); + assertEquals("./target/NarUnpacker/lib2/", properties.getProperty("nifi.nar.library.directory.alt")); + + final ExtensionMapping extensionMapping = NarUnpacker.unpackNars(properties); + + assertEquals(2,extensionMapping.getAllExtensionNames().size()); + + assertTrue(extensionMapping.getAllExtensionNames().contains("org.apache.nifi.processors.dummy.one")); + assertTrue(extensionMapping.getAllExtensionNames().contains("org.apache.nifi.processors.dummy.two")); + + final File extensionsWorkingDir = properties.getExtensionsWorkingDirectory(); + File[] extensionFiles = extensionsWorkingDir.listFiles(); + + assertEquals(2,extensionFiles.length); + assertEquals("dummy-one.nar-unpacked", extensionFiles[0].getName()); + assertEquals("dummy-two.nar-unpacked", extensionFiles[1].getName()); + } + + @Test + public void testUnpackNarsFromEmptyDir() throws IOException { + + NiFiProperties properties = loadSpecifiedProperties("/NarUnpacker/conf/nifi.properties"); + + final File emptyDir = new File ("./target/empty/dir"); + emptyDir.delete(); + emptyDir.deleteOnExit(); + assertTrue(emptyDir.mkdirs()); + + properties.setProperty("nifi.nar.library.directory.alt", emptyDir.toString()); + + final ExtensionMapping extensionMapping = NarUnpacker.unpackNars(properties); + + assertEquals(1,extensionMapping.getAllExtensionNames().size()); + assertTrue(extensionMapping.getAllExtensionNames().contains("org.apache.nifi.processors.dummy.one")); + + final File extensionsWorkingDir = properties.getExtensionsWorkingDirectory(); + File[] extensionFiles = extensionsWorkingDir.listFiles(); + + assertEquals(1,extensionFiles.length); + assertEquals("dummy-one.nar-unpacked", extensionFiles[0].getName()); + } + + @Test + public void testUnpackNarsFromNonExistantDir() { + + final File nonExistantDir = new File ("./target/this/dir/should/not/exist/"); + nonExistantDir.delete(); + nonExistantDir.deleteOnExit(); + + NiFiProperties properties = loadSpecifiedProperties("/NarUnpacker/conf/nifi.properties"); + properties.setProperty("nifi.nar.library.directory.alt", nonExistantDir.toString()); + + final ExtensionMapping extensionMapping = NarUnpacker.unpackNars(properties); + + assertTrue(extensionMapping.getAllExtensionNames().contains("org.apache.nifi.processors.dummy.one")); + + assertEquals(1,extensionMapping.getAllExtensionNames().size()); + + final File extensionsWorkingDir = properties.getExtensionsWorkingDirectory(); + File[] extensionFiles = extensionsWorkingDir.listFiles(); + + assertEquals(1,extensionFiles.length); + assertEquals("dummy-one.nar-unpacked", extensionFiles[0].getName()); + } + + @Test + public void testUnpackNarsFromNonDir() throws IOException { + + final File nonDir = new File ("./target/file.txt"); + nonDir.createNewFile(); + nonDir.deleteOnExit(); + + NiFiProperties properties = loadSpecifiedProperties("/NarUnpacker/conf/nifi.properties"); + properties.setProperty("nifi.nar.library.directory.alt", nonDir.toString()); + + final ExtensionMapping extensionMapping = NarUnpacker.unpackNars(properties); + + assertNull(extensionMapping); + } + + + private NiFiProperties loadSpecifiedProperties(String propertiesFile) { + String file = NarUnpackerTest.class.getResource(propertiesFile).getFile(); + + System.setProperty(NiFiProperties.PROPERTIES_FILE_PATH, file); + + NiFiProperties properties = NiFiProperties.getInstance(); + + // clear out existing properties + for (String prop : properties.stringPropertyNames()) { + properties.remove(prop); + } + + InputStream inStream = null; + try { + inStream = new BufferedInputStream(new FileInputStream(file)); + properties.load(inStream); + } catch (final Exception ex) { + throw new RuntimeException("Cannot load properties file due to " + ex.getLocalizedMessage(), ex); + } finally { + if (null != inStream) { + try { + inStream.close(); + } catch (final Exception ex) { + /** + * do nothing * + */ + } + } + } + + return properties; + } +} \ No newline at end of file diff --git a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-nar-utils/src/test/resources/NarUnpacker/conf/nifi.properties b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-nar-utils/src/test/resources/NarUnpacker/conf/nifi.properties new file mode 100644 index 0000000000..92e4a92670 --- /dev/null +++ b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-nar-utils/src/test/resources/NarUnpacker/conf/nifi.properties @@ -0,0 +1,129 @@ +# 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. + +# Core Properties # +nifi.version=nifi-test 3.0.0 +nifi.flow.configuration.file=./target/flow.xml.gz +nifi.flow.configuration.archive.dir=./target/archive/ +nifi.flowcontroller.autoResumeState=true +nifi.flowcontroller.graceful.shutdown.period=10 sec +nifi.flowservice.writedelay.interval=2 sec +nifi.administrative.yield.duration=30 sec + +nifi.reporting.task.configuration.file=./target/reporting-tasks.xml +nifi.controller.service.configuration.file=./target/controller-services.xml +nifi.templates.directory=./target/templates +nifi.ui.banner.text=UI Banner Text +nifi.ui.autorefresh.interval=30 sec +nifi.nar.library.directory=./target/NarUnpacker/lib/ +nifi.nar.library.directory.alt=./target/NarUnpacker/lib2/ + +nifi.nar.working.directory=./target/work/nar/ + +# H2 Settings +nifi.database.directory=./target/database_repository +nifi.h2.url.append=;LOCK_TIMEOUT=25000;WRITE_DELAY=0;AUTO_SERVER=FALSE + +# FlowFile Repository +nifi.flowfile.repository.directory=./target/test-repo +nifi.flowfile.repository.partitions=1 +nifi.flowfile.repository.checkpoint.interval=2 mins +nifi.queue.swap.threshold=20000 +nifi.swap.storage.directory=./target/test-repo/swap +nifi.swap.in.period=5 sec +nifi.swap.in.threads=1 +nifi.swap.out.period=5 sec +nifi.swap.out.threads=4 + +# Content Repository +nifi.content.claim.max.appendable.size=10 MB +nifi.content.claim.max.flow.files=100 +nifi.content.repository.directory.default=./target/content_repository + +# Provenance Repository Properties +nifi.provenance.repository.storage.directory=./target/provenance_repository +nifi.provenance.repository.max.storage.time=24 hours +nifi.provenance.repository.max.storage.size=1 GB +nifi.provenance.repository.rollover.time=5 mins +nifi.provenance.repository.rollover.size=100 MB + +# Site to Site properties +nifi.remote.input.socket.port=9990 +nifi.remote.input.secure=true + +# web properties # +nifi.web.war.directory=./target/lib +nifi.web.http.host= +nifi.web.http.port=8080 +nifi.web.https.host= +nifi.web.https.port= +nifi.web.jetty.working.directory=./target/work/jetty + +# security properties # +nifi.sensitive.props.key=key +nifi.sensitive.props.algorithm=PBEWITHMD5AND256BITAES-CBC-OPENSSL +nifi.sensitive.props.provider=BC + +nifi.security.keystore= +nifi.security.keystoreType= +nifi.security.keystorePasswd= +nifi.security.keyPasswd= +nifi.security.truststore= +nifi.security.truststoreType= +nifi.security.truststorePasswd= +nifi.security.needClientAuth= +nifi.security.authorizedUsers.file=./target/conf/authorized-users.xml +nifi.security.user.credential.cache.duration=24 hours +nifi.security.user.authority.provider=nifi.authorization.FileAuthorizationProvider +nifi.security.support.new.account.requests= +nifi.security.default.user.roles= + +# cluster common properties (cluster manager and nodes must have same values) # +nifi.cluster.protocol.heartbeat.interval=5 sec +nifi.cluster.protocol.is.secure=false +nifi.cluster.protocol.socket.timeout=30 sec +nifi.cluster.protocol.connection.handshake.timeout=45 sec +# if multicast is used, then nifi.cluster.protocol.multicast.xxx properties must be configured # +nifi.cluster.protocol.use.multicast=false +nifi.cluster.protocol.multicast.address= +nifi.cluster.protocol.multicast.port= +nifi.cluster.protocol.multicast.service.broadcast.delay=500 ms +nifi.cluster.protocol.multicast.service.locator.attempts=3 +nifi.cluster.protocol.multicast.service.locator.attempts.delay=1 sec + +# cluster node properties (only configure for cluster nodes) # +nifi.cluster.is.node=false +nifi.cluster.node.address= +nifi.cluster.node.protocol.port= +nifi.cluster.node.protocol.threads=2 +# if multicast is not used, nifi.cluster.node.unicast.xxx must have same values as nifi.cluster.manager.xxx # +nifi.cluster.node.unicast.manager.address= +nifi.cluster.node.unicast.manager.protocol.port= +nifi.cluster.node.unicast.manager.authority.provider.port= + +# cluster manager properties (only configure for cluster manager) # +nifi.cluster.is.manager=false +nifi.cluster.manager.address= +nifi.cluster.manager.protocol.port= +nifi.cluster.manager.authority.provider.port= +nifi.cluster.manager.authority.provider.threads=10 +nifi.cluster.manager.node.firewall.file= +nifi.cluster.manager.node.event.history.size=10 +nifi.cluster.manager.node.api.connection.timeout=30 sec +nifi.cluster.manager.node.api.read.timeout=30 sec +nifi.cluster.manager.node.api.request.threads=10 +nifi.cluster.manager.flow.retrieval.delay=5 sec +nifi.cluster.manager.protocol.threads=10 +nifi.cluster.manager.safemode.duration=0 sec diff --git a/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-nar-utils/src/test/resources/NarUnpacker/lib/dummy-one.nar b/nifi/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-nar-utils/src/test/resources/NarUnpacker/lib/dummy-one.nar new file mode 100644 index 0000000000000000000000000000000000000000..598b27f7d0b45a20c3bec9b99c434f37403aeaa8 GIT binary patch literal 1749 zcmWIWW@h1H0D;RTm2O}Ll;8x?zOEsTx}JV+`T=ldm(Y~SAuCHN%}dEiP0>wBElABv zNzF^nOf3fM6#=T^U|`K@ax2b!obr~Lfq|KofkBU;-n`7TOx={y+}uiCL%poTqPfAf z0jCQk{;!n}_H0wU`R|o9P?UrGE6r&(zoQSM&<1-LB^M6rd{1r z^2%zm+g8=w9)s39m8Vx~s_JapEW2**W&3&0=l$My=lN%w^7+4QM3gL!iEe1On=8&` z(bB!vLviAdpNX%XnBN$vJjn6B8*i{gxu+#wTuf?{p^#FmSmz0s0!63G$L~*XmlEpS z(I38f+rz1BS3`Y+X6%sCo$IVK!#+nzb*iuqlXCutxl6SdZBw17zz*Hd&R0J zcNE_KweoKFv$^X(f9pT>BIMK8zwa2?GR;gK*Lo@JWIe*?5#(R;;c=Sv%!iGC1!U$_ zuUN9Mbf)k@YZvEt&mOK?62B%htl-v8-C0*nrvFV1-j{r6t9IzFX;+kQ%w93$=gwn| zKVs7kcHdhf%$|8XqK@Ydlkc3VRw1cIr(UZqJ-qqUhS+rb1;&d%9lrZ+XT;{~3%<^i z~!{GL1&)?|*lVw{UUAP(JW;6G5bi@HQ zoe4DxTO`>}TIa?xIWk22@vqUc7QD5w+Wn87)sdt(DGs}DuH2UIrzO4U&(4eHFCs4` zxExj}y7rxksiZRBNHTcu%ZbK@;>Huq6zf{+^|cpQG*|79I&Ayc-T2O>+E}TIi`$!K z9sTsVAXfW&XjNQgM^?y!Th@0&UVl9P#C*z@>A{C~a!M55T0B#CZTbwO#v4s9)*onO z^)Y$=?^AwjS4+_0z@WehpZ1DZJ=S^MWyt(mTHGr|QM)f&w%=3TY$AufkmUGA#JDUAT zEZL-D_4}UtSLW%)Wx?8*pI;9C{pBN-w1>Sb_OYFg@{{D7 zO^Yt=duJK9RCi)z?$#CDI~n!wXutINTlVhL44YjaSA4jcK2cCZ{J6=o^&3xV8B{F& z_~6llURy!oXIW=Wjoa3?uD{oPpCz!vKwZV+)~)Pa74zVC z-=;JBt$N=j@D&f8Kd+e*bis<%a(ydsp}7@k=~lyt7TCYNjUJj5Sxj zE`98&*w=d7FgZsuN4kV_%l#Ef-CTo1 z^nBf1`@Fdh8Hl((Z+xRDU9oU!!otmA>Rh7keyUGwGdJ|A&QB`K=kZ9N{@e0SVB}{N z`3HXIW^aG%qN{yw-Km3}VVgX{)Gd+yUNS|CsWVs#*fYeNbGF0d>E10#b3 lgR5@9ruW$w7wBElABv zNzF^nOf3fM6#=T^U`Wqta=T%BBj-Ic0|PTF1A`txy?L2wnYt;Zxw)0PMtWI^MRS8| z15Ou8{9h{{?AfMx^W7_#mk}9pYWin2SDQ);C97UdS#5TE(<~3;X?t>S1sPwOxompn z%PXa7y_-~XbrQLDex5e@rEf$|x^;~8@_L)|w%==ao_}8X&hC4qM@7LD>jxapR{Amp zT;|n+hZb!tR=(E8ZkbSU;Oe|z`x6S%B^Y;`DcR&Wn{)}pDX2X<_|qVlzdm0tDM06$ zUEI+dU7?a!!?$V*PSRRZBW73sZoUi*G zd)KSk(|0_mIlSeFUYy6$Ki$2l%**H8UK8tDcstzarL|Si_L#N#Npc78xEwhvI_sBk zc@&Gj-Lg3r9+t;A7POwZtajr>^VGD-o64N5Ojfg8uKdPqWUQut>G8hVy49wa9?WG_ z-XMlr?9fMqRJnM>zmhG*2R`d&U0o+h&X?{p40SVqz2#AvrAQjEcdsvzBv>W z%-`bOxM;HMH@$-j3_5?*=biL(%e}wVwoqO4=z_Nk7M5+C`7NJsa<9_goq^Y-B|Zg2 z{AH*)_Ps{t)u(wOO1rWiD~In9xO-k|`VO5B%F2ri_NL8uJ8diMV0iCReXLB)#r<>U z9Od{>6u?$>dBKXUT>D(+S}&O&ELCim&?z{Pn#F= zCkLl)y!qWeGwQBXzMk#Q4u0KLpJgx1@jI!s4CQ`qbhH2b*rjGNnj z-JAC6)KkxxUvvGQ-d-VavC;IOfY_^d+k)p^I;v~qw7uwUzjQ#stCzA*IONs?>{IU?mj3+vY6r<2XEv0qv@^_kGGF0;rf3r)SQUcSa&I#c9Egg(peTg4uYrw?yn zWj$&eySb)o_M**ESpwoMj`}v*D@qQVy}Ps{X4T6TFK(u9Uh7=F+?ErRxH|rGosirkCU*Cz>-LPyXCe;VwAr6=`1R{I;X8u6xoiFl zPWu_&KC#x;W(Ld8>DjV>rXKLH`~Lgv_l@CvPwZpg=PkEpVya-2?5y3Wnd#QC=*C~w zyDwE9x;x9x2>iygm46qDRh&H|sMvuN9R{uSZtp@5=yd>#j1@o(5`~vezK(vLZmz*0 zdcJP1ecoJ$3`AU?H@;Dnu2{Glr^LV6B|803EF!HmC z`~$ypv$wx>(bYb;?$p7~uuUFe>K4iBU6CPi5(jqZaAuur%Xr4%e5!Ympx%#PiBXZZ zx0uu2-L(X!dF|J-ohWhDEzD%oM9B-*wljP0)IBu2tX}nz-{SEb?f`E_CJ|=bB`eVR zKp?>I))7R*eS)kFTS*I24AG_zq~Y3dl)H>f_TYfRR`RNVO=4hB0Mf|>%|b7Sxxo4u z7#JBO7(!K(60&nIFbZ}if@!gBFLXeBkY8Z-V@4mc{qtCm{DKsj$hI)E-xB