diff --git a/pom.xml b/pom.xml
index bf8e16421..a98c2661f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -113,6 +113,8 @@ Integrates with Spring Data, Spring Data REST and Apache Solr
./spring-content-s3-boot-starter
./spring-content-renditions
./spring-content-renditions-boot-starter
+ ./spring-content-metadata-extraction
+ ./spring-content-metadata-extraction-boot-starter
./spring-content-solr
./spring-content-solr-boot-starter
./spring-content-elasticsearch
diff --git a/spring-content-autoconfigure/pom.xml b/spring-content-autoconfigure/pom.xml
index 461412678..3bc1564b5 100644
--- a/spring-content-autoconfigure/pom.xml
+++ b/spring-content-autoconfigure/pom.xml
@@ -58,6 +58,12 @@
3.0.17-SNAPSHOT
true
+
+ com.github.paulcwarren
+ spring-content-metadata-extraction
+ 3.0.17-SNAPSHOT
+ true
+
diff --git a/spring-content-autoconfigure/src/main/java/internal/org/springframework/content/metadataextraction/boot/autoconfigure/MetadataExtractionContentAutoConfiguration.java b/spring-content-autoconfigure/src/main/java/internal/org/springframework/content/metadataextraction/boot/autoconfigure/MetadataExtractionContentAutoConfiguration.java
new file mode 100644
index 000000000..b25099eba
--- /dev/null
+++ b/spring-content-autoconfigure/src/main/java/internal/org/springframework/content/metadataextraction/boot/autoconfigure/MetadataExtractionContentAutoConfiguration.java
@@ -0,0 +1,41 @@
+/* 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 internal.org.springframework.content.metadataextraction.boot.autoconfigure;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.content.metadataextraction.config.MetadataExtractionConfiguration;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+/**
+ * Autoconfiguration class for enabling metadata extraction functionality in a Spring Boot application.
+ *
+ * This configuration class is activated when the {@link MetadataExtractionConfiguration} class is present in the classpath.
+ *
+ *
+ * By including this class, metadata extraction features are automatically configured without requiring explicit registration
+ * of the necessary components, simplifying integration into the application context.
+ *
+ *
+ * @author marcobelligoli
+ */
+@Configuration
+@ConditionalOnClass(MetadataExtractionConfiguration.class)
+@Import(MetadataExtractionConfiguration.class)
+public class MetadataExtractionContentAutoConfiguration {
+
+}
diff --git a/spring-content-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-content-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
index 257eb9eee..c3ed82d7b 100644
--- a/spring-content-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
+++ b/spring-content-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
@@ -3,6 +3,7 @@ internal.org.springframework.content.fs.boot.autoconfigure.FilesystemContentAuto
internal.org.springframework.content.jpa.boot.autoconfigure.JpaContentAutoConfiguration
internal.org.springframework.content.mongo.boot.autoconfigure.MongoContentAutoConfiguration
internal.org.springframework.content.renditions.boot.autoconfigure.RenditionsContentAutoConfiguration
+internal.org.springframework.content.metadataextraction.boot.autoconfigure.MetadataExtractionContentAutoConfiguration
internal.org.springframework.content.rest.boot.autoconfigure.ContentRestAutoConfiguration
internal.org.springframework.content.rest.boot.autoconfigure.HypermediaAutoConfiguration
internal.org.springframework.content.s3.boot.autoconfigure.S3ContentAutoConfiguration
diff --git a/spring-content-autoconfigure/src/test/java/org/springframework/content/metadataextraction/boot/ContentMetadataExtractionAutoConfigurationTest.java b/spring-content-autoconfigure/src/test/java/org/springframework/content/metadataextraction/boot/ContentMetadataExtractionAutoConfigurationTest.java
new file mode 100644
index 000000000..1d663b690
--- /dev/null
+++ b/spring-content-autoconfigure/src/test/java/org/springframework/content/metadataextraction/boot/ContentMetadataExtractionAutoConfigurationTest.java
@@ -0,0 +1,80 @@
+/* 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.springframework.content.metadataextraction.boot;
+
+import com.github.paulcwarren.ginkgo4j.Ginkgo4jConfiguration;
+import com.github.paulcwarren.ginkgo4j.Ginkgo4jRunner;
+import internal.org.springframework.content.s3.boot.autoconfigure.S3ContentAutoConfiguration;
+import internal.org.springframework.content.solr.boot.autoconfigure.SolrAutoConfiguration;
+import internal.org.springframework.content.solr.boot.autoconfigure.SolrExtensionAutoConfiguration;
+import org.junit.runner.RunWith;
+import org.springframework.boot.autoconfigure.AutoConfigurationPackage;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.content.commons.metadataextraction.MetadataExtractionService;
+import org.springframework.content.commons.renditions.Renderable;
+import org.springframework.content.commons.repository.ContentStore;
+import org.springframework.content.metadataextraction.extractors.DefaultMetadataExtractor;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.EnableMBeanExport;
+import org.springframework.jmx.support.RegistrationPolicy;
+import org.springframework.support.TestEntity;
+
+import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Context;
+import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.Describe;
+import static com.github.paulcwarren.ginkgo4j.Ginkgo4jDSL.It;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * Test class for verifying the functionality of the ContentMetadataExtractionAutoConfiguration.
+ *
+ * @author marcobelligoli
+ */
+@RunWith(Ginkgo4jRunner.class)
+@Ginkgo4jConfiguration(threads = 1)
+public class ContentMetadataExtractionAutoConfigurationTest {
+
+ {
+ Describe("ContentMetadataExtractionAutoConfiguration",
+ () -> Context("given a default configuration", () -> It("should load the all metadata extractors", () -> {
+
+ AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
+ context.register(TestConfig.class);
+ context.refresh();
+
+ assertThat(context.getBean(MetadataExtractionService.class), is(not(nullValue())));
+ assertThat(context.getBean(DefaultMetadataExtractor.class), is(not(nullValue())));
+
+ context.close();
+ })));
+ }
+
+ @Configuration
+ @AutoConfigurationPackage
+ @EnableAutoConfiguration(exclude = { SolrAutoConfiguration.class, SolrExtensionAutoConfiguration.class, S3ContentAutoConfiguration.class })
+ @EnableMBeanExport(registration = RegistrationPolicy.IGNORE_EXISTING)
+ public static class TestConfig {
+
+ }
+
+ public interface TestEntityContentStore extends ContentStore, Renderable {
+
+ }
+}
diff --git a/spring-content-commons/src/main/java/internal/org/springframework/content/commons/metadataextraction/MetadataExtractionServiceImpl.java b/spring-content-commons/src/main/java/internal/org/springframework/content/commons/metadataextraction/MetadataExtractionServiceImpl.java
new file mode 100644
index 000000000..8241a0226
--- /dev/null
+++ b/spring-content-commons/src/main/java/internal/org/springframework/content/commons/metadataextraction/MetadataExtractionServiceImpl.java
@@ -0,0 +1,54 @@
+/* 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 internal.org.springframework.content.commons.metadataextraction;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.content.commons.metadataextraction.MetadataExtractionService;
+import org.springframework.content.commons.metadataextraction.MetadataExtractor;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Implementation of the {@link MetadataExtractionService} interface.
+ *
+ * @author marcobelligoli
+ */
+public class MetadataExtractionServiceImpl implements MetadataExtractionService {
+
+ private final List metadataExtractorList = new ArrayList<>();
+
+ @Autowired(required = false)
+ public MetadataExtractionServiceImpl(MetadataExtractor... metadataExtractors) {
+
+ Collections.addAll(this.metadataExtractorList, metadataExtractors);
+ }
+
+ @Override
+ public Map extractMetadata(File file) {
+
+ Map fullMetadataMap = new HashMap<>();
+ for (var metadataExtractor : metadataExtractorList) {
+ fullMetadataMap.putAll(metadataExtractor.extractMetadata(file));
+ }
+ return fullMetadataMap;
+ }
+}
diff --git a/spring-content-commons/src/main/java/org/springframework/content/commons/metadataextraction/MetadataExtractionService.java b/spring-content-commons/src/main/java/org/springframework/content/commons/metadataextraction/MetadataExtractionService.java
new file mode 100644
index 000000000..85592c1a3
--- /dev/null
+++ b/spring-content-commons/src/main/java/org/springframework/content/commons/metadataextraction/MetadataExtractionService.java
@@ -0,0 +1,45 @@
+/* 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.springframework.content.commons.metadataextraction;
+
+import java.io.File;
+import java.util.Map;
+
+/**
+ * A service that extracts metadata from a given file.
+ *
+ * The extracted metadata will be returned as a map where the keys represent
+ * the metadata property names and the values represent the respective metadata values.
+ *
+ *
+ * This service retrieves all instances of {@link MetadataExtractor} present in the Spring context and,
+ * for each of them, performs metadata extraction from the provided file.
+ *
+ *
+ * @author marcobelligoli
+ */
+public interface MetadataExtractionService {
+
+ /**
+ * Extracts metadata from the specified file.
+ *
+ * @param file the file from which metadata will be extracted
+ * @return a map containing the extracted metadata, where keys represent
+ * metadata property names, and values represent the respective metadata values
+ */
+ Map extractMetadata(File file);
+}
diff --git a/spring-content-commons/src/main/java/org/springframework/content/commons/metadataextraction/MetadataExtractor.java b/spring-content-commons/src/main/java/org/springframework/content/commons/metadataextraction/MetadataExtractor.java
new file mode 100644
index 000000000..80aef61fb
--- /dev/null
+++ b/spring-content-commons/src/main/java/org/springframework/content/commons/metadataextraction/MetadataExtractor.java
@@ -0,0 +1,36 @@
+/* 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.springframework.content.commons.metadataextraction;
+
+import java.io.File;
+import java.util.Map;
+
+/**
+ * Interface of MetadataExtractor component
+ *
+ * @author marcobelligoli
+ */
+public interface MetadataExtractor {
+
+ /**
+ * Extracts metadata from the given file.
+ *
+ * @param file the file from which metadata is to be extracted
+ * @return a map containing metadata as key-value pairs
+ */
+ Map extractMetadata(File file);
+}
diff --git a/spring-content-commons/src/test/java/internal/org/springframework/content/commons/metadataextraction/MetadataExtractionServiceImplTest.java b/spring-content-commons/src/test/java/internal/org/springframework/content/commons/metadataextraction/MetadataExtractionServiceImplTest.java
new file mode 100644
index 000000000..8252b0167
--- /dev/null
+++ b/spring-content-commons/src/test/java/internal/org/springframework/content/commons/metadataextraction/MetadataExtractionServiceImplTest.java
@@ -0,0 +1,129 @@
+/* Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License. */
+package internal.org.springframework.content.commons.metadataextraction;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.springframework.content.commons.metadataextraction.MetadataExtractor;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * This class tests the `extractMetadata` method in the `MetadataExtractionServiceImpl` class.
+ * The method is responsible for iterating over multiple `MetadataExtractor` instances and aggregating
+ * the metadata they extract into a single map.
+ *
+ * @author marcobelligoli
+ */
+public class MetadataExtractionServiceImplTest {
+
+ @Test
+ public void testExtractMetadataWithSingleExtractor() {
+
+ File mockFile = Mockito.mock(File.class);
+ MetadataExtractor mockExtractor = Mockito.mock(MetadataExtractor.class);
+ MetadataExtractionServiceImpl service = new MetadataExtractionServiceImpl(mockExtractor);
+
+ Map mockMetadata = new HashMap<>();
+ mockMetadata.put("author", "John Doe");
+ Mockito.when(mockExtractor.extractMetadata(mockFile)).thenReturn(mockMetadata);
+
+ Map result = service.extractMetadata(mockFile);
+
+ Assert.assertEquals(1, result.size());
+ Assert.assertEquals("John Doe", result.get("author"));
+ Mockito.verify(mockExtractor).extractMetadata(mockFile);
+ }
+
+ @Test
+ public void testExtractMetadataWithMultipleExtractors() {
+
+ File mockFile = Mockito.mock(File.class);
+ MetadataExtractor mockExtractor1 = Mockito.mock(MetadataExtractor.class);
+ MetadataExtractor mockExtractor2 = Mockito.mock(MetadataExtractor.class);
+ MetadataExtractionServiceImpl service = new MetadataExtractionServiceImpl(mockExtractor1, mockExtractor2);
+
+ Map mockMetadata1 = new HashMap<>();
+ mockMetadata1.put("author", "John Doe");
+ Mockito.when(mockExtractor1.extractMetadata(mockFile)).thenReturn(mockMetadata1);
+
+ Map mockMetadata2 = new HashMap<>();
+ mockMetadata2.put("title", "Sample Document");
+ Mockito.when(mockExtractor2.extractMetadata(mockFile)).thenReturn(mockMetadata2);
+
+ Map result = service.extractMetadata(mockFile);
+
+ Assert.assertEquals(2, result.size());
+ Assert.assertEquals("John Doe", result.get("author"));
+ Assert.assertEquals("Sample Document", result.get("title"));
+ Mockito.verify(mockExtractor1).extractMetadata(mockFile);
+ Mockito.verify(mockExtractor2).extractMetadata(mockFile);
+ }
+
+ @Test
+ public void testExtractMetadataWithConflictingKeys() {
+
+ File mockFile = Mockito.mock(File.class);
+ MetadataExtractor mockExtractor1 = Mockito.mock(MetadataExtractor.class);
+ MetadataExtractor mockExtractor2 = Mockito.mock(MetadataExtractor.class);
+ MetadataExtractionServiceImpl service = new MetadataExtractionServiceImpl(mockExtractor1, mockExtractor2);
+
+ Map mockMetadata1 = new HashMap<>();
+ mockMetadata1.put("key", "value1");
+ Mockito.when(mockExtractor1.extractMetadata(mockFile)).thenReturn(mockMetadata1);
+
+ Map mockMetadata2 = new HashMap<>();
+ mockMetadata2.put("key", "value2");
+ Mockito.when(mockExtractor2.extractMetadata(mockFile)).thenReturn(mockMetadata2);
+
+ Map result = service.extractMetadata(mockFile);
+
+ Assert.assertEquals(1, result.size());
+ Assert.assertEquals("value2", result.get("key")); // Last extractor's value overrides
+ Mockito.verify(mockExtractor1).extractMetadata(mockFile);
+ Mockito.verify(mockExtractor2).extractMetadata(mockFile);
+ }
+
+ @Test
+ public void testExtractMetadataWithNoExtractors() {
+
+ File mockFile = Mockito.mock(File.class);
+ MetadataExtractionServiceImpl service = new MetadataExtractionServiceImpl();
+
+ Map result = service.extractMetadata(mockFile);
+
+ Assert.assertTrue(result.isEmpty());
+ }
+
+ @Test
+ public void testExtractMetadataWithEmptyMetadataFromExtractor() {
+
+ File mockFile = Mockito.mock(File.class);
+ MetadataExtractor mockExtractor = Mockito.mock(MetadataExtractor.class);
+ MetadataExtractionServiceImpl service = new MetadataExtractionServiceImpl(mockExtractor);
+
+ Mockito.when(mockExtractor.extractMetadata(mockFile)).thenReturn(new HashMap<>());
+
+ Map result = service.extractMetadata(mockFile);
+
+ Assert.assertTrue(result.isEmpty());
+ Mockito.verify(mockExtractor).extractMetadata(mockFile);
+ }
+}
\ No newline at end of file
diff --git a/spring-content-metadata-extraction-boot-starter/pom.xml b/spring-content-metadata-extraction-boot-starter/pom.xml
new file mode 100644
index 000000000..2c85856fa
--- /dev/null
+++ b/spring-content-metadata-extraction-boot-starter/pom.xml
@@ -0,0 +1,26 @@
+
+
+
+ spring-content
+ com.github.paulcwarren
+ 3.0.17-SNAPSHOT
+
+ 4.0.0
+
+ spring-content-metadata-extraction-boot-starter
+
+
+
+ com.github.paulcwarren
+ spring-content-metadata-extraction
+ 3.0.17-SNAPSHOT
+
+
+ com.github.paulcwarren
+ spring-content-autoconfigure
+ 3.0.17-SNAPSHOT
+
+
+
\ No newline at end of file
diff --git a/spring-content-metadata-extraction-boot-starter/src/main/java/.empty b/spring-content-metadata-extraction-boot-starter/src/main/java/.empty
new file mode 100644
index 000000000..e69de29bb
diff --git a/spring-content-metadata-extraction-boot-starter/src/main/resources/spring.provides b/spring-content-metadata-extraction-boot-starter/src/main/resources/spring.provides
new file mode 100644
index 000000000..ae3d7955a
--- /dev/null
+++ b/spring-content-metadata-extraction-boot-starter/src/main/resources/spring.provides
@@ -0,0 +1 @@
+provides: spring-content-metadata-extraction
diff --git a/spring-content-metadata-extraction-boot-starter/src/test/java/.empty b/spring-content-metadata-extraction-boot-starter/src/test/java/.empty
new file mode 100644
index 000000000..e69de29bb
diff --git a/spring-content-metadata-extraction/pom.xml b/spring-content-metadata-extraction/pom.xml
new file mode 100644
index 000000000..b5754987a
--- /dev/null
+++ b/spring-content-metadata-extraction/pom.xml
@@ -0,0 +1,65 @@
+
+
+
+ spring-content
+ com.github.paulcwarren
+ 3.0.17-SNAPSHOT
+
+ 4.0.0
+
+ spring-content-metadata-extraction
+
+
+
+ org.springframework.cloud
+ spring-cloud-dependencies
+ ${spring-cloud.version}
+ pom
+ import
+
+
+
+
+
+ com.github.paulcwarren
+ spring-content-commons
+ 3.0.17-SNAPSHOT
+
+
+ junit
+ junit
+ test
+
+
+ org.mockito
+ mockito-core
+ 3.12.4
+ test
+
+
+ org.springframework
+ spring-web
+
+
+ org.springframework.boot
+ spring-boot-autoconfigure
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+ org.springframework.cloud
+ spring-cloud-starter-contract-stub-runner
+ test
+
+
+
+
\ No newline at end of file
diff --git a/spring-content-metadata-extraction/src/main/asciidoc/metadata-extraction-index.adoc b/spring-content-metadata-extraction/src/main/asciidoc/metadata-extraction-index.adoc
new file mode 100644
index 000000000..3ae3e8c53
--- /dev/null
+++ b/spring-content-metadata-extraction/src/main/asciidoc/metadata-extraction-index.adoc
@@ -0,0 +1,25 @@
+= Spring Content Metadata Extraction - Reference Documentation
+Paul Warren, Marco Belligoli
+:revnumber: {version}
+:revdate: {localdate}
+:toc:
+:toc-placement!:
+:spring-content-commons-docs: ../../../../spring-content-commons/src/main/asciidoc
+
+(C) 2019-present The original authors.
+
+NOTE: Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically.
+
+toc::[]
+
+:numbered:
+
+include::metadata-extraction-preface.adoc[]
+
+:leveloffset: +1
+include::{spring-content-commons-docs}/content-repositories.adoc[]
+:leveloffset: -1
+
+:leveloffset: +1
+include::metadata-extraction.adoc[]
+:leveloffset: -1
diff --git a/spring-content-metadata-extraction/src/main/asciidoc/metadata-extraction-preface.adoc b/spring-content-metadata-extraction/src/main/asciidoc/metadata-extraction-preface.adoc
new file mode 100644
index 000000000..9b72cc15e
--- /dev/null
+++ b/spring-content-metadata-extraction/src/main/asciidoc/metadata-extraction-preface.adoc
@@ -0,0 +1,11 @@
+[[preface]]
+= Preface
+
+[[project]]
+[preface]
+== Project metadata
+
+* Version control - http://github.com/paulcwarren/spring-content/
+* Bugtracker - http://github.com/paulcwarren/spring-content/issues
+* Release repository - https://repo1.maven.org/maven2/
+* Snapshots repository - https://oss.sonatype.org/content/repositories/snapshots
diff --git a/spring-content-metadata-extraction/src/main/asciidoc/metadata-extraction.adoc b/spring-content-metadata-extraction/src/main/asciidoc/metadata-extraction.adoc
new file mode 100644
index 000000000..d4cc4bf76
--- /dev/null
+++ b/spring-content-metadata-extraction/src/main/asciidoc/metadata-extraction.adoc
@@ -0,0 +1,124 @@
+[[renditions]]
+= Metadata Extraction
+
+== Overview
+
+This feature is capable of extracting a specific set of metadata from files and representing it as a map of key-value pairs.
+
+It also provides an extension point for providing your own metadata extractors.
+
+To enable metadata extraction add the `spring-content-metadata-extraction` or `spring-content-metadata-extraction-boot-starter` dependency to your
+project. This enables the default metadata extractor.
+
+== Maven Central Coordinates
+The maven coordinates for this Spring Content library are as follows:
+```xml
+
+ com.github.paulcwarren
+ spring-content-metadata-extraction
+
+```
+
+As it is usual to use several Spring Content libraries together importing the bom is recommended:
+```xml
+
+ com.github.paulcwarren
+ spring-content-bom
+ ${spring-content-version}
+ pom
+ import
+
+```
+
+== Annotation-based Configuration
+
+.Enabling Spring Content Metadata Extraction with Java Config
+====
+[source,java]
+----
+@Configuration
+@Import(org.springframework.content.metadataextraction.config.MetadataExtractionConfiguration.class) <1>
+public static class ApplicationConfig {
+}
+----
+1. Not required when using `spring-content-metadata-extraction-boot-starter`
+====
+
+== Extractors
+
+By default, there is only a `DefaultMetadataExtractor` which extracts the following metadata from each type of file:
+
+- fileName
+- fileExtension
+- size
+- mimeType
+- creationTime
+- lastModifiedTime
+- lastAccessTime
+
+== Alfresco Transform Core Metadata Extractor
+
+Spring content metadata extraction modules support the integration with the https://docs.alfresco.com/transform-service/latest/[Alfresco Transform Core] service.
+To use it, simply add the property `alfresco.transform.core.url` with the url of Alfresco Transform Core service.
+
+== Metadata Extraction Extension Point
+
+If you need to create custom Metadata Extractors to meet your specific requirements, you can implement the MetadataExtractor extension point.
+This involves implementing the `MetadataExtractor` interface, annotating this implementation as a `@Service`, and ensuring it is registered
+as a bean in your application context at runtime.
+
+.Implementing the MetadataExtractor Extension Point
+====
+[source, java]
+----
+package my.custom.metadata.extractor;
+
+@Service <2>
+public class CustomMetadataExtractor implements MetadataExtractor { <1>
+
+ @Override
+ public Map extractMetadata(File file) {
+ // your custom code
+ return new HashMap<>();
+ }
+}
+
+...
+
+@Configuration
+@ComponentScan("my.custom.metadata.extractor") <3>
+public static class ApplicationConfig {
+}
+----
+1. Implementation of `MetadataExtractor`
+2. Marked as an `@Service`
+3. Ensure the service is scanned by Spring and offered as a bean
+====
+
+== Metadata Extraction Service
+
+As with the renditions module, here too, a bean of type **"MetadataExtractionService"** is available, which automatically provides all instances of
+**MetadataExtractor** and allows to retrieve, in one central location, all the metadata extracted by the various extractors.
+
+.Example of usage of MetadataExtractionService
+====
+[source, java]
+----
+@Service
+public class MyServiceImpl implements MyService {
+
+ private MetadataExtractionService metadataExtractionService;
+
+ public MyServiceImpl(MetadataExtractionService metadataExtractionService) {
+ this.metadataExtractionService = metadataExtractionService; <1>
+ }
+
+ public Map getFileMetadata(File file) {
+ return metadataExtractionService.extractMetadata(file); <2>
+ }
+}
+
+----
+1. Injection of `MetadataExtractionService` in a custom service
+2. Extract file metadata using all instances of `MetadataExtractor` (default, AlfrescoTransformCore if enabled, custom, ...)
+====
diff --git a/spring-content-metadata-extraction/src/main/java/org/springframework/content/metadataextraction/MetadataExtractionException.java b/spring-content-metadata-extraction/src/main/java/org/springframework/content/metadataextraction/MetadataExtractionException.java
new file mode 100644
index 000000000..9498c4e70
--- /dev/null
+++ b/spring-content-metadata-extraction/src/main/java/org/springframework/content/metadataextraction/MetadataExtractionException.java
@@ -0,0 +1,38 @@
+/* Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License. */
+package org.springframework.content.metadataextraction;
+
+import org.springframework.content.commons.metadataextraction.MetadataExtractor;
+
+/**
+ * Signals an error during metadata extraction.
+ *
+ * This exception is typically thrown by components that implement
+ * the {@link MetadataExtractor}
+ * interface when an error occurs while attempting to extract metadata from a file.
+ * Encapsulates the root cause of the error.
+ *
+ *
+ * @author marcobelligoli
+ */
+public class MetadataExtractionException extends RuntimeException {
+
+ public MetadataExtractionException(Throwable cause) {
+
+ super(cause);
+ }
+}
diff --git a/spring-content-metadata-extraction/src/main/java/org/springframework/content/metadataextraction/MetadataExtractionUtils.java b/spring-content-metadata-extraction/src/main/java/org/springframework/content/metadataextraction/MetadataExtractionUtils.java
new file mode 100644
index 000000000..a1275ad8e
--- /dev/null
+++ b/spring-content-metadata-extraction/src/main/java/org/springframework/content/metadataextraction/MetadataExtractionUtils.java
@@ -0,0 +1,112 @@
+/* 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.springframework.content.metadataextraction;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.io.ByteArrayResource;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Utility class for metadata extraction operations.
+ * This class provides methods for converting files or input streams
+ * into {@link ByteArrayResource} objects.
+ *
+ * It is designed to be used as a utility class and therefore cannot be instantiated.
+ *
+ */
+public class MetadataExtractionUtils {
+
+ private MetadataExtractionUtils() {
+
+ throw new IllegalStateException("Utility class");
+ }
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(MetadataExtractionUtils.class);
+
+ /**
+ * Converts the contents of the given file into a {@link ByteArrayResource}.
+ * The method reads all data from the provided file and packages it
+ * into a {@link ByteArrayResource} for further use.
+ *
+ * @param file The file to be converted into a {@link ByteArrayResource}.
+ * Must not be null. The method will attempt to read the file's contents.
+ * @return A {@link ByteArrayResource} containing the file's byte data.
+ * Throws a {@link MetadataExtractionException} if the file cannot be read.
+ * @throws MetadataExtractionException If an I/O error occurs while accessing the file.
+ */
+ public static ByteArrayResource getByteArrayResourceFromFile(File file) {
+
+ try (InputStream inputStream = new FileInputStream(file)) {
+ return getByteArrayResource(inputStream, file.getName());
+ }
+ catch (IOException e) {
+ throw new MetadataExtractionException(e);
+ }
+ }
+
+ private static ByteArrayResource getByteArrayResource(InputStream inputStream, String fileName) {
+
+ if (inputStream == null) {
+ return null;
+ }
+
+ LOGGER.debug("Converting input stream in byte array resource...");
+
+ if (inputStream instanceof FileInputStream fileInputStream) {
+ File file;
+ try {
+ file = new File(fileInputStream.getFD().toString());
+ fileName = file.getName();
+ }
+ catch (IOException e) {
+ LOGGER.warn(e.getMessage());
+ }
+ }
+
+ byte[] fileBytes;
+ try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+ byte[] buffer = new byte[1024];
+ int bytesRead;
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
+ baos.write(buffer, 0, bytesRead);
+ }
+ fileBytes = baos.toByteArray();
+ }
+ catch (IOException e) {
+ throw new MetadataExtractionException(e);
+ }
+
+ String finalFileName = fileName;
+ var result = new ByteArrayResource(fileBytes) {
+
+ @Override
+ public String getFilename() {
+
+ return finalFileName;
+ }
+ };
+
+ LOGGER.debug("Conversion done");
+ return result;
+ }
+}
diff --git a/spring-content-metadata-extraction/src/main/java/org/springframework/content/metadataextraction/config/MetadataExtractionConfiguration.java b/spring-content-metadata-extraction/src/main/java/org/springframework/content/metadataextraction/config/MetadataExtractionConfiguration.java
new file mode 100644
index 000000000..d15087683
--- /dev/null
+++ b/spring-content-metadata-extraction/src/main/java/org/springframework/content/metadataextraction/config/MetadataExtractionConfiguration.java
@@ -0,0 +1,48 @@
+/* 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.springframework.content.metadataextraction.config;
+
+import internal.org.springframework.content.commons.metadataextraction.MetadataExtractionServiceImpl;
+import org.springframework.content.commons.metadataextraction.MetadataExtractionService;
+import org.springframework.content.commons.metadataextraction.MetadataExtractor;
+import org.springframework.content.metadataextraction.extractors.DefaultMetadataExtractor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Spring configuration class for metadata extraction services.
+ *
+ * This configuration enables component scanning to detect and register beans related to
+ * metadata extraction, including implementations of {@link MetadataExtractor}.
+ * It also defines a bean for the {@link MetadataExtractionService}, which aggregates
+ * various {@link MetadataExtractor} implementations to perform metadata extraction
+ * on a given file.
+ *
+ *
+ * @author marcobelligoli
+ */
+@Configuration
+@ComponentScan(basePackageClasses = DefaultMetadataExtractor.class)
+public class MetadataExtractionConfiguration {
+
+ @Bean
+ public MetadataExtractionService metadataExtractionService(MetadataExtractor... metadataExtractors) {
+
+ return new MetadataExtractionServiceImpl(metadataExtractors);
+ }
+}
diff --git a/spring-content-metadata-extraction/src/main/java/org/springframework/content/metadataextraction/extractors/AlfrescoTransformCoreMetadataExtractor.java b/spring-content-metadata-extraction/src/main/java/org/springframework/content/metadataextraction/extractors/AlfrescoTransformCoreMetadataExtractor.java
new file mode 100644
index 000000000..a9fcd0404
--- /dev/null
+++ b/spring-content-metadata-extraction/src/main/java/org/springframework/content/metadataextraction/extractors/AlfrescoTransformCoreMetadataExtractor.java
@@ -0,0 +1,102 @@
+/* Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License. */
+package org.springframework.content.metadataextraction.extractors;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.content.commons.metadataextraction.MetadataExtractor;
+import org.springframework.content.metadataextraction.MetadataExtractionException;
+import org.springframework.content.metadataextraction.MetadataExtractionUtils;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.client.RestTemplate;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * Implementation of the {@link MetadataExtractor} interface for extracting metadata
+ * by leveraging Alfresco Transform Core service. This class communicates with the
+ * Alfresco Transform Core service to retrieve metadata for a given file.
+ *
+ * This service is enabled conditionally based on the presence of the
+ * "alfresco.transform.core.url" property in the application configuration.
+ *
+ */
+@Service
+@ConditionalOnProperty(name = "alfresco.transform.core.url")
+public class AlfrescoTransformCoreMetadataExtractor implements MetadataExtractor {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(AlfrescoTransformCoreMetadataExtractor.class);
+ private static final String ALFRESCO_TRANSFORM_CORE_TRANSFORM_URL = "/transform";
+ private static final String TARGET_MIME_TYPE_METADATA_EXTRACTOR = "alfresco-metadata-extract";
+ private final String alfrescoTransformCoreUrl;
+ private final RestTemplate restTemplate;
+ private final ObjectMapper objectMapper;
+
+ @Autowired
+ public AlfrescoTransformCoreMetadataExtractor(@Value("${alfresco.transform.core.url}") String alfrescoTransformCoreUrl) {
+
+ this.alfrescoTransformCoreUrl = alfrescoTransformCoreUrl;
+ this.restTemplate = new RestTemplate();
+ this.objectMapper = new ObjectMapper();
+ }
+
+ @Override
+ public Map extractMetadata(File file) {
+
+ try {
+ String url = alfrescoTransformCoreUrl + ALFRESCO_TRANSFORM_CORE_TRANSFORM_URL;
+
+ MultiValueMap requestParams = new LinkedMultiValueMap<>();
+ requestParams.put("file", Collections.singletonList(MetadataExtractionUtils.getByteArrayResourceFromFile(file)));
+ String sourceMimeType = Files.probeContentType(file.toPath());
+ requestParams.put("sourceMimetype", Collections.singletonList(sourceMimeType));
+ requestParams.put("targetMimetype", Collections.singletonList(TARGET_MIME_TYPE_METADATA_EXTRACTOR));
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(org.springframework.http.MediaType.MULTIPART_FORM_DATA);
+
+ LOGGER.debug("Calling POST {} with params {}...", url, requestParams);
+ HttpEntity> requestEntity = new HttpEntity<>(requestParams, headers);
+ ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);
+ LOGGER.debug("POST to {} done. Returned: {}", url, response.getBody());
+
+ Map map;
+ map = objectMapper.readValue(response.getBody(), new TypeReference<>() {
+
+ });
+ return map;
+ }
+ catch (IOException e) {
+ throw new MetadataExtractionException(e);
+ }
+ }
+}
diff --git a/spring-content-metadata-extraction/src/main/java/org/springframework/content/metadataextraction/extractors/DefaultMetadataExtractor.java b/spring-content-metadata-extraction/src/main/java/org/springframework/content/metadataextraction/extractors/DefaultMetadataExtractor.java
new file mode 100644
index 000000000..d7428ff24
--- /dev/null
+++ b/spring-content-metadata-extraction/src/main/java/org/springframework/content/metadataextraction/extractors/DefaultMetadataExtractor.java
@@ -0,0 +1,85 @@
+/* Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License. */
+package org.springframework.content.metadataextraction.extractors;
+
+import org.apache.commons.io.FilenameUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.content.commons.metadataextraction.MetadataExtractor;
+import org.springframework.content.metadataextraction.MetadataExtractionException;
+import org.springframework.stereotype.Service;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A service implementation of the {@link MetadataExtractor} interface that extracts base metadata
+ * from a given file and provides it as a map of key-value pairs.
+ *
+ * This implementation retrieves detailed metadata using Java NIO's file attributes and utility
+ * classes. Metadata extracted by this class includes:
+ *
+ * - fileName
+ * - fileExtension
+ * - size
+ * - mimeType
+ * - creationTime
+ * - lastModifiedTime
+ * - lastAccessTime
+ *
+ *
+ *
+ * If an error occurs during metadata extraction, a {@link MetadataExtractionException} is
+ * thrown, encapsulating the root cause of the error. This class also logs the extraction process
+ * for debugging purposes.
+ *
+ *
+ * @author marcobelligoli
+ */
+@Service
+public class DefaultMetadataExtractor implements MetadataExtractor {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(DefaultMetadataExtractor.class);
+
+ @Override
+ public Map extractMetadata(File file) {
+
+ LOGGER.debug("Starting extractMetadata...");
+ Map metadata = new HashMap<>();
+ try {
+ if (file != null) {
+ BasicFileAttributes attr = Files.readAttributes(file.toPath(), BasicFileAttributes.class);
+ metadata.put("fileName", file.getName());
+ metadata.put("fileExtension", FilenameUtils.getExtension(file.getName()));
+ metadata.put("size", attr.size());
+ metadata.put("mimeType", Files.probeContentType(file.toPath()));
+ metadata.put("creationTime", attr.creationTime().toString());
+ metadata.put("lastModifiedTime", attr.lastModifiedTime().toString());
+ metadata.put("lastAccessTime", attr.lastAccessTime().toString());
+ }
+ }
+ catch (IOException e) {
+ throw new MetadataExtractionException(e);
+ }
+ LOGGER.debug("extractMetadata done");
+ return metadata;
+ }
+}
diff --git a/spring-content-metadata-extraction/src/test/java/org/springframework/content/metadataextraction/extractors/AlfrescoTransformCoreMetadataExtractorTest.java b/spring-content-metadata-extraction/src/test/java/org/springframework/content/metadataextraction/extractors/AlfrescoTransformCoreMetadataExtractorTest.java
new file mode 100644
index 000000000..8be600885
--- /dev/null
+++ b/spring-content-metadata-extraction/src/test/java/org/springframework/content/metadataextraction/extractors/AlfrescoTransformCoreMetadataExtractorTest.java
@@ -0,0 +1,115 @@
+/* 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.springframework.content.metadataextraction.extractors;
+
+import com.github.tomakehurst.wiremock.WireMockServer;
+import com.github.tomakehurst.wiremock.client.WireMock;
+import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.springframework.content.metadataextraction.MetadataExtractionException;
+
+import java.io.File;
+import java.net.URISyntaxException;
+import java.net.URL;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Test class for validating the functionality of the {@link AlfrescoTransformCoreMetadataExtractor}.
+ * This test suite contains unit tests to ensure correct metadata extraction behavior,
+ * proper handling of errors, and correct communication with the Alfresco Transform Core service.
+ *
+ * It sets up a mock wire server to simulate the behavior of the Alfresco Transform Core service.
+ * Additionally, it verifies both successful metadata extraction and error scenarios.
+ *
+ */
+class AlfrescoTransformCoreMetadataExtractorTest {
+
+ private static final String ALFRESCO_TRANSFORM_CORE_HOST = "localhost";
+ private static WireMockServer wireMockServer;
+ private static AlfrescoTransformCoreMetadataExtractor metadataExtractor;
+
+ @BeforeAll
+ static void setup() {
+
+ wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort());
+ wireMockServer.start();
+ var port = wireMockServer.port();
+ WireMock.configureFor(ALFRESCO_TRANSFORM_CORE_HOST, port);
+ var baseUrl = String.format("http://%s:%d", ALFRESCO_TRANSFORM_CORE_HOST, port);
+ metadataExtractor = new AlfrescoTransformCoreMetadataExtractor(baseUrl);
+ }
+
+ @AfterAll
+ static void tearDown() {
+
+ if (wireMockServer != null) {
+ wireMockServer.stop();
+ }
+ }
+
+ private static void mockAlfrescoTransformCoreResponse(String response) {
+
+ stubFor(post(urlEqualTo("/transform")).willReturn(aResponse().withHeader("Content-Type", "application/json").withBody(response)));
+ }
+
+ @Test
+ void testExtractMetadata()
+ throws URISyntaxException {
+
+ File input = getFile();
+
+ mockAlfrescoTransformCoreResponse(
+ "{\"{http://www.alfresco.org/model/content/1.0}author\":\"John Doe\",\"{http://www.alfresco.org/model/content/1.0}created\":\"2024-04-16T07:39:22Z\",\"{http://www.alfresco.org/model/content/1.0}title\":null}");
+
+ var result = metadataExtractor.extractMetadata(input);
+
+ assertNotNull(result);
+ assertEquals("John Doe", result.get("{http://www.alfresco.org/model/content/1.0}author"));
+ assertEquals("2024-04-16T07:39:22Z", result.get("{http://www.alfresco.org/model/content/1.0}created"));
+ assertNull(result.get("{http://www.alfresco.org/model/content/1.0}title"));
+ }
+
+ @Test
+ void testExtractMetadataWithError()
+ throws URISyntaxException {
+
+ File input = getFile();
+
+ mockAlfrescoTransformCoreResponse("{\"{http://www.alfresco.org/model/content/1.0}title:null}");
+
+ assertThrows(MetadataExtractionException.class, () -> metadataExtractor.extractMetadata(input));
+ }
+
+ private static File getFile()
+ throws URISyntaxException {
+
+ ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
+ URL resource = classLoader.getResource("sample.jpeg");
+ assert resource != null;
+ return new File(resource.toURI());
+ }
+}
diff --git a/spring-content-metadata-extraction/src/test/java/org/springframework/content/metadataextraction/extractors/DefaultMetadataExtractorTest.java b/spring-content-metadata-extraction/src/test/java/org/springframework/content/metadataextraction/extractors/DefaultMetadataExtractorTest.java
new file mode 100644
index 000000000..74a75efe9
--- /dev/null
+++ b/spring-content-metadata-extraction/src/test/java/org/springframework/content/metadataextraction/extractors/DefaultMetadataExtractorTest.java
@@ -0,0 +1,92 @@
+/* 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.springframework.content.metadataextraction.extractors;
+
+import org.junit.Test;
+import org.springframework.content.metadataextraction.MetadataExtractionException;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.Map;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Unit test class for the {@link DefaultMetadataExtractor}.
+ *
+ * This class contains test cases to validate the functionality of the
+ * {@link DefaultMetadataExtractor#extractMetadata(File)} method.
+ *
+ * The following test scenarios are covered:
+ *
+ * - Test the extraction of metadata from a valid file.
+ * - Test the behavior when a null file is provided.
+ * - Test the handling of {@link IOException} during metadata extraction.
+ *
+ *
+ * @author marcobelligoli
+ */
+public class DefaultMetadataExtractorTest {
+
+ private final DefaultMetadataExtractor metadataExtractor = new DefaultMetadataExtractor();
+
+ @Test
+ public void testExtractMetadataWithValidFile()
+ throws URISyntaxException {
+
+ File input = getFile();
+
+ var result = metadataExtractor.extractMetadata(input);
+
+ assertNotNull(result);
+ assertNotNull(result.get("fileName"));
+ assertNotNull(result.get("lastModifiedTime"));
+ assertNotNull(result.get("lastAccessTime"));
+ assertNotNull(result.get("size"));
+ assertNotNull(result.get("mimeType"));
+ assertNotNull(result.get("creationTime"));
+ assertNotNull(result.get("fileExtension"));
+ }
+
+ @Test
+ public void testExtractMetadataWithNullFile() {
+
+ Map metadata = metadataExtractor.extractMetadata(null);
+ assertTrue(metadata.isEmpty());
+ }
+
+ @Test
+ public void testExtractMetadataWithIOException() {
+
+ File nonExistentFile = new File("/path/to/nonexistent/file");
+
+ assertThrows(MetadataExtractionException.class, () -> metadataExtractor.extractMetadata(nonExistentFile));
+ }
+
+ private static File getFile()
+ throws URISyntaxException {
+
+ ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
+ URL resource = classLoader.getResource("sample.jpeg");
+ assert resource != null;
+ return new File(resource.toURI());
+ }
+}
diff --git a/spring-content-metadata-extraction/src/test/resources/sample.jpeg b/spring-content-metadata-extraction/src/test/resources/sample.jpeg
new file mode 100644
index 000000000..f3915b65e
Binary files /dev/null and b/spring-content-metadata-extraction/src/test/resources/sample.jpeg differ