diff --git a/gradle/changelog/contentTypeResolverExtension.yaml b/gradle/changelog/contentTypeResolverExtension.yaml new file mode 100644 index 0000000000..2e2dbec4ee --- /dev/null +++ b/gradle/changelog/contentTypeResolverExtension.yaml @@ -0,0 +1,2 @@ +- type: added + description: Add api to overwrite content type resolver ([#2051](https://github.com/scm-manager/scm-manager/pull/2051)) diff --git a/scm-core/src/main/java/sonia/scm/io/ContentTypeResolverExtension.java b/scm-core/src/main/java/sonia/scm/io/ContentTypeResolverExtension.java new file mode 100644 index 0000000000..0562951315 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/io/ContentTypeResolverExtension.java @@ -0,0 +1,41 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.io; + +import sonia.scm.plugin.ExtensionPoint; + +import java.util.Optional; + +/** + * ContentTypeResolverExtension extends the default {@link ContentTypeResolver} with custom resolve actions. + * This can be used by plugins which want to change the content type of specific file extensions. + * + * @since 2.36.0 + */ +@ExtensionPoint +public interface ContentTypeResolverExtension { + + Optional resolve(String path, byte[] contentPrefix); +} diff --git a/scm-ui/ui-webapp/src/repos/sources/containers/SourcesView.tsx b/scm-ui/ui-webapp/src/repos/sources/containers/SourcesView.tsx index cecde05b68..bd9e103da0 100644 --- a/scm-ui/ui-webapp/src/repos/sources/containers/SourcesView.tsx +++ b/scm-ui/ui-webapp/src/repos/sources/containers/SourcesView.tsx @@ -81,7 +81,7 @@ const SourcesView: FC = ({ file, repository, revision }) => { file, contentType, revision, - basePath + basePath, }} > diff --git a/scm-webapp/src/main/java/sonia/scm/io/DefaultContentTypeResolver.java b/scm-webapp/src/main/java/sonia/scm/io/DefaultContentTypeResolver.java index 033232c3b7..c7831f8087 100644 --- a/scm-webapp/src/main/java/sonia/scm/io/DefaultContentTypeResolver.java +++ b/scm-webapp/src/main/java/sonia/scm/io/DefaultContentTypeResolver.java @@ -24,15 +24,20 @@ package sonia.scm.io; +import com.cloudogu.spotter.ContentType; import com.cloudogu.spotter.ContentTypeDetector; import com.cloudogu.spotter.Language; +import javax.inject.Inject; import java.util.Collections; import java.util.Map; import java.util.Optional; +import java.util.Set; public final class DefaultContentTypeResolver implements ContentTypeResolver { + private final Set resolverExtensions; + private static final Language[] BOOST = new Language[]{ // GCC Machine Description uses .md as extension, but markdown is much more likely Language.MARKDOWN, @@ -52,14 +57,25 @@ public final class DefaultContentTypeResolver implements ContentTypeResolver { .boost(BOOST) .bestEffortMatch(); + @Inject + public DefaultContentTypeResolver(Set resolverExtensions) { + this.resolverExtensions = resolverExtensions; + } + @Override public DefaultContentType resolve(String path) { - return new DefaultContentType(PATH_BASED.detect(path)); + Optional extensionContentType = resolveContentTypeFromExtensions(path, new byte[]{}); + return extensionContentType + .map(rawContentType -> new DefaultContentType(new ContentType(rawContentType))) + .orElseGet(() -> new DefaultContentType(PATH_BASED.detect(path))); } @Override public DefaultContentType resolve(String path, byte[] contentPrefix) { - return new DefaultContentType(PATH_AND_CONTENT_BASED.detect(path, contentPrefix)); + Optional extensionContentType = resolveContentTypeFromExtensions(path, contentPrefix); + return extensionContentType + .map(rawContentType -> new DefaultContentType(new ContentType(rawContentType))) + .orElseGet(() -> new DefaultContentType(PATH_AND_CONTENT_BASED.detect(path, contentPrefix))); } @Override @@ -70,4 +86,12 @@ public final class DefaultContentTypeResolver implements ContentTypeResolver { } return Collections.emptyMap(); } + + private Optional resolveContentTypeFromExtensions(String path, byte[] contentPrefix) { + return resolverExtensions.stream() + .map(r -> r.resolve(path, contentPrefix)) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst(); + } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ContentResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ContentResourceTest.java index 0380027c12..1c56fdf8e8 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ContentResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ContentResourceTest.java @@ -46,6 +46,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URL; +import java.util.Collections; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -74,7 +75,7 @@ public class ContentResourceTest { @Before public void initService() throws Exception { - contentResource = new ContentResource(repositoryServiceFactory, new DefaultContentTypeResolver()); + contentResource = new ContentResource(repositoryServiceFactory, new DefaultContentTypeResolver(Collections.emptySet())); NamespaceAndName existingNamespaceAndName = new NamespaceAndName(NAMESPACE, REPO_NAME); RepositoryService repositoryService = repositoryServiceFactory.create(existingNamespaceAndName); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResultToDiffResultDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResultToDiffResultDtoMapperTest.java index 66ae5a9459..4098c01e2a 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResultToDiffResultDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResultToDiffResultDtoMapperTest.java @@ -36,6 +36,7 @@ import sonia.scm.repository.api.DiffResult; import sonia.scm.repository.api.Hunk; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.OptionalInt; @@ -53,7 +54,7 @@ class DiffResultToDiffResultDtoMapperTest { private static final Repository REPOSITORY = new Repository("1", "git", "space", "X"); ResourceLinks resourceLinks = ResourceLinksMock.createMock(create("/scm/api/v2")); - DiffResultToDiffResultDtoMapper mapper = new DiffResultToDiffResultDtoMapper(resourceLinks, new DefaultContentTypeResolver()); + DiffResultToDiffResultDtoMapper mapper = new DiffResultToDiffResultDtoMapper(resourceLinks, new DefaultContentTypeResolver(Collections.emptySet())); @Test void shouldMapDiffResult() { diff --git a/scm-webapp/src/test/java/sonia/scm/io/DefaultContentTypeResolverTest.java b/scm-webapp/src/test/java/sonia/scm/io/DefaultContentTypeResolverTest.java index 5ae03229a4..1060ffd764 100644 --- a/scm-webapp/src/test/java/sonia/scm/io/DefaultContentTypeResolverTest.java +++ b/scm-webapp/src/test/java/sonia/scm/io/DefaultContentTypeResolverTest.java @@ -24,19 +24,22 @@ package sonia.scm.io; +import com.google.common.collect.ImmutableSet; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import java.nio.charset.StandardCharsets; +import java.util.Collections; import java.util.Map; +import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; class DefaultContentTypeResolverTest { - private final DefaultContentTypeResolver contentTypeResolver = new DefaultContentTypeResolver(); + private final DefaultContentTypeResolver contentTypeResolver = new DefaultContentTypeResolver(Collections.emptySet()); @Test void shouldReturnPrimaryPart() { @@ -56,6 +59,14 @@ class DefaultContentTypeResolverTest { assertThat(contentType.getRaw()).isEqualTo("application/pdf"); } + @Test + void shouldReturnContentTypeFromExtension() { + DefaultContentTypeResolver contentTypeResolver = new DefaultContentTypeResolver(ImmutableSet.of((path, contentPrefix) -> Optional.of("scm/test"))); + + ContentType contentType = contentTypeResolver.resolve("hog.pdf"); + assertThat(contentType.getRaw()).isEqualTo("scm/test"); + } + @Nested class IsTextTests {