From e492a30eea3a48d7869eaa3c827408edc8685ac9 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 3 Aug 2021 10:41:38 +0200 Subject: [PATCH] Expose content type resolver api to plugins (#1752) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expose an api which makes it easy to detect the content type of files. The api is based on the spotter api, but does not expose spotter classes. Co-authored-by: René Pfeuffer --- .../changelog/content_type_resolver_api.yaml | 2 + .../main/java/sonia/scm/io/ContentType.java | 71 ++++++++++ .../sonia/scm/io/ContentTypeResolver.java | 52 +++++++ .../scm/api/v2/resources/ContentResource.java | 12 +- .../DiffResultToDiffResultDtoMapper.java | 11 +- .../v2/resources/ProgrammingLanguages.java | 17 +-- .../sonia/scm/io/DefaultContentType.java} | 49 ++++--- .../DefaultContentTypeResolver.java} | 18 ++- .../lifecycle/modules/ScmServletModule.java | 4 + .../v2/ContentSearchableTypeResolverTest.java | 65 --------- .../api/v2/resources/ContentResourceTest.java | 9 +- .../DiffResultToDiffResultDtoMapperTest.java | 7 +- .../io/DefaultContentTypeResolverTest.java | 127 ++++++++++++++++++ 13 files changed, 320 insertions(+), 124 deletions(-) create mode 100644 gradle/changelog/content_type_resolver_api.yaml create mode 100644 scm-core/src/main/java/sonia/scm/io/ContentType.java create mode 100644 scm-core/src/main/java/sonia/scm/io/ContentTypeResolver.java rename scm-webapp/src/{test/java/sonia/scm/api/v2/resources/ProgrammingLanguagesTest.java => main/java/sonia/scm/io/DefaultContentType.java} (55%) rename scm-webapp/src/main/java/sonia/scm/{api/v2/ContentTypeResolver.java => io/DefaultContentTypeResolver.java} (80%) delete mode 100644 scm-webapp/src/test/java/sonia/scm/api/v2/ContentSearchableTypeResolverTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/io/DefaultContentTypeResolverTest.java diff --git a/gradle/changelog/content_type_resolver_api.yaml b/gradle/changelog/content_type_resolver_api.yaml new file mode 100644 index 0000000000..1bce680605 --- /dev/null +++ b/gradle/changelog/content_type_resolver_api.yaml @@ -0,0 +1,2 @@ +- type: Changed + description: Expose content type resolver api to plugins ([#1752](https://github.com/scm-manager/scm-manager/pull/1752)) diff --git a/scm-core/src/main/java/sonia/scm/io/ContentType.java b/scm-core/src/main/java/sonia/scm/io/ContentType.java new file mode 100644 index 0000000000..db788c9f34 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/io/ContentType.java @@ -0,0 +1,71 @@ +/* + * 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 java.util.Optional; + +/** + * Detected type of content. + * + * @since 2.23.0 + */ +public interface ContentType { + + /** + * Returns the primary part of the content type (e.g.: text of text/plain). + * + * @return primary content type part + */ + String getPrimary(); + + /** + * Returns the secondary part of the content type (e.g.: plain of text/plain). + * + * @return secondary content type part + */ + String getSecondary(); + + /** + * Returns the raw presentation of the content type (e.g.: text/plain). + * + * @return raw presentation + */ + String getRaw(); + + /** + * Returns {@code true} if the content type is text based. + * + * @return {@code true} for text content + */ + boolean isText(); + + /** + * Returns an optional with the programming language + * or empty if the content is not programming language. + * + * @return programming language or empty + */ + Optional getLanguage(); +} diff --git a/scm-core/src/main/java/sonia/scm/io/ContentTypeResolver.java b/scm-core/src/main/java/sonia/scm/io/ContentTypeResolver.java new file mode 100644 index 0000000000..347d0a1ae1 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/io/ContentTypeResolver.java @@ -0,0 +1,52 @@ +/* + * 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; + +/** + * ContentTypeResolver is able to detect the {@link ContentType} of files based on their path and (optinally) a few starting bytes. These files do not have to be real files on the file system, but can be hypothetical constructs ("What content type is most probable for a file named like this"). + * + * @since 2.23.0 + */ +public interface ContentTypeResolver { + + /** + * Detects the {@link ContentType} of the given path, by only using path based strategies. + * + * @param path path of the file + * + * @return {@link ContentType} of path + */ + ContentType resolve(String path); + + /** + * Detects the {@link ContentType} of the given path, by using path and content based strategies. + * + * @param path path of the file + * @param contentPrefix first few bytes of the content + * + * @return {@link ContentType} of path and content prefix + */ + ContentType resolve(String path, byte[] contentPrefix); +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java index b58be79e51..2a6e1dd2c5 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ContentResource.java @@ -24,7 +24,6 @@ package sonia.scm.api.v2.resources; -import com.github.sdorra.spotter.ContentType; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; @@ -33,7 +32,8 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.NotFoundException; -import sonia.scm.api.v2.ContentTypeResolver; +import sonia.scm.io.ContentType; +import sonia.scm.io.ContentTypeResolver; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; @@ -61,10 +61,12 @@ public class ContentResource { private static final int HEAD_BUFFER_SIZE = 1024; private final RepositoryServiceFactory serviceFactory; + private final ContentTypeResolver contentTypeResolver; @Inject - public ContentResource(RepositoryServiceFactory serviceFactory) { + public ContentResource(RepositoryServiceFactory serviceFactory, ContentTypeResolver contentTypeResolver) { this.serviceFactory = serviceFactory; + this.contentTypeResolver = contentTypeResolver; } /** @@ -204,10 +206,10 @@ public class ContentResource { } private void appendContentHeader(String path, byte[] head, Response.ResponseBuilder responseBuilder) { - ContentType contentType = ContentTypeResolver.resolve(path, head); + ContentType contentType = contentTypeResolver.resolve(path, head); responseBuilder.header("Content-Type", contentType.getRaw()); contentType.getLanguage().ifPresent( - language -> responseBuilder.header(ProgrammingLanguages.HEADER, ProgrammingLanguages.getValue(language)) + language -> responseBuilder.header(ProgrammingLanguages.HEADER, language) ); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffResultToDiffResultDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffResultToDiffResultDtoMapper.java index 883ba8f398..0c916cf601 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffResultToDiffResultDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/DiffResultToDiffResultDtoMapper.java @@ -24,10 +24,9 @@ package sonia.scm.api.v2.resources; -import com.github.sdorra.spotter.Language; import com.google.inject.Inject; import de.otto.edison.hal.Links; -import sonia.scm.api.v2.ContentTypeResolver; +import sonia.scm.io.ContentTypeResolver; import sonia.scm.repository.Repository; import sonia.scm.repository.api.DiffFile; import sonia.scm.repository.api.DiffLine; @@ -49,10 +48,12 @@ import static de.otto.edison.hal.Links.linkingTo; class DiffResultToDiffResultDtoMapper { private final ResourceLinks resourceLinks; + private final ContentTypeResolver contentTypeResolver; @Inject - DiffResultToDiffResultDtoMapper(ResourceLinks resourceLinks) { + DiffResultToDiffResultDtoMapper(ResourceLinks resourceLinks, ContentTypeResolver contentTypeResolver) { this.resourceLinks = resourceLinks; + this.contentTypeResolver = contentTypeResolver; } public DiffResultDto mapForIncoming(Repository repository, DiffResult result, String source, String target) { @@ -154,8 +155,8 @@ class DiffResultToDiffResultDtoMapper { dto.setOldPath(oldPath); dto.setOldRevision(file.getOldRevision()); - Optional language = ContentTypeResolver.resolve(path).getLanguage(); - language.ifPresent(value -> dto.setLanguage(ProgrammingLanguages.getValue(value))); + Optional language = contentTypeResolver.resolve(path).getLanguage(); + language.ifPresent(dto::setLanguage); List hunks = new ArrayList<>(); for (Hunk hunk : file) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ProgrammingLanguages.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ProgrammingLanguages.java index df34cf575f..e6d88bd216 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ProgrammingLanguages.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ProgrammingLanguages.java @@ -21,28 +21,13 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.api.v2.resources; -import com.github.sdorra.spotter.Language; - -import java.util.Optional; - final class ProgrammingLanguages { static final String HEADER = "X-Programming-Language"; - private static final String DEFAULT = "text"; - private ProgrammingLanguages() { } - - static String getValue(Language language) { - Optional aceMode = language.getAceMode(); - if (!aceMode.isPresent()) { - Optional codemirrorMode = language.getCodemirrorMode(); - return codemirrorMode.orElse(DEFAULT); - } - return aceMode.get(); - } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ProgrammingLanguagesTest.java b/scm-webapp/src/main/java/sonia/scm/io/DefaultContentType.java similarity index 55% rename from scm-webapp/src/test/java/sonia/scm/api/v2/resources/ProgrammingLanguagesTest.java rename to scm-webapp/src/main/java/sonia/scm/io/DefaultContentType.java index 7ae66cd3c2..19efaa42d0 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ProgrammingLanguagesTest.java +++ b/scm-webapp/src/main/java/sonia/scm/io/DefaultContentType.java @@ -21,29 +21,46 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - -package sonia.scm.api.v2.resources; -import com.github.sdorra.spotter.Language; -import org.junit.jupiter.api.Test; +package sonia.scm.io; -import static org.assertj.core.api.Assertions.assertThat; +import java.util.Optional; -class ProgrammingLanguagesTest { +public class DefaultContentType implements ContentType { - @Test - void shouldReturnAceModeIfPresent() { - assertThat(ProgrammingLanguages.getValue(Language.GO)).isEqualTo("golang"); - assertThat(ProgrammingLanguages.getValue(Language.JAVA)).isEqualTo("java"); + private static final String DEFAULT_LANG_MODE = "text"; + + private final com.github.sdorra.spotter.ContentType contentType; + + DefaultContentType(com.github.sdorra.spotter.ContentType contentType) { + this.contentType = contentType; } - @Test - void shouldReturnCodemirrorIfAceModeIsMissing() { - assertThat(ProgrammingLanguages.getValue(Language.HTML_ECR)).isEqualTo("htmlmixed"); + @Override + public String getPrimary() { + return contentType.getPrimary(); } - @Test - void shouldReturnTextIfNoModeIsPresent() { - assertThat(ProgrammingLanguages.getValue(Language.HXML)).isEqualTo("text"); + @Override + public String getSecondary() { + return contentType.getSecondary(); + } + + @Override + public String getRaw() { + return contentType.getRaw(); + } + + @Override + public boolean isText() { + return contentType.isText(); + } + + @Override + public Optional getLanguage() { + return contentType.getLanguage().map(language -> { + Optional aceMode = language.getAceMode(); + return aceMode.orElseGet(() -> language.getCodemirrorMode().orElse(DEFAULT_LANG_MODE)); + }); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/ContentTypeResolver.java b/scm-webapp/src/main/java/sonia/scm/io/DefaultContentTypeResolver.java similarity index 80% rename from scm-webapp/src/main/java/sonia/scm/api/v2/ContentTypeResolver.java rename to scm-webapp/src/main/java/sonia/scm/io/DefaultContentTypeResolver.java index ff993689d0..7b354b6fb2 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/ContentTypeResolver.java +++ b/scm-webapp/src/main/java/sonia/scm/io/DefaultContentTypeResolver.java @@ -22,13 +22,12 @@ * SOFTWARE. */ -package sonia.scm.api.v2; +package sonia.scm.io; -import com.github.sdorra.spotter.ContentType; import com.github.sdorra.spotter.ContentTypeDetector; import com.github.sdorra.spotter.Language; -public final class ContentTypeResolver { +public final class DefaultContentTypeResolver implements ContentTypeResolver { private static final ContentTypeDetector PATH_BASED = ContentTypeDetector.builder() .defaultPathBased().boost(Language.MARKDOWN) @@ -38,14 +37,13 @@ public final class ContentTypeResolver { .defaultPathAndContentBased().boost(Language.MARKDOWN) .bestEffortMatch(); - private ContentTypeResolver() { + @Override + public DefaultContentType resolve(String path) { + return new DefaultContentType(PATH_BASED.detect(path)); } - public static ContentType resolve(String path) { - return PATH_BASED.detect(path); - } - - public static ContentType resolve(String path, byte[] contentPrefix) { - return PATH_AND_CONTENT_BASED.detect(path, contentPrefix); + @Override + public DefaultContentType resolve(String path, byte[] contentPrefix) { + return new DefaultContentType(PATH_AND_CONTENT_BASED.detect(path, contentPrefix)); } } diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ScmServletModule.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ScmServletModule.java index baf3923236..13610604e0 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ScmServletModule.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/ScmServletModule.java @@ -59,6 +59,8 @@ import sonia.scm.group.GroupManagerProvider; import sonia.scm.group.xml.XmlGroupDAO; import sonia.scm.initialization.DefaultInitializationFinisher; import sonia.scm.initialization.InitializationFinisher; +import sonia.scm.io.ContentTypeResolver; +import sonia.scm.io.DefaultContentTypeResolver; import sonia.scm.metrics.MeterRegistryProvider; import sonia.scm.migration.MigrationDAO; import sonia.scm.net.SSLContextProvider; @@ -290,6 +292,8 @@ class ScmServletModule extends ServletModule { bind(IndexQueue.class, DefaultIndexQueue.class); bind(SearchEngine.class, LuceneSearchEngine.class); bind(IndexLogStore.class, DefaultIndexLogStore.class); + + bind(ContentTypeResolver.class).to(DefaultContentTypeResolver.class); } private void bind(Class clazz, Class defaultImplementation) { diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/ContentSearchableTypeResolverTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/ContentSearchableTypeResolverTest.java deleted file mode 100644 index 075ad44a4b..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/ContentSearchableTypeResolverTest.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * 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.api.v2; - -import com.github.sdorra.spotter.ContentType; -import com.github.sdorra.spotter.Language; -import org.junit.jupiter.api.Test; - -import java.nio.charset.StandardCharsets; - -import static org.assertj.core.api.Assertions.assertThat; - -class ContentSearchableTypeResolverTest { - - @Test - void shouldResolveMarkdown() { - String content = String.join("\n", - "% Markdown content", - "% Which does not start with markdown" - ); - ContentType contentType = ContentTypeResolver.resolve("somedoc.md", content.getBytes(StandardCharsets.UTF_8)); - assertThat(contentType.getLanguage()).contains(Language.MARKDOWN); - } - - @Test - void shouldResolveMarkdownWithoutContent() { - ContentType contentType = ContentTypeResolver.resolve("somedoc.md"); - assertThat(contentType.getLanguage()).contains(Language.MARKDOWN); - } - - @Test - void shouldResolveMarkdownEvenWithDotsInFilename() { - ContentType contentType = ContentTypeResolver.resolve("somedoc.1.1.md"); - assertThat(contentType.getLanguage()).contains(Language.MARKDOWN); - } - - @Test - void shouldResolveDockerfile() { - ContentType contentType = ContentTypeResolver.resolve("Dockerfile"); - assertThat(contentType.getLanguage()).contains(Language.DOCKERFILE); - } - -} 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 7cb067a965..d7a72c10cc 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 @@ -29,10 +29,10 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Answers; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.NotFoundException; +import sonia.scm.io.DefaultContentTypeResolver; import sonia.scm.repository.NamespaceAndName; import sonia.scm.repository.api.CatCommandBuilder; import sonia.scm.repository.api.RepositoryService; @@ -68,13 +68,14 @@ public class ContentResourceTest { @Mock(answer = Answers.RETURNS_DEEP_STUBS) private RepositoryServiceFactory repositoryServiceFactory; - @InjectMocks private ContentResource contentResource; private CatCommandBuilder catCommand; @Before public void initService() throws Exception { + contentResource = new ContentResource(repositoryServiceFactory, new DefaultContentTypeResolver()); + NamespaceAndName existingNamespaceAndName = new NamespaceAndName(NAMESPACE, REPO_NAME); RepositoryService repositoryService = repositoryServiceFactory.create(existingNamespaceAndName); catCommand = repositoryService.getCatCommand(); @@ -169,7 +170,7 @@ public class ContentResourceTest { @Test public void shouldNotReadCompleteFileForHead() throws Exception { FailingAfterSomeBytesStream stream = new FailingAfterSomeBytesStream(); - doAnswer(invocation -> stream).when(catCommand).getStream(eq("readHeadOnly")); + doAnswer(invocation -> stream).when(catCommand).getStream("readHeadOnly"); Response response = contentResource.metadata(NAMESPACE, REPO_NAME, REV, "readHeadOnly"); assertEquals(200, response.getStatus()); @@ -201,7 +202,7 @@ public class ContentResourceTest { outputStream.close(); return null; }).when(catCommand).retriveContent(any(), eq(path)); - doAnswer(invocation -> new ByteArrayInputStream(content)).when(catCommand).getStream(eq(path)); + doAnswer(invocation -> new ByteArrayInputStream(content)).when(catCommand).getStream(path); } private ByteArrayOutputStream readOutputStream(Response response) throws IOException { 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 65a931f2c1..f3171f5f90 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 @@ -28,6 +28,7 @@ import de.otto.edison.hal.Link; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.io.DefaultContentTypeResolver; import sonia.scm.repository.Repository; import sonia.scm.repository.api.DiffFile; import sonia.scm.repository.api.DiffLine; @@ -52,7 +53,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); + DiffResultToDiffResultDtoMapper mapper = new DiffResultToDiffResultDtoMapper(resourceLinks, new DefaultContentTypeResolver()); @Test void shouldMapDiffResult() { @@ -62,8 +63,8 @@ class DiffResultToDiffResultDtoMapperTest { assertAddedFile(files.get(0), "A.java", "abc", "java"); assertModifiedFile(files.get(1), "B.ts", "abc", "def", "typescript"); assertDeletedFile(files.get(2), "C.go", "ghi", "golang"); - assertRenamedFile(files.get(3), "typo.ts", "okay.ts", "def", "fixed", "typescript"); - assertCopiedFile(files.get(4), "good.ts", "better.ts", "def", "fixed", "typescript"); + assertRenamedFile(files.get(3), "typo.ts", "okay.ts", "def", "fixed", "typescript"); + assertCopiedFile(files.get(4), "good.ts", "better.ts", "def", "fixed", "typescript"); DiffResultDto.HunkDto hunk = files.get(1).getHunks().get(0); assertHunk(hunk, "@@ -3,4 1,2 @@", 1, 2, 3, 4); diff --git a/scm-webapp/src/test/java/sonia/scm/io/DefaultContentTypeResolverTest.java b/scm-webapp/src/test/java/sonia/scm/io/DefaultContentTypeResolverTest.java new file mode 100644 index 0000000000..a2af3f1d8f --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/io/DefaultContentTypeResolverTest.java @@ -0,0 +1,127 @@ +/* + * 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 org.assertj.core.api.Assertions; +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 static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class DefaultContentTypeResolverTest { + + private final DefaultContentTypeResolver contentTypeResolver = new DefaultContentTypeResolver(); + + @Test + void shouldReturnPrimaryPart() { + ContentType contentType = contentTypeResolver.resolve("hog.pdf"); + assertThat(contentType.getPrimary()).isEqualTo("application"); + } + + @Test + void shouldReturnSecondaryPart() { + ContentType contentType = contentTypeResolver.resolve("hog.pdf"); + assertThat(contentType.getSecondary()).isEqualTo("pdf"); + } + + @Test + void shouldReturnRaw() { + ContentType contentType = contentTypeResolver.resolve("hog.pdf"); + assertThat(contentType.getRaw()).isEqualTo("application/pdf"); + } + + @Nested + class IsTextTests { + + @ParameterizedTest(name = "shouldReturnIsTextFor: {argumentsWithNames}") + @ValueSource(strings = {"App.java", "Dockerfile", "Playbook.yml", "README.md", "LICENSE.txt"}) + void shouldReturnIsTextFor(String path) { + ContentType contentType = contentTypeResolver.resolve(path); + assertThat(contentType.isText()).isTrue(); + } + + @ParameterizedTest(name = "shouldReturnIsNotTextFor: {argumentsWithNames}") + @ValueSource(strings = {"scan.exe", "hog.pdf", "library.so", "awesome.dll", "something.dylib"}) + void shouldReturnIsNotTextFor(String path) { + ContentType contentType = contentTypeResolver.resolve(path); + assertThat(contentType.isText()).isFalse(); + } + } + + @Nested + class LanguageTests { + + @Test + void shouldResolveMarkdown() { + String content = String.join("\n", + "% Markdown content", + "% Which does not start with markdown" + ); + ContentType contentType = contentTypeResolver.resolve("somedoc.md", content.getBytes(StandardCharsets.UTF_8)); + Assertions.assertThat(contentType.getLanguage()).contains("markdown"); + } + + @Test + void shouldResolveMarkdownWithoutContent() { + ContentType contentType = contentTypeResolver.resolve("somedoc.md"); + Assertions.assertThat(contentType.getLanguage()).contains("markdown"); + } + + @Test + void shouldResolveMarkdownEvenWithDotsInFilename() { + ContentType contentType = contentTypeResolver.resolve("somedoc.1.1.md"); + Assertions.assertThat(contentType.getLanguage()).contains("markdown"); + } + + @Test + void shouldResolveDockerfile() { + ContentType contentType = contentTypeResolver.resolve("Dockerfile"); + Assertions.assertThat(contentType.getLanguage()).contains("dockerfile"); + } + + + @Test + void shouldReturnAceModeIfPresent() { + assertThat(contentTypeResolver.resolve("app.go").getLanguage()).contains("golang"); // codemirror is just go + assertThat(contentTypeResolver.resolve("App.java").getLanguage()).contains("java"); // codemirror is clike + } + + @Test + void shouldReturnCodemirrorIfAceModeIsMissing() { + assertThat(contentTypeResolver.resolve("index.ecr").getLanguage()).contains("htmlmixed"); + } + + @Test + void shouldReturnTextIfNoModeIsPresent() { + assertThat(contentTypeResolver.resolve("index.hxml").getLanguage()).contains("text"); + } + + } + +}