From d992730fb79fb689db4353de3ce823ea92c2324a Mon Sep 17 00:00:00 2001 From: Joe Percivall Date: Tue, 18 Aug 2015 19:17:59 -0400 Subject: [PATCH 1/4] NIFI-864 first contrib to nifi supports working with images and addressing comments Committing modified files Addressing code review comments Signed-off-by: Matt Gilman --- nifi-assembly/LICENSE | 30 +++ nifi-assembly/NOTICE | 5 + nifi-assembly/pom.xml | 6 + .../nifi-image-bundle/nifi-image-nar/pom.xml | 37 +++ .../src/main/resources/META-INF/LICENSE | 239 ++++++++++++++++++ .../src/main/resources/META-INF/NOTICE | 16 ++ .../nifi-image-processors/pom.xml | 62 +++++ .../image/ExtractImageMetadata.java | 163 ++++++++++++ .../nifi/processors/image/ResizeImage.java | 194 ++++++++++++++ .../org.apache.nifi.processor.Processor | 16 ++ .../image/ExtractImageMetadataTest.java | 159 ++++++++++++ .../processors/image/TestResizeImage.java | 57 +++++ .../src/test/resources/16color-10x10.bmp | Bin 0 -> 198 bytes .../src/test/resources/mspaint-8x10.png | Bin 0 -> 124 bytes .../src/test/resources/notImage.txt | 1 + .../photoshop-8x12-32colors-alpha.gif | Bin 0 -> 1243 bytes .../src/test/resources/simple.jpg | Bin 0 -> 25248 bytes nifi-nar-bundles/nifi-image-bundle/pom.xml | 34 +++ nifi-nar-bundles/pom.xml | 1 + 19 files changed, 1020 insertions(+) create mode 100644 nifi-nar-bundles/nifi-image-bundle/nifi-image-nar/pom.xml create mode 100644 nifi-nar-bundles/nifi-image-bundle/nifi-image-nar/src/main/resources/META-INF/LICENSE create mode 100644 nifi-nar-bundles/nifi-image-bundle/nifi-image-nar/src/main/resources/META-INF/NOTICE create mode 100644 nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/pom.xml create mode 100644 nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/main/java/org/apache/nifi/processors/image/ExtractImageMetadata.java create mode 100644 nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/main/java/org/apache/nifi/processors/image/ResizeImage.java create mode 100644 nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor create mode 100644 nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/test/java/org/apache/nifi/processors/image/ExtractImageMetadataTest.java create mode 100644 nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/test/java/org/apache/nifi/processors/image/TestResizeImage.java create mode 100644 nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/test/resources/16color-10x10.bmp create mode 100644 nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/test/resources/mspaint-8x10.png create mode 100644 nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/test/resources/notImage.txt create mode 100644 nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/test/resources/photoshop-8x12-32colors-alpha.gif create mode 100644 nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/test/resources/simple.jpg create mode 100644 nifi-nar-bundles/nifi-image-bundle/pom.xml diff --git a/nifi-assembly/LICENSE b/nifi-assembly/LICENSE index 5e6b490f41..5abc79a51c 100644 --- a/nifi-assembly/LICENSE +++ b/nifi-assembly/LICENSE @@ -913,3 +913,33 @@ The binary distribution of this product bundles 'leveldbjni-all-1.8.jar' which i The binary distribution of this product bundles 'Woodstox StAX 2 API' which is "licensed under standard BSD license" + +This product bundles 'Adobe XMPCore' which is available under "The BSD license". More +information can be found here: http://www.adobe.com/devnet/xmp/library/eula-xmp-library-java.html + + Copyright (c) 2009, Adobe Systems Incorporated All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of Adobe Systems Incorporated, nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANT ABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/nifi-assembly/NOTICE b/nifi-assembly/NOTICE index 4efa96f2bb..3362740c1a 100644 --- a/nifi-assembly/NOTICE +++ b/nifi-assembly/NOTICE @@ -704,6 +704,11 @@ The following binary components are provided under the Apache Software License v Google Guice - Extensions - Servlet Copyright 2006-2011 Google, Inc. + (ASLv2) Metadata-Extractor + The following NOTICE information applies: + Metadata-Extractor + Copyright 2002-2015 Drew Noakes + ************************ Common Development and Distribution License 1.1 ************************ diff --git a/nifi-assembly/pom.xml b/nifi-assembly/pom.xml index a712b86638..bd4a5aac6c 100644 --- a/nifi-assembly/pom.xml +++ b/nifi-assembly/pom.xml @@ -217,6 +217,12 @@ language governing permissions and limitations under the License. --> nifi-ambari-nar nar + + org.apache.nifi + nifi-image-nar + 0.3.0-SNAPSHOT + nar + diff --git a/nifi-nar-bundles/nifi-image-bundle/nifi-image-nar/pom.xml b/nifi-nar-bundles/nifi-image-bundle/nifi-image-nar/pom.xml new file mode 100644 index 0000000000..760e67f1e2 --- /dev/null +++ b/nifi-nar-bundles/nifi-image-bundle/nifi-image-nar/pom.xml @@ -0,0 +1,37 @@ + + + + 4.0.0 + + + org.apache.nifi + nifi-image-bundle + 0.3.0-SNAPSHOT + + + nifi-image-nar + 0.3.0-SNAPSHOT + nar + + + + org.apache.nifi + nifi-image-processors + 0.3.0-SNAPSHOT + + + + diff --git a/nifi-nar-bundles/nifi-image-bundle/nifi-image-nar/src/main/resources/META-INF/LICENSE b/nifi-nar-bundles/nifi-image-bundle/nifi-image-nar/src/main/resources/META-INF/LICENSE new file mode 100644 index 0000000000..2c3e42f5e1 --- /dev/null +++ b/nifi-nar-bundles/nifi-image-bundle/nifi-image-nar/src/main/resources/META-INF/LICENSE @@ -0,0 +1,239 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed 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. + +APACHE NIFI SUBCOMPONENTS: + +The Apache NiFi project contains subcomponents with separate copyright +notices and license terms. Your use of the source code for the these +subcomponents is subject to the terms and conditions of the following +licenses. + +This product bundles 'Adobe XMPCore' which is available under "The BSD license". More +information can be found here: http://www.adobe.com/devnet/xmp/library/eula-xmp-library-java.html + + Copyright (c) 2009, Adobe Systems Incorporated All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, + are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of Adobe Systems Incorporated, nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANT ABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, + OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF + THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-image-bundle/nifi-image-nar/src/main/resources/META-INF/NOTICE b/nifi-nar-bundles/nifi-image-bundle/nifi-image-nar/src/main/resources/META-INF/NOTICE new file mode 100644 index 0000000000..44658fbcf9 --- /dev/null +++ b/nifi-nar-bundles/nifi-image-bundle/nifi-image-nar/src/main/resources/META-INF/NOTICE @@ -0,0 +1,16 @@ +nifi-image-nar +Copyright 2015 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +****************** +Apache Software License v2 +****************** + +The following binary components are provided under the Apache Software License v2 + + (ASLv2) Metadata-Extractor + The following NOTICE information applies: + Metadata-Extractor + Copyright 2002-2015 Drew Noakes diff --git a/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/pom.xml b/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/pom.xml new file mode 100644 index 0000000000..4b9bc37bbb --- /dev/null +++ b/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/pom.xml @@ -0,0 +1,62 @@ + + + + 4.0.0 + + + org.apache.nifi + nifi-image-bundle + 0.3.0-SNAPSHOT + + + nifi-image-processors + jar + + + + org.apache.nifi + nifi-api + + + org.apache.nifi + nifi-processor-utils + + + org.apache.nifi + nifi-mock + test + + + com.drewnoakes + metadata-extractor + 2.7.2 + + + + + + + org.apache.rat + apache-rat-plugin + + + src/test/resources/notImage.txt + + + + + + diff --git a/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/main/java/org/apache/nifi/processors/image/ExtractImageMetadata.java b/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/main/java/org/apache/nifi/processors/image/ExtractImageMetadata.java new file mode 100644 index 0000000000..67a55f5ba8 --- /dev/null +++ b/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/main/java/org/apache/nifi/processors/image/ExtractImageMetadata.java @@ -0,0 +1,163 @@ +/* + * 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.processors.image; + +import com.drew.imaging.ImageMetadataReader; +import com.drew.imaging.ImageProcessingException; +import com.drew.metadata.Directory; +import com.drew.metadata.Metadata; +import com.drew.metadata.Tag; +import org.apache.nifi.annotation.behavior.SupportsBatching; +import org.apache.nifi.annotation.behavior.WritesAttribute; +import org.apache.nifi.annotation.behavior.WritesAttributes; +import org.apache.nifi.annotation.documentation.CapabilityDescription; +import org.apache.nifi.annotation.documentation.Tags; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.flowfile.FlowFile; +import org.apache.nifi.logging.ProcessorLog; +import org.apache.nifi.processor.AbstractProcessor; +import org.apache.nifi.processor.Relationship; +import org.apache.nifi.processor.ProcessorInitializationContext; +import org.apache.nifi.processor.ProcessContext; +import org.apache.nifi.processor.ProcessSession; +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.processor.io.InputStreamCallback; +import org.apache.nifi.processor.util.StandardValidators; +import org.apache.nifi.util.ObjectHolder; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.HashSet; +import java.util.Map; +import java.util.HashMap; + +@Tags({"Exif", "Exchangeable", "image", "file", "format", "JPG", "GIF", "PNG", "BMP", "metadata","IPTC", "XMP"}) +@CapabilityDescription("Extract the image metadata from flowfiles containing images. This processor relies on this " + + "metadata extractor library https://github.com/drewnoakes/metadata-extractor. It extracts a long list of " + + "metadata types including but not limited to EXIF, IPTC, XMP and Photoshop fields. For the full list visit " + + "the library's website." + + "NOTE: The library being used loads the images into memory so extremely large images may cause problems.") +@WritesAttributes({@WritesAttribute(attribute = ".", description = "The extracted image metadata " + + "will be inserted with the attribute name \".\". ")}) +@SupportsBatching +public class ExtractImageMetadata extends AbstractProcessor { + + public static final PropertyDescriptor MaxAttributes = new PropertyDescriptor.Builder() + .name("Max number of attributes") + .description("Specify the max number of attributes to add to the flowfile. There is no guarantee in what order" + + " the tags will be processed. By default it will process all of them.") + .required(true) + .defaultValue(Integer.toString(Integer.MAX_VALUE)) + .addValidator(StandardValidators.NON_NEGATIVE_INTEGER_VALIDATOR) + .build(); + + public static final Relationship SUCCESS = new Relationship.Builder() + .name("success") + .description("Any FlowFile that successfully has image metadata extracted will be routed to success") + .build(); + + public static final Relationship FAILURE = new Relationship.Builder() + .name("failure") + .description("Any FlowFile that fails to have image metadata extracted will be routed to failure") + .build(); + + private Set relationships; + private List properties; + + @Override + protected void init(final ProcessorInitializationContext context) { + + final List properties = new ArrayList<>(); + properties.add(MaxAttributes); + this.properties = Collections.unmodifiableList(properties); + + final Set relationships = new HashSet<>(); + relationships.add(SUCCESS); + relationships.add(FAILURE); + this.relationships = Collections.unmodifiableSet(relationships); + } + + @Override + public Set getRelationships() { + return this.relationships; + } + + @Override + protected List getSupportedPropertyDescriptors() { + return this.properties; + } + + @Override + public void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException { + final ProcessorLog logger = this.getLogger(); + FlowFile flowfile = session.get(); + final ObjectHolder value = new ObjectHolder(null); + final int max = Integer.parseInt(context.getProperty(MaxAttributes).getValue()); + if (flowfile == null) { + return; + } + + try { + session.read(flowfile, new InputStreamCallback() { + @Override + public void process(InputStream in) throws IOException { + try { + Metadata imageMetadata = ImageMetadataReader.readMetadata(in); + value.set(imageMetadata); + } catch (ImageProcessingException ex) { + throw new ProcessException(ex); + } + } + }); + + Metadata metadata = value.get(); + Map results = getTags(max,metadata); + + // Write the results to an attribute + if (!results.isEmpty()) { + flowfile = session.putAllAttributes(flowfile, results); + } + + session.transfer(flowfile, SUCCESS); + } catch (ProcessException e) { + logger.error("Failed to extract image metadata from {} due to {}", new Object[]{flowfile, e}); + session.transfer(flowfile, FAILURE); + } + } + + private Map getTags(int max, Metadata metadata) { + Map results = new HashMap<>(); + int i =0; + + for (Directory directory : metadata.getDirectories()) { + for (Tag tag : directory.getTags()) { + results.put(directory.getName() + "." + tag.getTagName(), tag.getDescription()); + + i++; + if(i>=max) { + return results; + } + } + } + + return results; + } +} diff --git a/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/main/java/org/apache/nifi/processors/image/ResizeImage.java b/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/main/java/org/apache/nifi/processors/image/ResizeImage.java new file mode 100644 index 0000000000..c085b5fd5c --- /dev/null +++ b/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/main/java/org/apache/nifi/processors/image/ResizeImage.java @@ -0,0 +1,194 @@ +/* + * 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.processors.image; + +import org.apache.nifi.annotation.behavior.EventDriven; +import org.apache.nifi.annotation.behavior.SupportsBatching; +import org.apache.nifi.annotation.documentation.CapabilityDescription; +import org.apache.nifi.annotation.documentation.Tags; +import org.apache.nifi.components.AllowableValue; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.flowfile.FlowFile; +import org.apache.nifi.processor.AbstractProcessor; +import org.apache.nifi.processor.ProcessContext; +import org.apache.nifi.processor.ProcessSession; +import org.apache.nifi.processor.Relationship; +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.processor.io.StreamCallback; +import org.apache.nifi.processor.util.StandardValidators; +import org.apache.nifi.util.StopWatch; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.stream.ImageInputStream; +import java.awt.Image; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Set; +import java.util.HashSet; +import java.util.List; +import java.util.Iterator; +import java.util.concurrent.TimeUnit; + +@EventDriven +@SupportsBatching +@Tags({ "resize", "image", "jpg", "jpeg", "png", "bmp", "wbmp", "gif" }) +@CapabilityDescription("Resizes an image to user-specified dimensions. This Processor uses the image codecs registered with the " + + "environment that NiFi is running in. By default, this includes JPEG, PNG, BMP, WBMP, and GIF images.") +public class ResizeImage extends AbstractProcessor { + static final AllowableValue RESIZE_DEFAULT = new AllowableValue("Default", "Default", "Use the default algorithm"); + static final AllowableValue RESIZE_FAST = new AllowableValue("Scale Fast", "Scale Fast", "Emphasize speed of the scaling over smoothness"); + static final AllowableValue RESIZE_SMOOTH = new AllowableValue("Scale Smooth", "Scale Smooth", "Emphasize smoothness of the scaling over speed"); + static final AllowableValue RESIZE_REPLICATE = new AllowableValue("Replicate Scale Filter", "Replicate Scale Filter", "Use the Replicate Scale Filter algorithm"); + static final AllowableValue RESIZE_AREA_AVERAGING = new AllowableValue("Area Averaging", "Area Averaging", "Use the Area Averaging scaling algorithm"); + + static final PropertyDescriptor IMAGE_WIDTH = new PropertyDescriptor.Builder() + .name("Image Width (in pixels)") + .description("The desired number of pixels for the image's width") + .required(true) + .expressionLanguageSupported(true) + .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR) + .build(); + static final PropertyDescriptor IMAGE_HEIGHT = new PropertyDescriptor.Builder() + .name("Image Height (in pixels)") + .description("The desired number of pixels for the image's height") + .required(true) + .expressionLanguageSupported(true) + .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR) + .build(); + static final PropertyDescriptor SCALING_ALGORITHM = new PropertyDescriptor.Builder() + .name("Scaling Algorithm") + .description("Specifies which algorithm should be used to resize the image") + .required(true) + .allowableValues(RESIZE_DEFAULT, RESIZE_FAST, RESIZE_SMOOTH, RESIZE_REPLICATE, RESIZE_AREA_AVERAGING) + .defaultValue(RESIZE_DEFAULT.getValue()) + .build(); + + static final Relationship REL_SUCCESS = new Relationship.Builder() + .name("success") + .description("A FlowFile is routed to this relationship if it is successfully resized") + .build(); + static final Relationship REL_FAILURE = new Relationship.Builder() + .name("failure") + .description("A FlowFile is routed to this relationship if it is not in the specified format") + .build(); + + @Override + protected List getSupportedPropertyDescriptors() { + final List properties = new ArrayList<>(); + properties.add(IMAGE_WIDTH); + properties.add(IMAGE_HEIGHT); + properties.add(SCALING_ALGORITHM); + return properties; + } + + @Override + public Set getRelationships() { + final Set relationships = new HashSet<>(); + relationships.add(REL_SUCCESS); + relationships.add(REL_FAILURE); + return relationships; + } + + @Override + public void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException { + FlowFile flowFile = session.get(); + if (flowFile == null) { + return; + } + + final int width, height; + try { + width = context.getProperty(IMAGE_WIDTH).evaluateAttributeExpressions(flowFile).asInteger(); + height = context.getProperty(IMAGE_HEIGHT).evaluateAttributeExpressions(flowFile).asInteger(); + } catch (final NumberFormatException nfe) { + getLogger().error("Failed to resize {} due to {}", new Object[] { flowFile, nfe }); + session.transfer(flowFile, REL_FAILURE); + return; + } + + final String algorithm = context.getProperty(SCALING_ALGORITHM).getValue(); + final int hints; + if (algorithm.equalsIgnoreCase(RESIZE_DEFAULT.getValue())) { + hints = Image.SCALE_DEFAULT; + } else if (algorithm.equalsIgnoreCase(RESIZE_FAST.getValue())) { + hints = Image.SCALE_FAST; + } else if (algorithm.equalsIgnoreCase(RESIZE_SMOOTH.getValue())) { + hints = Image.SCALE_SMOOTH; + } else if (algorithm.equalsIgnoreCase(RESIZE_REPLICATE.getValue())) { + hints = Image.SCALE_REPLICATE; + } else if (algorithm.equalsIgnoreCase(RESIZE_AREA_AVERAGING.getValue())) { + hints = Image.SCALE_AREA_AVERAGING; + } else { + throw new AssertionError("Invalid Scaling Algorithm: " + algorithm); + } + + final StopWatch stopWatch = new StopWatch(true); + try { + flowFile = session.write(flowFile, new StreamCallback() { + @Override + public void process(final InputStream rawIn, final OutputStream out) throws IOException { + try (final BufferedInputStream in = new BufferedInputStream(rawIn)) { + final ImageInputStream iis = ImageIO.createImageInputStream(in); + if (iis == null) { + throw new ProcessException("FlowFile is not in a valid format"); + } + + final Iterator readers = ImageIO.getImageReaders(iis); + if (!readers.hasNext()) { + throw new ProcessException("FlowFile is not in a valid format"); + } + + final ImageReader reader = readers.next(); + final String formatName = reader.getFormatName(); + reader.setInput(iis, true); + final BufferedImage image = reader.read(0); + + final Image scaledImage = image.getScaledInstance(width, height, hints); + final BufferedImage scaledBufferedImg; + if (scaledImage instanceof BufferedImage) { + scaledBufferedImg = (BufferedImage) scaledImage; + } else { + scaledBufferedImg = new BufferedImage(scaledImage.getWidth(null), scaledImage.getHeight(null), image.getType()); + final Graphics2D graphics = scaledBufferedImg.createGraphics(); + try { + graphics.drawImage(scaledImage, 0, 0, null); + } finally { + graphics.dispose(); + } + } + + ImageIO.write(scaledBufferedImg, formatName, out); + } + } + }); + + session.getProvenanceReporter().modifyContent(flowFile, stopWatch.getElapsed(TimeUnit.MILLISECONDS)); + session.transfer(flowFile, REL_SUCCESS); + } catch (final ProcessException pe) { + getLogger().error("Failed to resize {} due to {}", new Object[] { flowFile, pe }); + session.transfer(flowFile, REL_FAILURE); + } + } + +} diff --git a/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor b/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor new file mode 100644 index 0000000000..224cb54172 --- /dev/null +++ b/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor @@ -0,0 +1,16 @@ +# 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. +org.apache.nifi.processors.image.ExtractImageMetadata +org.apache.nifi.processors.image.ResizeImage \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/test/java/org/apache/nifi/processors/image/ExtractImageMetadataTest.java b/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/test/java/org/apache/nifi/processors/image/ExtractImageMetadataTest.java new file mode 100644 index 0000000000..9e68301e6f --- /dev/null +++ b/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/test/java/org/apache/nifi/processors/image/ExtractImageMetadataTest.java @@ -0,0 +1,159 @@ +/* + * 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.processors.image; + +import org.apache.nifi.processor.Relationship; +import org.apache.nifi.util.MockFlowFile; +import org.apache.nifi.util.TestRunner; +import org.apache.nifi.util.TestRunners; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + + +public class ExtractImageMetadataTest { + private static String BMP_HEADER = "BMP Header."; + private static String JPEG_HEADER = "JPEG."; + private static String GIF_HEADER = "GIF Header."; + private static String PNG_HEADER = "PNG."; + + private TestRunner testRunner; + + @Before + public void init() { + testRunner = TestRunners.newTestRunner(ExtractImageMetadata.class); + } + + @Test + public void testFailedExtraction() throws IOException { + MockFlowFile flowFile = verifyTestRunnerFlow("src/test/resources/notImage.txt", ExtractImageMetadata.FAILURE,"1000"); + } + + @Test + public void testExtractJPG() throws IOException { + MockFlowFile flowFile = verifyTestRunnerFlow("src/test/resources/simple.jpg", ExtractImageMetadata.SUCCESS,"1000"); + Map attributes = flowFile.getAttributes(); + + assertEquals("800 pixels", attributes.get(JPEG_HEADER + "Image Width")); + assertEquals("600 pixels", attributes.get(JPEG_HEADER + "Image Height")); + assertEquals("8 bits", attributes.get(JPEG_HEADER + "Data Precision")); + assertEquals("Baseline", attributes.get(JPEG_HEADER + "Compression Type")); + assertEquals("3", attributes.get(JPEG_HEADER + "Number of Components")); + assertEquals("Y component: Quantization table 0, Sampling factors 2 horiz/2 vert", + attributes.get(JPEG_HEADER + "Component 1")); + assertEquals("Cb component: Quantization table 1, Sampling factors 1 horiz/1 vert", + attributes.get(JPEG_HEADER + "Component 2")); + assertEquals("Cr component: Quantization table 1, Sampling factors 1 horiz/1 vert", + attributes.get(JPEG_HEADER + "Component 3")); + } + + @Test + public void testExtractGIF() throws IOException { + MockFlowFile flowFile = verifyTestRunnerFlow( + "src/test/resources/photoshop-8x12-32colors-alpha.gif", ExtractImageMetadata.SUCCESS,"1000"); + Map attributes = flowFile.getAttributes(); + + assertEquals("8", attributes.get(GIF_HEADER + "Image Width")); + assertEquals("12", attributes.get(GIF_HEADER + "Image Height")); + assertEquals("true", attributes.get(GIF_HEADER + "Has Global Color Table")); + assertEquals("32", attributes.get(GIF_HEADER + "Color Table Size")); + assertEquals("8", attributes.get(GIF_HEADER + "Transparent Color Index")); + assertEquals("89a", attributes.get(GIF_HEADER + "GIF Format Version")); + assertEquals("5", attributes.get(GIF_HEADER + "Bits per Pixel")); + assertEquals("false", attributes.get(GIF_HEADER + "Is Color Table Sorted")); + } + + @Test + public void testExtractPNG() throws IOException { + MockFlowFile flowFile = verifyTestRunnerFlow("src/test/resources/mspaint-8x10.png", ExtractImageMetadata.SUCCESS, "1000"); + Map attributes = flowFile.getAttributes(); + + assertEquals("8", attributes.get(PNG_HEADER + "Image Width")); + assertEquals("12", attributes.get(PNG_HEADER + "Image Height")); + assertEquals("0.45455", attributes.get(PNG_HEADER + "Image Gamma")); + assertEquals("Deflate", attributes.get(PNG_HEADER + "Compression Type")); + assertEquals("No Interlace", attributes.get(PNG_HEADER + "Interlace Method")); + assertEquals("Perceptual", attributes.get(PNG_HEADER + "sRGB Rendering Intent")); + assertEquals("Adaptive", attributes.get(PNG_HEADER + "Filter Method")); + assertEquals("8", attributes.get(PNG_HEADER + "Bits Per Sample")); + assertEquals("True Color", attributes.get(PNG_HEADER + "Color Type")); + } + @Test + public void testExtractBMP() throws IOException { + MockFlowFile flowFile = verifyTestRunnerFlow("src/test/resources/16color-10x10.bmp", ExtractImageMetadata.SUCCESS, "1000"); + Map attributes = flowFile.getAttributes(); + + assertEquals("10", attributes.get(BMP_HEADER+"Image Width")); + assertEquals("10", attributes.get(BMP_HEADER+"Image Height")); + assertEquals("4", attributes.get(BMP_HEADER+"Bits Per Pixel")); + assertEquals("None", attributes.get(BMP_HEADER+"Compression")); + assertEquals("0", attributes.get(BMP_HEADER+"X Pixels per Meter")); + assertEquals("0", attributes.get(BMP_HEADER+"Y Pixels per Meter")); + assertEquals("0", attributes.get(BMP_HEADER+"Palette Colour Count")); + assertEquals("0", attributes.get(BMP_HEADER+"Important Colour Count")); + assertEquals("1", attributes.get(BMP_HEADER+"Planes")); + assertEquals("40", attributes.get(BMP_HEADER+"Header Size")); + } + @Test + public void testExtractLimitedAttributesBMP() throws IOException { + MockFlowFile flowFile = verifyTestRunnerFlow("src/test/resources/16color-10x10.bmp", ExtractImageMetadata.SUCCESS, "5"); + Map attributes = flowFile.getAttributes(); + + assertEquals("10", attributes.get(BMP_HEADER+"Image Width")); + assertEquals("10", attributes.get(BMP_HEADER+"Image Height")); + assertEquals("4", attributes.get(BMP_HEADER+"Bits Per Pixel")); + assertEquals("1", attributes.get(BMP_HEADER+"Planes")); + assertEquals("40", attributes.get(BMP_HEADER+"Header Size")); + + + assertNull(attributes.get(BMP_HEADER + "Compression")); + assertNull(attributes.get(BMP_HEADER + "X Pixels per Meter")); + assertNull(attributes.get(BMP_HEADER + "Y Pixels per Meter")); + assertNull(attributes.get(BMP_HEADER + "Palette Colour Count")); + assertNull(attributes.get(BMP_HEADER + "Important Colour Count")); + } + + public MockFlowFile verifyTestRunnerFlow(String pathStr,Relationship rel, String max) throws IOException { + Path path = Paths.get(pathStr); + testRunner.enqueue(path); + testRunner.setProperty(ExtractImageMetadata.MaxAttributes, max); + + testRunner.run(); + testRunner.assertAllFlowFilesTransferred(rel, 1); + + + MockFlowFile flowFile = testRunner.getFlowFilesForRelationship(rel).get(0); + testRunner.assertQueueEmpty(); + + testRunner.enqueue(flowFile); + testRunner.clearTransferState(); + testRunner.run(); + testRunner.assertAllFlowFilesTransferred(rel, 1); + + flowFile = testRunner.getFlowFilesForRelationship(rel).get(0); + flowFile.assertContentEquals(new File(pathStr)); + return flowFile; + } +} diff --git a/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/test/java/org/apache/nifi/processors/image/TestResizeImage.java b/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/test/java/org/apache/nifi/processors/image/TestResizeImage.java new file mode 100644 index 0000000000..53bf17ea08 --- /dev/null +++ b/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/test/java/org/apache/nifi/processors/image/TestResizeImage.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.processors.image; + +import org.apache.nifi.util.MockFlowFile; +import org.apache.nifi.util.TestRunner; +import org.apache.nifi.util.TestRunners; +import org.junit.Test; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; + +import static org.junit.Assert.assertEquals; + +public class TestResizeImage { + + @Test + public void testResize() throws IOException { + final TestRunner runner = TestRunners.newTestRunner(new ResizeImage()); + runner.setProperty(ResizeImage.IMAGE_HEIGHT, "64"); + runner.setProperty(ResizeImage.IMAGE_WIDTH, "64"); + runner.setProperty(ResizeImage.SCALING_ALGORITHM, ResizeImage.RESIZE_SMOOTH); + + runner.enqueue(Paths.get("src/test/resources/simple.jpg")); + runner.run(); + + runner.assertAllFlowFilesTransferred(ResizeImage.REL_SUCCESS, 1); + final MockFlowFile mff = runner.getFlowFilesForRelationship(ResizeImage.REL_SUCCESS).get(0); + final byte[] data = mff.toByteArray(); + + final BufferedImage img = ImageIO.read(new ByteArrayInputStream(data)); + assertEquals(64, img.getWidth()); + assertEquals(64, img.getHeight()); + final File out = new File("target/simple.jpg"); + ImageIO.write(img, "JPG", out); + } + +} diff --git a/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/test/resources/16color-10x10.bmp b/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/test/resources/16color-10x10.bmp new file mode 100644 index 0000000000000000000000000000000000000000..26206f458fc159daa86b52ca5dc555fd6dafe219 GIT binary patch literal 198 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 + + + 4.0.0 + + + org.apache.nifi + nifi-nar-bundles + 0.3.0-SNAPSHOT + + + nifi-image-bundle + 0.3.0-SNAPSHOT + pom + + + nifi-image-processors + nifi-image-nar + + + diff --git a/nifi-nar-bundles/pom.xml b/nifi-nar-bundles/pom.xml index fdcececcaa..1be2cfab2b 100644 --- a/nifi-nar-bundles/pom.xml +++ b/nifi-nar-bundles/pom.xml @@ -43,6 +43,7 @@ nifi-mongodb-bundle nifi-flume-bundle nifi-ambari-bundle + nifi-image-bundle From dedff148caa8536fe1c6c06a4b4aba4d4d6f1736 Mon Sep 17 00:00:00 2001 From: Joe Percivall Date: Thu, 20 Aug 2015 11:18:46 -0400 Subject: [PATCH 2/4] Making metadata extractor max tags property optional Signed-off-by: Matt Gilman --- .../image/ExtractImageMetadata.java | 25 +++++++++++++++---- .../image/ExtractImageMetadataTest.java | 14 ++++++----- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/main/java/org/apache/nifi/processors/image/ExtractImageMetadata.java b/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/main/java/org/apache/nifi/processors/image/ExtractImageMetadata.java index 67a55f5ba8..10eb892518 100644 --- a/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/main/java/org/apache/nifi/processors/image/ExtractImageMetadata.java +++ b/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/main/java/org/apache/nifi/processors/image/ExtractImageMetadata.java @@ -27,6 +27,7 @@ import org.apache.nifi.annotation.behavior.WritesAttributes; import org.apache.nifi.annotation.documentation.CapabilityDescription; import org.apache.nifi.annotation.documentation.Tags; import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.components.PropertyValue; import org.apache.nifi.flowfile.FlowFile; import org.apache.nifi.logging.ProcessorLog; import org.apache.nifi.processor.AbstractProcessor; @@ -64,8 +65,7 @@ public class ExtractImageMetadata extends AbstractProcessor { .name("Max number of attributes") .description("Specify the max number of attributes to add to the flowfile. There is no guarantee in what order" + " the tags will be processed. By default it will process all of them.") - .required(true) - .defaultValue(Integer.toString(Integer.MAX_VALUE)) + .required(false) .addValidator(StandardValidators.NON_NEGATIVE_INTEGER_VALIDATOR) .build(); @@ -109,12 +109,15 @@ public class ExtractImageMetadata extends AbstractProcessor { public void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException { final ProcessorLog logger = this.getLogger(); FlowFile flowfile = session.get(); - final ObjectHolder value = new ObjectHolder(null); - final int max = Integer.parseInt(context.getProperty(MaxAttributes).getValue()); + if (flowfile == null) { return; } + final ObjectHolder value = new ObjectHolder<>(null); + String propertyValue = context.getProperty(MaxAttributes).getValue(); + final int max = propertyValue!=null ? Integer.parseInt(propertyValue) : -1; + try { session.read(flowfile, new InputStreamCallback() { @Override @@ -129,7 +132,7 @@ public class ExtractImageMetadata extends AbstractProcessor { }); Metadata metadata = value.get(); - Map results = getTags(max,metadata); + Map results = max == -1 ? getTags(metadata) : getTags(max, metadata); // Write the results to an attribute if (!results.isEmpty()) { @@ -143,6 +146,18 @@ public class ExtractImageMetadata extends AbstractProcessor { } } + private Map getTags(Metadata metadata) { + Map results = new HashMap<>(); + + for (Directory directory : metadata.getDirectories()) { + for (Tag tag : directory.getTags()) { + results.put(directory.getName() + "." + tag.getTagName(), tag.getDescription()); + } + } + + return results; + } + private Map getTags(int max, Metadata metadata) { Map results = new HashMap<>(); int i =0; diff --git a/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/test/java/org/apache/nifi/processors/image/ExtractImageMetadataTest.java b/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/test/java/org/apache/nifi/processors/image/ExtractImageMetadataTest.java index 9e68301e6f..a4dde01c8b 100644 --- a/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/test/java/org/apache/nifi/processors/image/ExtractImageMetadataTest.java +++ b/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/test/java/org/apache/nifi/processors/image/ExtractImageMetadataTest.java @@ -48,12 +48,12 @@ public class ExtractImageMetadataTest { @Test public void testFailedExtraction() throws IOException { - MockFlowFile flowFile = verifyTestRunnerFlow("src/test/resources/notImage.txt", ExtractImageMetadata.FAILURE,"1000"); + MockFlowFile flowFile = verifyTestRunnerFlow("src/test/resources/notImage.txt", ExtractImageMetadata.FAILURE,null); } @Test public void testExtractJPG() throws IOException { - MockFlowFile flowFile = verifyTestRunnerFlow("src/test/resources/simple.jpg", ExtractImageMetadata.SUCCESS,"1000"); + MockFlowFile flowFile = verifyTestRunnerFlow("src/test/resources/simple.jpg", ExtractImageMetadata.SUCCESS,null); Map attributes = flowFile.getAttributes(); assertEquals("800 pixels", attributes.get(JPEG_HEADER + "Image Width")); @@ -72,7 +72,7 @@ public class ExtractImageMetadataTest { @Test public void testExtractGIF() throws IOException { MockFlowFile flowFile = verifyTestRunnerFlow( - "src/test/resources/photoshop-8x12-32colors-alpha.gif", ExtractImageMetadata.SUCCESS,"1000"); + "src/test/resources/photoshop-8x12-32colors-alpha.gif", ExtractImageMetadata.SUCCESS,null); Map attributes = flowFile.getAttributes(); assertEquals("8", attributes.get(GIF_HEADER + "Image Width")); @@ -87,7 +87,7 @@ public class ExtractImageMetadataTest { @Test public void testExtractPNG() throws IOException { - MockFlowFile flowFile = verifyTestRunnerFlow("src/test/resources/mspaint-8x10.png", ExtractImageMetadata.SUCCESS, "1000"); + MockFlowFile flowFile = verifyTestRunnerFlow("src/test/resources/mspaint-8x10.png", ExtractImageMetadata.SUCCESS, null); Map attributes = flowFile.getAttributes(); assertEquals("8", attributes.get(PNG_HEADER + "Image Width")); @@ -102,7 +102,7 @@ public class ExtractImageMetadataTest { } @Test public void testExtractBMP() throws IOException { - MockFlowFile flowFile = verifyTestRunnerFlow("src/test/resources/16color-10x10.bmp", ExtractImageMetadata.SUCCESS, "1000"); + MockFlowFile flowFile = verifyTestRunnerFlow("src/test/resources/16color-10x10.bmp", ExtractImageMetadata.SUCCESS, null); Map attributes = flowFile.getAttributes(); assertEquals("10", attributes.get(BMP_HEADER+"Image Width")); @@ -138,7 +138,9 @@ public class ExtractImageMetadataTest { public MockFlowFile verifyTestRunnerFlow(String pathStr,Relationship rel, String max) throws IOException { Path path = Paths.get(pathStr); testRunner.enqueue(path); - testRunner.setProperty(ExtractImageMetadata.MaxAttributes, max); + if(max != null) { + testRunner.setProperty(ExtractImageMetadata.MaxAttributes, max); + } testRunner.run(); testRunner.assertAllFlowFilesTransferred(rel, 1); From b17be66a104b79fd9883c0ce2fade713877c56a8 Mon Sep 17 00:00:00 2001 From: Joe Percivall Date: Thu, 20 Aug 2015 13:24:30 -0400 Subject: [PATCH 3/4] Fixing contrib check and addressing comments Signed-off-by: Matt Gilman --- .../image/ExtractImageMetadata.java | 37 ++++++------------- .../image/ExtractImageMetadataTest.java | 2 +- 2 files changed, 13 insertions(+), 26 deletions(-) diff --git a/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/main/java/org/apache/nifi/processors/image/ExtractImageMetadata.java b/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/main/java/org/apache/nifi/processors/image/ExtractImageMetadata.java index 10eb892518..7fe6195e4d 100644 --- a/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/main/java/org/apache/nifi/processors/image/ExtractImageMetadata.java +++ b/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/main/java/org/apache/nifi/processors/image/ExtractImageMetadata.java @@ -27,7 +27,6 @@ import org.apache.nifi.annotation.behavior.WritesAttributes; import org.apache.nifi.annotation.documentation.CapabilityDescription; import org.apache.nifi.annotation.documentation.Tags; import org.apache.nifi.components.PropertyDescriptor; -import org.apache.nifi.components.PropertyValue; import org.apache.nifi.flowfile.FlowFile; import org.apache.nifi.logging.ProcessorLog; import org.apache.nifi.processor.AbstractProcessor; @@ -61,8 +60,8 @@ import java.util.HashMap; @SupportsBatching public class ExtractImageMetadata extends AbstractProcessor { - public static final PropertyDescriptor MaxAttributes = new PropertyDescriptor.Builder() - .name("Max number of attributes") + public static final PropertyDescriptor MAX_NUMBER_OF_ATTRIBUTES = new PropertyDescriptor.Builder() + .name("Max Number of Attributes") .description("Specify the max number of attributes to add to the flowfile. There is no guarantee in what order" + " the tags will be processed. By default it will process all of them.") .required(false) @@ -86,7 +85,7 @@ public class ExtractImageMetadata extends AbstractProcessor { protected void init(final ProcessorInitializationContext context) { final List properties = new ArrayList<>(); - properties.add(MaxAttributes); + properties.add(MAX_NUMBER_OF_ATTRIBUTES); this.properties = Collections.unmodifiableList(properties); final Set relationships = new HashSet<>(); @@ -107,16 +106,14 @@ public class ExtractImageMetadata extends AbstractProcessor { @Override public void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException { - final ProcessorLog logger = this.getLogger(); FlowFile flowfile = session.get(); - if (flowfile == null) { return; } + final ProcessorLog logger = this.getLogger(); final ObjectHolder value = new ObjectHolder<>(null); - String propertyValue = context.getProperty(MaxAttributes).getValue(); - final int max = propertyValue!=null ? Integer.parseInt(propertyValue) : -1; + final Integer max = context.getProperty(MAX_NUMBER_OF_ATTRIBUTES).asInteger(); try { session.read(flowfile, new InputStreamCallback() { @@ -132,7 +129,7 @@ public class ExtractImageMetadata extends AbstractProcessor { }); Metadata metadata = value.get(); - Map results = max == -1 ? getTags(metadata) : getTags(max, metadata); + Map results = getTags(max, metadata); // Write the results to an attribute if (!results.isEmpty()) { @@ -146,19 +143,7 @@ public class ExtractImageMetadata extends AbstractProcessor { } } - private Map getTags(Metadata metadata) { - Map results = new HashMap<>(); - - for (Directory directory : metadata.getDirectories()) { - for (Tag tag : directory.getTags()) { - results.put(directory.getName() + "." + tag.getTagName(), tag.getDescription()); - } - } - - return results; - } - - private Map getTags(int max, Metadata metadata) { + private Map getTags(Integer max, Metadata metadata) { Map results = new HashMap<>(); int i =0; @@ -166,9 +151,11 @@ public class ExtractImageMetadata extends AbstractProcessor { for (Tag tag : directory.getTags()) { results.put(directory.getName() + "." + tag.getTagName(), tag.getDescription()); - i++; - if(i>=max) { - return results; + if(max!=null) { + i++; + if (i >= max) { + return results; + } } } } diff --git a/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/test/java/org/apache/nifi/processors/image/ExtractImageMetadataTest.java b/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/test/java/org/apache/nifi/processors/image/ExtractImageMetadataTest.java index a4dde01c8b..cf31e15e54 100644 --- a/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/test/java/org/apache/nifi/processors/image/ExtractImageMetadataTest.java +++ b/nifi-nar-bundles/nifi-image-bundle/nifi-image-processors/src/test/java/org/apache/nifi/processors/image/ExtractImageMetadataTest.java @@ -139,7 +139,7 @@ public class ExtractImageMetadataTest { Path path = Paths.get(pathStr); testRunner.enqueue(path); if(max != null) { - testRunner.setProperty(ExtractImageMetadata.MaxAttributes, max); + testRunner.setProperty(ExtractImageMetadata.MAX_NUMBER_OF_ATTRIBUTES, max); } testRunner.run(); From d421e3c2424b224522e9e5a83f014ccda2496739 Mon Sep 17 00:00:00 2001 From: Bryan Bende Date: Fri, 21 Aug 2015 11:06:20 -0400 Subject: [PATCH 4/4] NIFI-846 MonitorActivity not setting start of evaluation period correctly --- .../processors/standard/MonitorActivity.java | 48 +++++++++++-------- .../standard/TestMonitorActivity.java | 39 ++++++++++++++- 2 files changed, 66 insertions(+), 21 deletions(-) diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/MonitorActivity.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/MonitorActivity.java index 99a29e5bb6..29006239da 100644 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/MonitorActivity.java +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/MonitorActivity.java @@ -16,6 +16,26 @@ */ package org.apache.nifi.processors.standard; +import org.apache.nifi.annotation.behavior.SideEffectFree; +import org.apache.nifi.annotation.behavior.TriggerSerially; +import org.apache.nifi.annotation.behavior.TriggerWhenEmpty; +import org.apache.nifi.annotation.behavior.WritesAttribute; +import org.apache.nifi.annotation.behavior.WritesAttributes; +import org.apache.nifi.annotation.documentation.CapabilityDescription; +import org.apache.nifi.annotation.documentation.Tags; +import org.apache.nifi.annotation.lifecycle.OnScheduled; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.flowfile.FlowFile; +import org.apache.nifi.flowfile.attributes.CoreAttributes; +import org.apache.nifi.logging.ProcessorLog; +import org.apache.nifi.processor.AbstractProcessor; +import org.apache.nifi.processor.ProcessContext; +import org.apache.nifi.processor.ProcessSession; +import org.apache.nifi.processor.ProcessorInitializationContext; +import org.apache.nifi.processor.Relationship; +import org.apache.nifi.processor.io.OutputStreamCallback; +import org.apache.nifi.processor.util.StandardValidators; + import java.io.IOException; import java.io.OutputStream; import java.nio.charset.Charset; @@ -30,25 +50,6 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; -import org.apache.nifi.annotation.behavior.SideEffectFree; -import org.apache.nifi.annotation.behavior.TriggerSerially; -import org.apache.nifi.annotation.behavior.TriggerWhenEmpty; -import org.apache.nifi.annotation.documentation.CapabilityDescription; -import org.apache.nifi.annotation.documentation.Tags; -import org.apache.nifi.annotation.behavior.WritesAttribute; -import org.apache.nifi.annotation.behavior.WritesAttributes; -import org.apache.nifi.components.PropertyDescriptor; -import org.apache.nifi.flowfile.FlowFile; -import org.apache.nifi.flowfile.attributes.CoreAttributes; -import org.apache.nifi.logging.ProcessorLog; -import org.apache.nifi.processor.AbstractProcessor; -import org.apache.nifi.processor.ProcessContext; -import org.apache.nifi.processor.ProcessSession; -import org.apache.nifi.processor.ProcessorInitializationContext; -import org.apache.nifi.processor.Relationship; -import org.apache.nifi.processor.io.OutputStreamCallback; -import org.apache.nifi.processor.util.StandardValidators; - @SideEffectFree @TriggerSerially @TriggerWhenEmpty @@ -149,6 +150,15 @@ public class MonitorActivity extends AbstractProcessor { return properties; } + @OnScheduled + public void resetLastSuccessfulTransfer() { + setLastSuccessfulTransfer(System.currentTimeMillis()); + } + + protected final void setLastSuccessfulTransfer(final long timestamp) { + latestSuccessTransfer.set(timestamp); + } + @Override public void onTrigger(final ProcessContext context, final ProcessSession session) { final long thresholdMillis = context.getProperty(THRESHOLD).asTimePeriod(TimeUnit.MILLISECONDS); diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestMonitorActivity.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestMonitorActivity.java index 2e8744154e..f02e6da3fa 100644 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestMonitorActivity.java +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestMonitorActivity.java @@ -31,7 +31,7 @@ public class TestMonitorActivity { @Test public void testFirstMessage() throws InterruptedException, IOException { - final TestRunner runner = TestRunners.newTestRunner(new MonitorActivity()); + final TestRunner runner = TestRunners.newTestRunner(new TestableProcessor(1000L)); runner.setProperty(MonitorActivity.CONTINUALLY_SEND_MESSAGES, "false"); runner.setProperty(MonitorActivity.THRESHOLD, "100 millis"); @@ -101,7 +101,7 @@ public class TestMonitorActivity { @Test public void testFirstMessageWithInherit() throws InterruptedException, IOException { - final TestRunner runner = TestRunners.newTestRunner(new MonitorActivity()); + final TestRunner runner = TestRunners.newTestRunner(new TestableProcessor(1000L)); runner.setProperty(MonitorActivity.CONTINUALLY_SEND_MESSAGES, "false"); runner.setProperty(MonitorActivity.THRESHOLD, "100 millis"); runner.setProperty(MonitorActivity.COPY_ATTRIBUTES, "true"); @@ -188,4 +188,39 @@ public class TestMonitorActivity { String.format("lineage start dates match when they shouldn't original=%1$s restored=%2$s", originalFlowFile.getLineageStartDate(), restoredFlowFile.getLineageStartDate()), restoredFlowFile.getLineageStartDate() != originalFlowFile.getLineageStartDate()); } + + @Test + public void testFirstRunNoMessages() throws InterruptedException, IOException { + // don't use the TestableProcessor, we want the real timestamp from @OnScheduled + final TestRunner runner = TestRunners.newTestRunner(new MonitorActivity()); + runner.setProperty(MonitorActivity.CONTINUALLY_SEND_MESSAGES, "false"); + runner.setProperty(MonitorActivity.THRESHOLD, "100 millis"); + + Thread.sleep(1000L); + + // shouldn't generate inactivity b/c run() will reset the lastSuccessfulTransfer + runner.run(); + runner.assertTransferCount(MonitorActivity.REL_SUCCESS, 0); + runner.assertTransferCount(MonitorActivity.REL_INACTIVE, 0); + runner.assertTransferCount(MonitorActivity.REL_ACTIVITY_RESTORED, 0); + runner.clearTransferState(); + } + + /** + * Since each call to run() will call @OnScheduled methods which will set the lastSuccessfulTransfer to the + * current time, we need a way to create an artificial time difference between calls to run. + */ + private class TestableProcessor extends MonitorActivity { + + private final long timestampDifference; + + public TestableProcessor(final long timestampDifference) { + this.timestampDifference = timestampDifference; + } + + @Override + public void resetLastSuccessfulTransfer() { + setLastSuccessfulTransfer(System.currentTimeMillis() - timestampDifference); + } + } }