This is an automated email from the ASF dual-hosted git repository. dklco pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-app-cms.git
commit b6fc5024ee941b83248cbce75404d2f632e540db Author: Dan Klco <[email protected]> AuthorDate: Wed May 15 09:11:03 2019 -0400 Updated to fix issues with and cleanup the the File Metadata Extractor and adding a servlet for generating thumbnails --- .../java/org/apache/sling/cms/CMSConstants.java | 7 +- .../apache/sling/cms/FileMetadataExtractor.java | 59 +++++++ builder/pom.xml | 1 - builder/src/main/provisioning/cms.txt | 30 +++- builder/src/main/provisioning/sling.txt | 1 - core/pom.xml | 17 ++ .../core/internal/FileMetadataExtractorImpl.java | 178 +++++++++++++++++++++ .../internal/jobs/FileMetadataExtractorJob.java | 9 +- .../internal/listeners/FileMetadataExtractor.java | 155 ------------------ .../listeners/FileMetadataExtractorListener.java | 90 +++++++++++ .../sling/cms/core/internal/models/FileImpl.java | 4 +- .../core/internal/servlets/TransformServlet.java | 82 ++++++++++ .../internal/FileMetadataExtractorImplTest.java | 62 +++++++ core/src/test/resources/apache.png | Bin 0 -> 12022 bytes metadata-extractor/pom.xml | 102 ++++++++++++ pom.xml | 13 ++ ...eUserMapperImpl.amended-sling-cms-metadata.json | 6 + 17 files changed, 644 insertions(+), 172 deletions(-) diff --git a/api/src/main/java/org/apache/sling/cms/CMSConstants.java b/api/src/main/java/org/apache/sling/cms/CMSConstants.java index d74e6e7..58416f7 100644 --- a/api/src/main/java/org/apache/sling/cms/CMSConstants.java +++ b/api/src/main/java/org/apache/sling/cms/CMSConstants.java @@ -60,6 +60,11 @@ public class CMSConstants { public static final String NAMESPACE = "sling"; /** + * The subpath for the metadata under sling:File resources + */ + public static final String NN_METADATA = "metadata"; + + /** * Component node type. */ public static final String NT_COMPONENT = NAMESPACE + ":Component"; @@ -129,7 +134,7 @@ public class CMSConstants { */ public static final String[] PUBLISHABLE_TYPES = new String[] { CMSConstants.NT_FILE, CMSConstants.NT_PAGE, JcrResourceConstants.NT_SLING_FOLDER, JcrResourceConstants.NT_SLING_ORDERED_FOLDER }; - + /** * The name of the admin user */ diff --git a/api/src/main/java/org/apache/sling/cms/FileMetadataExtractor.java b/api/src/main/java/org/apache/sling/cms/FileMetadataExtractor.java new file mode 100644 index 0000000..54a709e --- /dev/null +++ b/api/src/main/java/org/apache/sling/cms/FileMetadataExtractor.java @@ -0,0 +1,59 @@ +/* + * 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.sling.cms; + +import java.io.IOException; +import java.util.Map; + +import org.osgi.annotation.versioning.ProviderType; + +/** + * Service for extracting metadata from a file + */ +@ProviderType +public interface FileMetadataExtractor { + + /** + * Extract the metadata from the specified file and return the resulting + * metadata + * + * @param file + * @return the metadata from the file + * @throws IOException + */ + Map<String,Object> extractMetadata(File file) throws IOException; + + /** + * Extract the metadata from the specified file and persist the results under + * the jcr:content/metadata node of the file resource + * + * @param file the file to extract the metadata from + * @throws IOException + */ + void updateMetadata(File file) throws IOException; + + /** + * Extract the metadata from the specified file and persist the results under + * the jcr:content/metadata node of the file resource + * + * @param file the file to extract the metadata from + * @param save if true, persist the results, if not leave the changes + * unpersisted + * @throws IOException + */ + void updateMetadata(File file, boolean persist) throws IOException; +} diff --git a/builder/pom.xml b/builder/pom.xml index 7541f92..d469f15 100644 --- a/builder/pom.xml +++ b/builder/pom.xml @@ -153,7 +153,6 @@ <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> - <version>4.5.2</version> <scope>test</scope> </dependency> <dependency> diff --git a/builder/src/main/provisioning/cms.txt b/builder/src/main/provisioning/cms.txt index 4b8f6d1..b0b7960 100644 --- a/builder/src/main/provisioning/cms.txt +++ b/builder/src/main/provisioning/cms.txt @@ -17,18 +17,26 @@ # under the License. [feature name=cms] -[artifacts startLevel=20] +[artifacts startLevel=15] org.apache.cocoon/cocoon-serializers-charsets/1.0.0 org.jsoup/jsoup/1.11.3 - org.jsoup/jsoup/1.11.3 com.vladsch.flexmark/flexmark-osgi/0.34.22 org.apache.sling/org.apache.sling.fileoptim/0.9.2 org.apache.sling/org.apache.sling.resourcemerger/1.3.6 + org.apache.servicemix.bundles/org.apache.servicemix.bundles.xmlbeans/3.0.2_1 + org.apache.commons/commons-math3/3.6.1 + org.apache.commons/commons-compress/1.18 + +[artifacts startLevel=20] + org.apache.servicemix.bundles/org.apache.servicemix.bundles.poi/4.0.1_2 org.apache.sling/org.apache.sling.cms.api/${cms.version} org.apache.sling/org.apache.sling.cms.core/${cms.version} org.apache.sling/org.apache.sling.cms.ui/${cms.version} org.apache.sling/org.apache.sling.cms.reference/${cms.version} org.apache.tika/tika-bundle/1.19.1 + +[artifacts startLevel=25] + org.apache.sling/org.apache.sling.cms.metadata-extractor/${cms.version} [:repoinit] @@ -80,16 +88,22 @@ end # Service users - create service user sling-rewriter - set ACL for sling-rewriter - allow jcr:read on / - end create service user sling-cms-error set ACL for sling-cms-error allow jcr:read on / end - create service user sling-ugc - set ACL for sling-ugc + create service user sling-cms-metadata + set ACL for sling-cms-metadata + allow jcr:write,jcr:nodeTypeManagement,jcr:versionManagement on /content + allow jcr:write,jcr:nodeTypeManagement,jcr:versionManagement on /static + allow jcr:read on / + end + create service user sling-rewriter + set ACL for sling-rewriter + allow jcr:read on / + end + create service user sling-cms-ugc + set ACL for sling-cms-ugc allow jcr:write on /etc/usergenerated end create service user sling-cms-versionmgr diff --git a/builder/src/main/provisioning/sling.txt b/builder/src/main/provisioning/sling.txt index dad9dd2..087c498 100644 --- a/builder/src/main/provisioning/sling.txt +++ b/builder/src/main/provisioning/sling.txt @@ -95,7 +95,6 @@ org.apache.pdfbox/fontbox/2.0.12 org.apache.pdfbox/jempbox/1.8.16 org.apache.tika/tika-core/1.19.1 - org.apache.tika/tika-parsers/1.19.1 [artifacts startLevel=15] org.apache.sling/org.apache.sling.jcr.jcr-wrapper/2.0.0 diff --git a/core/pom.xml b/core/pom.xml index 58358e7..04a5232 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -34,6 +34,7 @@ org.apache.sling.cms.core.models, org.apache.sling.cms.core.internal.models </Sling-Model-Packages> + <Embed-Dependency>*;scope=compile|runtime</Embed-Dependency> </instructions> </configuration> </plugin> @@ -197,6 +198,7 @@ <groupId>org.apache.sling</groupId> <artifactId>org.apache.sling.cms.api</artifactId> <version>${project.version}</version> + <scope>provided</scope> </dependency> <dependency> <groupId>org.apache.jackrabbit</groupId> @@ -238,5 +240,20 @@ <groupId>org.apache.sling</groupId> <artifactId>org.apache.sling.fileoptim</artifactId> </dependency> + <dependency> + <groupId>net.coobird</groupId> + <artifactId>thumbnailator</artifactId> + <version>[0.4, 0.5)</version> + <scope>compile</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <artifactId>pdfbox</artifactId> + <groupId>org.apache.pdfbox</groupId> + </dependency> </dependencies> </project> \ No newline at end of file diff --git a/core/src/main/java/org/apache/sling/cms/core/internal/FileMetadataExtractorImpl.java b/core/src/main/java/org/apache/sling/cms/core/internal/FileMetadataExtractorImpl.java new file mode 100644 index 0000000..fd0623f --- /dev/null +++ b/core/src/main/java/org/apache/sling/cms/core/internal/FileMetadataExtractorImpl.java @@ -0,0 +1,178 @@ +/* + * 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.sling.cms.core.internal; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.apache.jackrabbit.JcrConstants; +import org.apache.jackrabbit.util.Text; +import org.apache.sling.api.resource.ModifiableValueMap; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.cms.CMSConstants; +import org.apache.sling.cms.File; +import org.apache.sling.cms.FileMetadataExtractor; +import org.apache.tika.exception.TikaException; +import org.apache.tika.metadata.Metadata; +import org.apache.tika.metadata.Property; +import org.apache.tika.parser.AutoDetectParser; +import org.apache.tika.parser.ParseContext; +import org.apache.tika.parser.Parser; +import org.apache.tika.sax.BodyContentHandler; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.wiring.FrameworkWiring; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xml.sax.SAXException; + +@Component(service = FileMetadataExtractor.class) +public class FileMetadataExtractorImpl implements FileMetadataExtractor { + + private static final String METADATA_EXTRACTOR_BUNDLE_NAME = "org.apache.sling.cms.metadata-extractor"; + + private static final Logger log = LoggerFactory.getLogger(FileMetadataExtractorImpl.class); + + private BundleContext bcx; + + @Activate + public void activate(ComponentContext context) { + bcx = context.getBundleContext(); + } + + @Override + public Map<String, Object> extractMetadata(File file) throws IOException { + try { + return extractMetadata(file.getResource()); + } catch (NoClassDefFoundError ncdfe) { + + log.info("Caught exception: {}, Attempting to reload metadata extractor bundle", String.valueOf(ncdfe)); + Bundle metadataExtractorBundle = Arrays.stream(bcx.getBundles()) + .filter(b -> METADATA_EXTRACTOR_BUNDLE_NAME.equals(b.getSymbolicName())).findFirst().orElse(null); + + if (metadataExtractorBundle != null) { + try { + log.debug("Reloading metadata bundle: {}", metadataExtractorBundle); + Bundle systemBundle = bcx.getBundle(0); + metadataExtractorBundle.stop(); + metadataExtractorBundle.start(); + + FrameworkWiring frameworkWiring = systemBundle.adapt(FrameworkWiring.class); + frameworkWiring.refreshBundles(Collections.singleton(metadataExtractorBundle)); + log.debug("Bundle reloaded successfully!"); + } catch (Exception e) { + log.warn("Failed to refresh metadata exporter packages", e); + } + } + try { + return extractMetadata(file.getResource()); + } catch (SAXException | TikaException | NoClassDefFoundError e) { + throw new IOException("Failed to parse metadata after reloading metadata extractor bundle", e); + } + } catch (SAXException | TikaException e) { + throw new IOException("Failed to parse metadata", e); + } + } + + @Override + public void updateMetadata(File file) throws IOException { + updateMetadata(file, true); + } + + @Override + public void updateMetadata(File file, boolean persist) throws IOException { + log.trace("Updating metadata for {}, persist {}", file, persist); + try { + Resource resource = file.getResource(); + Resource content = resource.getChild(JcrConstants.JCR_CONTENT); + if (content == null) { + log.warn("Content resource is null"); + return; + } + + Map<String, Object> properties = null; + Resource metadata = content.getChild(CMSConstants.NN_METADATA); + if (metadata != null) { + properties = metadata.adaptTo(ModifiableValueMap.class); + } else { + properties = new HashMap<>(); + properties.put(JcrConstants.JCR_PRIMARYTYPE, JcrConstants.NT_UNSTRUCTURED); + } + if (properties != null) { + properties.putAll(extractMetadata(file.getResource())); + if (metadata == null) { + resource.getResourceResolver().create(content, CMSConstants.NN_METADATA, properties); + } + if (persist) { + resource.getResourceResolver().commit(); + } + log.info("Metadata extracted from {}", resource.getPath()); + } else { + throw new IOException("Unable to update metadata for " + resource.getPath()); + } + } catch (SAXException | TikaException e) { + throw new IOException("Failed to parse metadata", e); + } + + } + + public Map<String, Object> extractMetadata(Resource resource) throws IOException, SAXException, TikaException { + log.info("Extracting metadata from {}", resource.getPath()); + InputStream is = resource.adaptTo(InputStream.class); + Map<String, Object> properties = new HashMap<>(); + Parser parser = new AutoDetectParser(); + BodyContentHandler handler = new BodyContentHandler(); + Metadata md = new Metadata(); + ParseContext context = new ParseContext(); + parser.parse(is, handler, md, context); + for (String name : md.names()) { + putMetadata(properties, name, md); + } + return properties; + + } + + private void putMetadata(Map<String, Object> properties, String name, Metadata metadata) { + log.trace("Updating property: {}", name); + String filtered = Text.escapeIllegalJcrChars(name); + Property property = Property.get(name); + if (property != null) { + if (metadata.isMultiValued(property)) { + properties.put(filtered, metadata.getValues(property)); + } else if (metadata.getDate(property) != null) { + Calendar cal = Calendar.getInstance(); + cal.setTime(metadata.getDate(property)); + properties.put(filtered, cal); + } else if (metadata.getInt(property) != null) { + properties.put(filtered, metadata.getInt(property)); + } else { + properties.put(filtered, metadata.get(property)); + } + } else { + properties.put(filtered, metadata.get(name)); + } + } + +} diff --git a/core/src/main/java/org/apache/sling/cms/core/internal/jobs/FileMetadataExtractorJob.java b/core/src/main/java/org/apache/sling/cms/core/internal/jobs/FileMetadataExtractorJob.java index b09d0ce..5655c0d 100644 --- a/core/src/main/java/org/apache/sling/cms/core/internal/jobs/FileMetadataExtractorJob.java +++ b/core/src/main/java/org/apache/sling/cms/core/internal/jobs/FileMetadataExtractorJob.java @@ -16,6 +16,7 @@ */ package org.apache.sling.cms.core.internal.jobs; +import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -26,7 +27,7 @@ import org.apache.sling.api.resource.ResourceResolverFactory; import org.apache.sling.cms.CMSConstants; import org.apache.sling.cms.ConfigurableJobExecutor; import org.apache.sling.cms.File; -import org.apache.sling.cms.core.internal.listeners.FileMetadataExtractor; +import org.apache.sling.cms.FileMetadataExtractor; import org.apache.sling.event.jobs.Job; import org.apache.sling.event.jobs.consumer.JobConsumer; import org.apache.sling.event.jobs.consumer.JobExecutionContext; @@ -89,11 +90,11 @@ public class FileMetadataExtractorJob extends ConfigurableJobExecutor { extractor.extractMetadata(file); context.incrementProgressCount(processed++); context.log("Extracted metadata for {0}", file.getPath()); - } catch (Throwable t) { + } catch (IOException e) { context.log("Failed to extract matadata for {0}", file.getPath()); context.incrementProgressCount(processed++); - context.log("Exception {0}", t.getMessage()); - log.warn("Failed to extract metadata for " + file.getPath(), t); + context.log("Exception {0}", e.getMessage()); + log.warn("Failed to extract metadata for " + file.getPath(), e); } } diff --git a/core/src/main/java/org/apache/sling/cms/core/internal/listeners/FileMetadataExtractor.java b/core/src/main/java/org/apache/sling/cms/core/internal/listeners/FileMetadataExtractor.java deleted file mode 100644 index d2b94a6..0000000 --- a/core/src/main/java/org/apache/sling/cms/core/internal/listeners/FileMetadataExtractor.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.sling.cms.core.internal.listeners; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Calendar; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.apache.jackrabbit.JcrConstants; -import org.apache.jackrabbit.util.Text; -import org.apache.sling.api.resource.LoginException; -import org.apache.sling.api.resource.ModifiableValueMap; -import org.apache.sling.api.resource.Resource; -import org.apache.sling.api.resource.ResourceResolver; -import org.apache.sling.api.resource.ResourceResolverFactory; -import org.apache.sling.api.resource.observation.ExternalResourceChangeListener; -import org.apache.sling.api.resource.observation.ResourceChange; -import org.apache.sling.api.resource.observation.ResourceChangeListener; -import org.apache.sling.cms.File; -import org.apache.tika.exception.TikaException; -import org.apache.tika.metadata.Metadata; -import org.apache.tika.metadata.Property; -import org.apache.tika.parser.AutoDetectParser; -import org.apache.tika.parser.ParseContext; -import org.apache.tika.parser.Parser; -import org.apache.tika.sax.BodyContentHandler; -import org.osgi.service.component.annotations.Component; -import org.osgi.service.component.annotations.Reference; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.xml.sax.SAXException; - -/** - * A Resource Change Listener which extracts the metadata from sling:Files when - * they are uploaded. - */ -@Component(service = { FileMetadataExtractor.class, ResourceChangeListener.class, - ExternalResourceChangeListener.class }, property = { ResourceChangeListener.CHANGES + "=ADDED", - ResourceChangeListener.PATHS + "=/content", - ResourceChangeListener.PATHS + "=/static" }, immediate = true) -public class FileMetadataExtractor implements ResourceChangeListener, ExternalResourceChangeListener { - - public static final String NN_METADATA = "metadata"; - public static final String PN_X_PARSED_BY = "X-Parsed-By"; - - @Reference - private ResourceResolverFactory factory; - - private static final Logger log = LoggerFactory.getLogger(FileMetadataExtractor.class); - - public void extractMetadata(File file) throws IOException, SAXException, TikaException { - extractMetadata(file.getResource()); - } - - public void extractMetadata(Resource resource) throws IOException, SAXException, TikaException { - - log.info("Extracting metadata from {}", resource.getPath()); - ResourceResolver resolver = resource.getResourceResolver(); - InputStream is = resource.adaptTo(InputStream.class); - Resource content = resource.getChild(JcrConstants.JCR_CONTENT); - if (content == null) { - log.warn("Content resource is null"); - return; - } - Map<String, Object> properties = new HashMap<>(); - Resource metadata = content.getChild(NN_METADATA); - if (metadata != null) { - properties = metadata.adaptTo(ModifiableValueMap.class); - } else { - properties.put(JcrConstants.JCR_PRIMARYTYPE, JcrConstants.NT_UNSTRUCTURED); - } - if (properties != null) { - Parser parser = new AutoDetectParser(); - BodyContentHandler handler = new BodyContentHandler(); - Metadata md = new Metadata(); - ParseContext context = new ParseContext(); - parser.parse(is, handler, md, context); - for (String name : md.names()) { - updateProperty(properties, name, md); - } - if (metadata == null) { - resolver.create(content, NN_METADATA, properties); - } - resolver.commit(); - log.info("Metadata extracted from {}", resource.getPath()); - } else { - log.warn("Failed to update metadata for {}", resource.getPath()); - } - - } - - private void updateProperty(Map<String, Object> properties, String name, Metadata metadata) { - log.trace("Updating property: {}", name); - String filtered = Text.escapeIllegalJcrChars(name); - Property property = Property.get(name); - if (property != null) { - if (metadata.isMultiValued(property)) { - properties.put(filtered, metadata.getValues(property)); - } else if (metadata.getDate(property) != null) { - Calendar cal = Calendar.getInstance(); - cal.setTime(metadata.getDate(property)); - properties.put(filtered, cal); - } else if (metadata.getInt(property) != null) { - properties.put(filtered, metadata.getInt(property)); - } else { - properties.put(filtered, metadata.get(property)); - } - } else { - properties.put(filtered, metadata.get(name)); - } - } - - @Override - public void onChange(List<ResourceChange> changes) { - Map<String, Object> serviceParams = new HashMap<>(); - serviceParams.put(ResourceResolverFactory.SUBSERVICE, "sling-cms-metadata"); - ResourceResolver serviceResolver = null; - try { - serviceResolver = factory.getServiceResourceResolver(serviceParams); - for (ResourceChange rc : changes) { - Resource changed = serviceResolver.getResource(rc.getPath()); - try { - extractMetadata(changed); - } catch (Throwable t) { - log.warn("Failed to extract metadata due to exception", t); - } - } - } catch (LoginException e) { - log.error("Exception getting service user", e); - } finally { - if (serviceResolver != null) { - serviceResolver.close(); - } - } - - } - -} diff --git a/core/src/main/java/org/apache/sling/cms/core/internal/listeners/FileMetadataExtractorListener.java b/core/src/main/java/org/apache/sling/cms/core/internal/listeners/FileMetadataExtractorListener.java new file mode 100644 index 0000000..008c81f --- /dev/null +++ b/core/src/main/java/org/apache/sling/cms/core/internal/listeners/FileMetadataExtractorListener.java @@ -0,0 +1,90 @@ +/* + * 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.sling.cms.core.internal.listeners; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.apache.sling.api.resource.LoginException; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.api.resource.ResourceResolverFactory; +import org.apache.sling.api.resource.observation.ExternalResourceChangeListener; +import org.apache.sling.api.resource.observation.ResourceChange; +import org.apache.sling.api.resource.observation.ResourceChangeListener; +import org.apache.sling.cms.File; +import org.apache.sling.cms.FileMetadataExtractor; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * A Resource Change Listener which extracts the metadata from sling:Files when + * they are uploaded. + */ +@Component(service = { FileMetadataExtractorListener.class, ResourceChangeListener.class, + ExternalResourceChangeListener.class }, property = { ResourceChangeListener.CHANGES + "=ADDED", + ResourceChangeListener.PATHS + "=/content", + ResourceChangeListener.PATHS + "=/static" }, immediate = true) +public class FileMetadataExtractorListener implements ResourceChangeListener, ExternalResourceChangeListener { + + @Reference + private FileMetadataExtractor extractor; + + @Reference + private ResourceResolverFactory factory; + + private static final Logger log = LoggerFactory.getLogger(FileMetadataExtractorListener.class); + + @Override + public void onChange(List<ResourceChange> changes) { + Map<String, Object> serviceParams = new HashMap<>(); + serviceParams.put(ResourceResolverFactory.SUBSERVICE, "sling-cms-metadata"); + ResourceResolver serviceResolver = null; + try { + serviceResolver = factory.getServiceResourceResolver(serviceParams); + for (ResourceChange rc : changes) { + handleChange(serviceResolver, rc); + } + } catch (LoginException e) { + log.error("Exception getting service user", e); + } finally { + if (serviceResolver != null) { + serviceResolver.close(); + } + } + + } + + private void handleChange(ResourceResolver serviceResolver, ResourceChange rc) { + File changed = Optional.ofNullable(serviceResolver.getResource(rc.getPath())).map(r -> r.adaptTo(File.class)) + .orElse(null); + try { + if (changed != null) { + extractor.updateMetadata(changed); + } else { + log.warn("Failed to get File from: {}", rc.getPath()); + } + } catch (IOException t) { + log.warn("Failed to extract metadata due to exception", t); + } + } + +} diff --git a/core/src/main/java/org/apache/sling/cms/core/internal/models/FileImpl.java b/core/src/main/java/org/apache/sling/cms/core/internal/models/FileImpl.java index 91d6ea2..ded1c6b 100644 --- a/core/src/main/java/org/apache/sling/cms/core/internal/models/FileImpl.java +++ b/core/src/main/java/org/apache/sling/cms/core/internal/models/FileImpl.java @@ -28,10 +28,10 @@ import org.apache.jackrabbit.util.Text; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.resource.ValueMap; import org.apache.sling.api.wrappers.ValueMapDecorator; +import org.apache.sling.cms.CMSConstants; import org.apache.sling.cms.File; import org.apache.sling.cms.Site; import org.apache.sling.cms.SiteManager; -import org.apache.sling.cms.core.internal.listeners.FileMetadataExtractor; import org.apache.sling.models.annotations.Default; import org.apache.sling.models.annotations.Model; import org.apache.sling.models.annotations.Optional; @@ -118,7 +118,7 @@ public class FileImpl implements File { @Override public ValueMap getMetadata() { - Resource metadata = this.getContentResource().getChild(FileMetadataExtractor.NN_METADATA); + Resource metadata = this.getContentResource().getChild(CMSConstants.NN_METADATA); Map<String, Object> data = new TreeMap<>(); if (metadata != null) { metadata.getValueMap().entrySet() diff --git a/core/src/main/java/org/apache/sling/cms/core/internal/servlets/TransformServlet.java b/core/src/main/java/org/apache/sling/cms/core/internal/servlets/TransformServlet.java new file mode 100644 index 0000000..df79c9e --- /dev/null +++ b/core/src/main/java/org/apache/sling/cms/core/internal/servlets/TransformServlet.java @@ -0,0 +1,82 @@ +/* + * 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.sling.cms.core.internal.servlets; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Optional; + +import javax.imageio.ImageIO; +import javax.servlet.Servlet; +import javax.servlet.ServletException; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.rendering.ImageType; +import org.apache.pdfbox.rendering.PDFRenderer; +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.SlingHttpServletResponse; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.servlets.SlingSafeMethodsServlet; +import org.apache.sling.cms.File; +import org.osgi.service.component.annotations.Component; + +import com.google.common.net.MediaType; + +import net.coobird.thumbnailator.Thumbnails; +import net.coobird.thumbnailator.Thumbnails.Builder; +import net.coobird.thumbnailator.geometry.Positions; + +@Component(service = { Servlet.class }, property = { "sling.servlet.extensions=transform", + "sling.servlet.resourceTypes=sling:File", "sling.servlet.resourceTypes=nt:file" }) +public class TransformServlet extends SlingSafeMethodsServlet { + + private static final long serialVersionUID = -1513067546618762171L; + + protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) + throws ServletException, IOException { + + Builder<? extends InputStream> builder = Thumbnails.of(getInputStream(request.getResource())); + for (String cmd : request.getRequestPathInfo().getSuffix().split("/")) { + if (cmd.startsWith("resize-")) { + builder.size(Integer.parseInt(cmd.split("\\-")[1], 10), Integer.parseInt(cmd.split("\\-")[2], 10)); + } + } + builder.crop(Positions.CENTER); + response.setContentType("image/png"); + builder.toOutputStream(response.getOutputStream()); + } + + private InputStream getInputStream(Resource resource) throws IOException { + String contentType = Optional.ofNullable(resource.adaptTo(File.class)).map(File::getContentType).orElse(""); + if (contentType.startsWith("image")) { + return resource.adaptTo(InputStream.class); + } + if (MediaType.PDF.toString().equals(contentType)) { + PDDocument document = PDDocument.load(resource.adaptTo(InputStream.class)); + PDFRenderer pdfRenderer = new PDFRenderer(document); + BufferedImage bim = pdfRenderer.renderImageWithDPI(0, 300, ImageType.RGB); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + ImageIO.write(bim, "jpeg", os); + document.close(); + return new ByteArrayInputStream(os.toByteArray()); + } + return null; + } +} diff --git a/core/src/test/java/org/apache/sling/cms/core/internal/FileMetadataExtractorImplTest.java b/core/src/test/java/org/apache/sling/cms/core/internal/FileMetadataExtractorImplTest.java new file mode 100644 index 0000000..9ddf32f --- /dev/null +++ b/core/src/test/java/org/apache/sling/cms/core/internal/FileMetadataExtractorImplTest.java @@ -0,0 +1,62 @@ +/* + * 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.sling.cms.core.internal; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +import org.apache.sling.api.resource.Resource; +import org.apache.sling.cms.File; +import org.apache.sling.cms.FileMetadataExtractor; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class FileMetadataExtractorImplTest { + + private static final Logger log = LoggerFactory.getLogger(FileMetadataExtractorImplTest.class); + + private File file; + + @Before + public void init() { + + Resource resource = Mockito.mock(Resource.class); + Mockito.when(resource.adaptTo(InputStream.class)) + .thenReturn(FileMetadataExtractorImplTest.class.getClassLoader().getResourceAsStream("apache.png")); + + file = Mockito.mock(File.class); + Mockito.when(file.getResource()).thenReturn(resource); + } + + @Test + public void testExtractMetadata() throws IOException { + FileMetadataExtractor extractor = new FileMetadataExtractorImpl(); + Map<String, Object> metadata = extractor.extractMetadata(file); + + assertNotNull(metadata); + assertTrue(metadata.size() > 0); + + log.info("Extracted metadata: {}", metadata); + } +} diff --git a/core/src/test/resources/apache.png b/core/src/test/resources/apache.png new file mode 100644 index 0000000..fc3f667 Binary files /dev/null and b/core/src/test/resources/apache.png differ diff --git a/metadata-extractor/pom.xml b/metadata-extractor/pom.xml new file mode 100644 index 0000000..59327d5 --- /dev/null +++ b/metadata-extractor/pom.xml @@ -0,0 +1,102 @@ +<?xml version="1.0" encoding="ISO-8859-1"?> +<!-- Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with this work for additional + information regarding copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use this file except + in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to + in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + the License for the specific language governing permissions and limitations under the License. --> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd" +> + <modelVersion>4.0.0</modelVersion> + <parent> + <artifactId>org.apache.sling.cms</artifactId> + <groupId>org.apache.sling</groupId> + <version>0.11.3-SNAPSHOT</version> + </parent> + <artifactId>org.apache.sling.cms.metadata-extractor</artifactId> + <packaging>bundle</packaging> + <name>Apache Sling - Metadata Extractor</name> + <description>OSGi Bundle Wrapper for https://github.com/drewnoakes/metadata-extractor</description> + + <properties> + <sling.java.version>8</sling.java.version> + </properties> + + <build> + <plugins> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <extensions>true</extensions> + <configuration> + <instructions> + <Embed-Dependency>*;scope=compile|runtime</Embed-Dependency> + <Export-Package>com.drew.*</Export-Package> + <Import-Package>!com.adobe.xmp,!com.adobe.xmp.impl,!com.adobe.xmp.options,!com.adobe.xmp.properties,*</Import-Package> + </instructions> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <configuration> + <source>8</source> + <target>8</target> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.sling</groupId> + <artifactId>maven-sling-plugin</artifactId> + <configuration> + <slingUrl>${sling.protocol}://${sling.host}:${sling.port}/system/console</slingUrl> + <user>${sling.username}</user> + <password>${sling.password}</password> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <useSystemClassLoader>false</useSystemClassLoader> + </configuration> + </plugin> + </plugins> + </build> + + <profiles> + <profile> + <id>autoInstallBundle</id> + <build> + <plugins> + <plugin> + <groupId>org.apache.sling</groupId> + <artifactId>maven-sling-plugin</artifactId> + <executions> + <execution> + <id>install-bundle</id> + <goals> + <goal>install</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> + </profile> + </profiles> + + <dependencies> + <dependency> + <groupId>com.drewnoakes</groupId> + <artifactId>metadata-extractor</artifactId> + <version>2.11.0</version> + <scope>compile</scope> + </dependency> + <dependency> + <groupId>com.adobe.xmp</groupId> + <artifactId>xmpcore</artifactId> + <version>6.0.6</version> + <scope>compile</scope> + </dependency> + </dependencies> +</project> \ No newline at end of file diff --git a/pom.xml b/pom.xml index 0939d7e..f186526 100644 --- a/pom.xml +++ b/pom.xml @@ -37,6 +37,7 @@ <modules> <module>api</module> <module>core</module> + <module>metadata-extractor</module> <module>ui</module> <module>reference</module> <module>builder</module> @@ -225,6 +226,18 @@ <version>2.2.0</version> <scope>provided</scope> </dependency> + <dependency> + <groupId>org.apache.pdfbox</groupId> + <artifactId>pdfbox</artifactId> + <version>2.0.12</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <version>2.27.0</version> + <scope>test</scope> + </dependency> </dependencies> </dependencyManagement> diff --git a/ui/src/main/resources/jcr_root/libs/sling-cms/install/org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended-sling-cms-metadata.json b/ui/src/main/resources/jcr_root/libs/sling-cms/install/org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended-sling-cms-metadata.json new file mode 100644 index 0000000..458a8de --- /dev/null +++ b/ui/src/main/resources/jcr_root/libs/sling-cms/install/org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended-sling-cms-metadata.json @@ -0,0 +1,6 @@ +{ + "jcr:primaryType": "sling:OsgiConfig", + "user.mapping": [ + "org.apache.sling.cms.core:sling-cms-metadata=sling-cms-metadata" + ] +} \ No newline at end of file
