Add api to overwrite content type resolver (#2051)

Introduce content type resolver extension to provide
custom content types for specific file extensions.
This commit is contained in:
Eduard Heimbuch
2022-06-07 11:02:56 +02:00
committed by GitHub
parent 4f83670824
commit 084fe9e2ae
7 changed files with 86 additions and 6 deletions

View File

@@ -0,0 +1,2 @@
- type: added
description: Add api to overwrite content type resolver ([#2051](https://github.com/scm-manager/scm-manager/pull/2051))

View File

@@ -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<String> resolve(String path, byte[] contentPrefix);
}

View File

@@ -81,7 +81,7 @@ const SourcesView: FC<Props> = ({ file, repository, revision }) => {
file,
contentType,
revision,
basePath
basePath,
}}
>
<DownloadViewer repository={repository} file={file} />

View File

@@ -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<ContentTypeResolverExtension> 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<ContentTypeResolverExtension> resolverExtensions) {
this.resolverExtensions = resolverExtensions;
}
@Override
public DefaultContentType resolve(String path) {
return new DefaultContentType(PATH_BASED.detect(path));
Optional<String> 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<String> 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<String> resolveContentTypeFromExtensions(String path, byte[] contentPrefix) {
return resolverExtensions.stream()
.map(r -> r.resolve(path, contentPrefix))
.filter(Optional::isPresent)
.map(Optional::get)
.findFirst();
}
}

View File

@@ -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);

View File

@@ -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() {

View File

@@ -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 {