From e840d16ce5b5b1bf1acbcc3d9dc2882f2ea731b7 Mon Sep 17 00:00:00 2001 From: Marius Brill <53812669+mbrill-nt@users.noreply.github.com> Date: Wed, 12 Aug 2020 16:24:55 +0200 Subject: [PATCH] Closes #560 - Added endpoint to configuration server to find files containing specific text (#659) Co-authored-by: Marius Oehler --- .../util/ConfigurationFilesCache.java | 8 +- .../util/ConfigurationQueryHelper.java | 10 +- .../ocelot/rest/file/FileController.java | 43 +++-- .../search/FileContentSearchEngine.java | 180 ++++++++++++++++++ .../inspectit/ocelot/search/SearchResult.java | 40 ++++ .../util/ConfigurationQueryHelperTest.java | 57 +++--- .../search/FileContentSearchEngineTest.java | 131 +++++++++++++ 7 files changed, 413 insertions(+), 56 deletions(-) create mode 100644 components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/search/FileContentSearchEngine.java create mode 100644 components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/search/SearchResult.java create mode 100644 components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/search/FileContentSearchEngineTest.java diff --git a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/autocomplete/util/ConfigurationFilesCache.java b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/autocomplete/util/ConfigurationFilesCache.java index 83f901d48f..5918a62be2 100644 --- a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/autocomplete/util/ConfigurationFilesCache.java +++ b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/autocomplete/util/ConfigurationFilesCache.java @@ -34,7 +34,7 @@ public class ConfigurationFilesCache { /** * The current parsed contents of all configuration files. */ - private Collection currentParsedFiles = Collections.emptyList(); + private Collection parsedContents = Collections.emptyList(); /** * Returns the most recently loaded .yaml and .yml files as a list of Objects. Each Object resembles the corresponding @@ -53,8 +53,8 @@ public class ConfigurationFilesCache { * * @return A Collection containing all loaded .yaml and .yml files root elements as Maps or Lists. */ - public Collection getParsedConfigurationFiles() { - return currentParsedFiles; + public Collection getParsedContents() { + return parsedContents; } /** @@ -68,7 +68,7 @@ public synchronized void loadFiles() { if (activeReloadTask != null) { activeReloadTask.cancel(); } - activeReloadTask = new ConfigurationFilesCacheReloadTask(fileAccess, (configs) -> currentParsedFiles = configs); + activeReloadTask = new ConfigurationFilesCacheReloadTask(fileAccess, (configs) -> parsedContents = configs); executor.submit(activeReloadTask); } diff --git a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/autocomplete/util/ConfigurationQueryHelper.java b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/autocomplete/util/ConfigurationQueryHelper.java index 6cad8c01fa..f8ef780bc2 100644 --- a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/autocomplete/util/ConfigurationQueryHelper.java +++ b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/autocomplete/util/ConfigurationQueryHelper.java @@ -23,10 +23,11 @@ public class ConfigurationQueryHelper { * The method returns a list containing "to". * * @param path A path parsed to a List. + * * @return The attributes which could be found in the given path. */ public List getKeysForPath(List path) { - return configurationFilesCache.getParsedConfigurationFiles() + return configurationFilesCache.getParsedContents() .stream() .flatMap(root -> extractKeys(root, path).stream()) .collect(Collectors.toList()); @@ -42,6 +43,7 @@ public List getKeysForPath(List path) { * * @param o The object the keys should be returned from. * @param mapPath The path leading to the keys that should be retrieved. + * * @return */ private List extractKeys(Object o, List mapPath) { @@ -65,6 +67,7 @@ private List extractKeys(Object o, List mapPath) { * Then the list is returned. * * @param collection The collection which should be parsed. + * * @return A List containing all Strings found in the given Collection. */ private List toStringList(Collection collection) { @@ -82,6 +85,7 @@ private List toStringList(Collection collection) { * * @param list The list which contents should be searched. * @param mapPath The path which should be checked. + * * @return a list of strings containing the attributes that could found in the mapPath. */ private List extractKeysFromList(List list, List mapPath) { @@ -120,6 +124,7 @@ private List extractKeysFromList(List list, List mapPath) { * * @param map The list which contents should be searched. * @param mapPath The path which should be checked. + * * @return a list of strings containing the attributes that could found in the mapPath. */ private List extractKeysFromMap(Map map, List mapPath) { @@ -129,7 +134,8 @@ private List extractKeysFromMap(Map map, List mapPath) { String currentLiteral = mapPath.get(0); List subPath = mapPath.subList(1, mapPath.size()); if (currentLiteral.equals("*")) { - return map.values().stream() + return map.values() + .stream() .flatMap(value -> extractKeys(value, subPath).stream()) .collect(Collectors.toList()); } else { diff --git a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/rest/file/FileController.java b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/rest/file/FileController.java index c03d1ccf3a..789c6779ba 100644 --- a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/rest/file/FileController.java +++ b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/rest/file/FileController.java @@ -8,10 +8,13 @@ import org.springframework.web.bind.annotation.*; import rocks.inspectit.ocelot.file.FileData; import rocks.inspectit.ocelot.rest.util.RequestUtil; +import rocks.inspectit.ocelot.search.FileContentSearchEngine; +import rocks.inspectit.ocelot.search.SearchResult; import rocks.inspectit.ocelot.security.config.UserRoleConfiguration; import javax.servlet.http.HttpServletRequest; import java.io.IOException; +import java.util.List; import java.util.Optional; /** @@ -23,20 +26,21 @@ public class FileController extends FileBaseController { @Autowired private ObjectMapper objectMapper; + @Autowired + private FileContentSearchEngine fileContentSearchEngine; + @Secured(UserRoleConfiguration.WRITE_ACCESS_ROLE) @ApiOperation(value = "Write a file", notes = "Creates or overwrites a file with the provided text content") @ApiImplicitParam(name = "Path", type = "string", value = "The part of the url after /files/ defines the path to the file to write.") @PutMapping(value = "files/**") public void writeFile(HttpServletRequest request, - @ApiParam("If true, the request body is not parsed as json and is instead written directly to the result file.") @RequestParam(defaultValue = "false") boolean raw, - @ApiParam(value = "The content to write, either raw or a json", - examples = @Example(value = { - @ExampleProperty(mediaType = "application/json", value = "{ 'content' : 'This is the file content' }"), - @ExampleProperty(mediaType = "text/plain", value = "This is the file content") - }) - ) - - @RequestBody(required = false) String content) throws IOException { + @ApiParam("If true, the request body is not parsed as json and is instead written directly to" + " the result file.") + @RequestParam(defaultValue = "false") + boolean raw, + @ApiParam(value = "The content to write, either raw or a json", examples = @Example(value = {@ExampleProperty(mediaType = "application/json", value = "{ 'content' : 'This is the" + " file content' }"), @ExampleProperty(mediaType = "text/plain", value = "This is the file content")})) + + @RequestBody(required = false) + String content) throws IOException { String path = RequestUtil.getRequestSubPath(request); String fileContent; @@ -52,16 +56,14 @@ public void writeFile(HttpServletRequest request, @ApiOperation(value = "Read a file", notes = "Returns the contents of the given file.") @ApiImplicitParam(name = "Path", type = "string", value = "The part of the url after /files/ defines the path to the file to read.") - @ApiResponse(code = 200, - message = "Ok", - examples = @Example(value = { - @ExampleProperty(mediaType = "application/json", value = "{ 'content' : 'This is the file content' }"), - @ExampleProperty(mediaType = "text/plain", value = "This is the file content") - })) + @ApiResponse(code = 200, message = "Ok", examples = @Example(value = {@ExampleProperty(mediaType = "application/json", value = "{ 'content' : 'This is the file content' }"), + + @ExampleProperty(mediaType = "text/plain", value = "This is the file content")})) @GetMapping(value = "files/**") public Object readFile(HttpServletRequest request, - @ApiParam("If true, the response body is not formatted as json and is instead the plain text content of the file.") - @RequestParam(defaultValue = "false") boolean raw) { + @ApiParam("If true, the response body is not formatted as json and is instead the plain text" + " content of the file.") + @RequestParam(defaultValue = "false") + boolean raw) { String path = RequestUtil.getRequestSubPath(request); Optional contentOptional = fileManager.getWorkingDirectory().readConfigurationFile(path); @@ -87,4 +89,11 @@ public void deleteFile(HttpServletRequest request) throws IOException { fileManager.getWorkingDirectory().deleteConfiguration(path); } + + @ApiOperation(value = "Search the given query in all present files.", notes = "Searches the given query in all present files. " + "Searches for as many matches as defined by the limit parameter. If the the limit is set " + "to -1, the query is searched for all occurrences in all files. All found matches are " + "returned in a list of SearchResult instances. Each of these instances contains the " + "following variables:" + "

" + "file: a String resembling the name of the file the match was found in." + "

" + "startLine: the number of the line in this file where the found match starts as " + "integer." + "

" + "endLine: the number of the line in this file where the found match ends as integer." + "

" + "startColumn: the number of the column where the found found match starts as " + "integer." + "

" + "endColumn: the number of the column where the found match ends as integer.") + @ApiImplicitParams({@ApiImplicitParam(name = "query", value = "The query string that should be searched in the files."), @ApiImplicitParam(name = "limit", value = "The limit for the returned values. Use '-1' for no limit.")}) + @GetMapping(value = "search") + public List searchForContent(@RequestParam String query, @RequestParam(defaultValue = "100") int limit) { + return fileContentSearchEngine.search(query, limit); + } } diff --git a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/search/FileContentSearchEngine.java b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/search/FileContentSearchEngine.java new file mode 100644 index 0000000000..dfe399cccf --- /dev/null +++ b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/search/FileContentSearchEngine.java @@ -0,0 +1,180 @@ +package rocks.inspectit.ocelot.search; + +import lombok.Value; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import rocks.inspectit.ocelot.file.FileInfo; +import rocks.inspectit.ocelot.file.FileManager; +import rocks.inspectit.ocelot.file.accessor.git.RevisionAccess; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Component to search for a specific pattern in the configuration files. + */ +@Component +public class FileContentSearchEngine { + + @Autowired + private FileManager fileManager; + + /** + * Searches in all files in the current Workspace Revision of the server for the given query. The minimum amount of + * returned entries is the amount that could be found, the maximum amount of returned entries is defined by the + * limit parameter. + * Returns a List containing {@link SearchResult} instances. Each of these instances resembles one occurrence of the + * given query. + *

+ * If the given query is an empty String, an empty Map is returned. + * + * @param query The String that is searched for. + * @param limit The maximum amount of entries that should be searched. Set to -1 to get all entries returned. + * + * @return A List containing {@link SearchResult} instances for each found match. + */ + public List search(String query, int limit) { + if (query.isEmpty()) { + return Collections.emptyList(); + } + + return search(query, limit, fileManager.getWorkspaceRevision()); + } + + /** + * Searches in all configuration files which can be accessed by the given {@link RevisionAccess} for the specified + * query string. The amount of results can be limited using the limit argument. + * + * @param query the query string to look for + * @param limit the maximum amount of results + * @param revisionAccess the accessor for fetching the files + * + * @return a list of {@link SearchResult} representing the matches + */ + private List search(String query, int limit, RevisionAccess revisionAccess) { + Pattern queryPattern = Pattern.compile(Pattern.quote(query)); + List files = revisionAccess.listConfigurationFiles(""); + + AtomicInteger limitCounter = new AtomicInteger(limit); + + List result = files.stream() + .map(fileInfo -> fileInfo.getAbsoluteFilePaths("")) + .reduce(Stream.empty(), Stream::concat) + .map(filename -> { + Optional content = revisionAccess.readConfigurationFile(filename); + return content.map(fileContent -> findQuery(filename, fileContent, queryPattern, limitCounter)); + }) + .filter(Optional::isPresent) + .map(Optional::get) + .flatMap(Collection::stream) + .collect(Collectors.toList()); + + return result; + } + + /** + * Searches in the specified content for the specified query pattern. The passed content represents the content of the + * file with the specified name. + * + * @param fileName the filename of the current file + * @param content the file's content + * @param queryPattern the pattern to search for + * @param limitCounter the amount of results to add + * + * @return a list of {@link SearchResult} representing the matches + */ + private List findQuery(String fileName, String content, Pattern queryPattern, AtomicInteger limitCounter) { + if (StringUtils.isEmpty(content)) { + return Collections.emptyList(); + } + + List results = new ArrayList<>(); + + List lines = getLines(content); + + ListIterator listIterator = lines.listIterator(); + Line currentLine = listIterator.next(); + + Matcher matcher = queryPattern.matcher(content); + while (matcher.find() && limitCounter.decrementAndGet() >= 0) { + int start = matcher.start(); + int end = matcher.end(); + + while (start >= currentLine.getEndIndex()) { + currentLine = listIterator.next(); + } + int startLine = currentLine.getLineNumber(); + int relativeStart = start - currentLine.getStartIndex(); + + while (end > currentLine.getEndIndex()) { + currentLine = listIterator.next(); + } + int endLine = currentLine.getLineNumber(); + int relativeEnd = end - currentLine.getStartIndex(); + + SearchResult result = new SearchResult(fileName, startLine, relativeStart, endLine, relativeEnd); + results.add(result); + } + + return results; + } + + /** + * Extracts a list of {@link Line}s of the given content. + * + * @param content the content used as basis + * + * @return list of {@link Line}s representing the content + */ + private List getLines(String content) { + if (StringUtils.isEmpty(content)) { + return Collections.emptyList(); + } + + List result = new LinkedList<>(); + int lineNumber = 0; + int startIndex = 0; + do { + int nextIndex = content.indexOf("\n", startIndex) + 1; + + // in case there are no further line breaks + if (nextIndex == 0) { + nextIndex = content.length(); + } + + Line line = new Line(lineNumber++, startIndex, nextIndex); + result.add(line); + + startIndex = nextIndex; + } while (startIndex < content.length()); + + return result; + } + + /** + * Class for representing a line in a string. + */ + @Value + private class Line { + + /** + * The line number. + */ + int lineNumber; + + /** + * The absolute index where the line is starting. + */ + int startIndex; + + /** + * The absolute end index where the line is ending. + */ + int endIndex; + } +} diff --git a/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/search/SearchResult.java b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/search/SearchResult.java new file mode 100644 index 0000000000..4afa13dc0a --- /dev/null +++ b/components/inspectit-ocelot-configurationserver/src/main/java/rocks/inspectit/ocelot/search/SearchResult.java @@ -0,0 +1,40 @@ +package rocks.inspectit.ocelot.search; + +import lombok.AllArgsConstructor; +import lombok.Data; + +/** + * This class resembles a query matched by the FileContentSearchEngine. The file variable contains the file name the + * query was found in. The start variables provide information on in which line and on which column in this line a + * searched substring was found. The end variable does alike for the end of the query. + */ +@AllArgsConstructor +@Data +public class SearchResult { + + /** + * The name of the file the match was found in. + */ + private String file; + + /** + * The start line of the match. + */ + private int startLine; + + /** + * The start column of the match in the starting line. + */ + private int startColumn; + + /** + * The end line of the match. + */ + private int endLine; + + /** + * The end column of the match in the ending line. + */ + private int endColumn; + +} diff --git a/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/autocomplete/util/ConfigurationQueryHelperTest.java b/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/autocomplete/util/ConfigurationQueryHelperTest.java index 5064ae00b8..c4e46993b6 100644 --- a/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/autocomplete/util/ConfigurationQueryHelperTest.java +++ b/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/autocomplete/util/ConfigurationQueryHelperTest.java @@ -5,21 +5,21 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; import java.util.*; -import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class ConfigurationQueryHelperTest { + @Mock private ConfigurationFilesCache configurationFilesCache; @InjectMocks - private ConfigurationQueryHelper autoCompleter; + private ConfigurationQueryHelper configurationQueryHelper; @Nested public class GetKeysForPath { @@ -27,7 +27,6 @@ public class GetKeysForPath { @Test public void getScopeSuggestion() { List propertyPath = Arrays.asList("inspectit", "instrumentation", "scopes"); - ConfigurationQueryHelper spyAutoCompleter = Mockito.spy(autoCompleter); HashMap topLevelMap = new HashMap<>(); HashMap inspectit = new HashMap<>(); HashMap instrumentation = new HashMap<>(); @@ -38,9 +37,9 @@ public void getScopeSuggestion() { inspectit.put("instrumentation", instrumentation); topLevelMap.put("inspectit", inspectit); Collection mockData = Collections.singletonList(topLevelMap); - when(configurationFilesCache.getParsedConfigurationFiles()).thenReturn(mockData); + when(configurationFilesCache.getParsedContents()).thenReturn(mockData); - List output = spyAutoCompleter.getKeysForPath(propertyPath); + List output = configurationQueryHelper.getKeysForPath(propertyPath); assertThat(output).hasSize(2); assertThat(output).contains("my_scope1"); @@ -50,7 +49,6 @@ public void getScopeSuggestion() { @Test public void getNonScopeSuggestion() { List propertyPath = Arrays.asList("inspectit", "metrics"); - ConfigurationQueryHelper spyAutoCompleter = Mockito.spy(autoCompleter); HashMap topLevelMap = new HashMap<>(); HashMap inspectit = new HashMap<>(); HashMap metrics = new HashMap<>(); @@ -61,9 +59,9 @@ public void getNonScopeSuggestion() { inspectit.put("metrics", metrics); topLevelMap.put("inspectit", inspectit); Collection mockData = Collections.singletonList(topLevelMap); - when(configurationFilesCache.getParsedConfigurationFiles()).thenReturn(mockData); + when(configurationFilesCache.getParsedContents()).thenReturn(mockData); - List output = spyAutoCompleter.getKeysForPath(propertyPath); + List output = configurationQueryHelper.getKeysForPath(propertyPath); assertThat(output).hasSize(1); assertThat(output).contains("definitions"); @@ -72,7 +70,6 @@ public void getNonScopeSuggestion() { @Test public void noScopeDefined() { List propertyPath = Arrays.asList("inspectit", "instrumentation", "scopes"); - ConfigurationQueryHelper spyAutoCompleter = Mockito.spy(autoCompleter); HashMap topLevelMap = new HashMap<>(); HashMap inspectit = new HashMap<>(); HashMap metrics = new HashMap<>(); @@ -83,9 +80,9 @@ public void noScopeDefined() { inspectit.put("metrics", metrics); topLevelMap.put("inspectit", inspectit); Collection mockData = Collections.singletonList(topLevelMap); - when(configurationFilesCache.getParsedConfigurationFiles()).thenReturn(mockData); + when(configurationFilesCache.getParsedContents()).thenReturn(mockData); - List output = spyAutoCompleter.getKeysForPath(propertyPath); + List output = configurationQueryHelper.getKeysForPath(propertyPath); assertThat(output).hasSize(0); } @@ -93,7 +90,6 @@ public void noScopeDefined() { @Test public void withWildCard() { List propertyPath = Arrays.asList("inspectit", "*"); - ConfigurationQueryHelper spyAutoCompleter = Mockito.spy(autoCompleter); HashMap topLevelMap = new HashMap<>(); HashMap inspectit = new HashMap<>(); HashMap instrumentation = new HashMap<>(); @@ -110,9 +106,9 @@ public void withWildCard() { inspectit.put("list", list); topLevelMap.put("inspectit", inspectit); Collection mockData = Collections.singletonList(topLevelMap); - when(configurationFilesCache.getParsedConfigurationFiles()).thenReturn(mockData); + when(configurationFilesCache.getParsedContents()).thenReturn(mockData); - List output = spyAutoCompleter.getKeysForPath(propertyPath); + List output = configurationQueryHelper.getKeysForPath(propertyPath); assertThat(output).hasSize(5); assertThat(output).contains("definitions"); @@ -125,16 +121,15 @@ public void withWildCard() { @Test public void throughList() { List propertyPath = Arrays.asList("inspectit", "exampleList", "0"); - ConfigurationQueryHelper spyAutoCompleter = Mockito.spy(autoCompleter); HashMap topLevelMap = new HashMap<>(); HashMap inspectit = new HashMap<>(); List list = Collections.singletonList("Hello!"); inspectit.put("exampleList", list); topLevelMap.put("inspectit", inspectit); Collection mockData = Collections.singletonList(topLevelMap); - when(configurationFilesCache.getParsedConfigurationFiles()).thenReturn(mockData); + when(configurationFilesCache.getParsedContents()).thenReturn(mockData); - List output = spyAutoCompleter.getKeysForPath(propertyPath); + List output = configurationQueryHelper.getKeysForPath(propertyPath); assertThat(output).hasSize(1); assertThat(output).contains("Hello!"); @@ -143,16 +138,15 @@ public void throughList() { @Test public void wildcardThroughList() { List propertyPath = Arrays.asList("inspectit", "exampleList", "*"); - ConfigurationQueryHelper spyAutoCompleter = Mockito.spy(autoCompleter); HashMap topLevelMap = new HashMap<>(); HashMap inspectit = new HashMap<>(); List list = Arrays.asList("Hello", "there!"); inspectit.put("exampleList", list); topLevelMap.put("inspectit", inspectit); Collection mockData = Collections.singletonList(topLevelMap); - when(configurationFilesCache.getParsedConfigurationFiles()).thenReturn(mockData); + when(configurationFilesCache.getParsedContents()).thenReturn(mockData); - List output = spyAutoCompleter.getKeysForPath(propertyPath); + List output = configurationQueryHelper.getKeysForPath(propertyPath); assertThat(output).hasSize(2); assertThat(output).contains("Hello"); @@ -162,16 +156,15 @@ public void wildcardThroughList() { @Test public void nonNumberListIndex() { List propertyPath = Arrays.asList("inspectit", "exampleList", "iShouldNotBeHere"); - ConfigurationQueryHelper spyAutoCompleter = Mockito.spy(autoCompleter); HashMap topLevelMap = new HashMap<>(); HashMap inspectit = new HashMap<>(); List list = Arrays.asList("Hello", "there!"); inspectit.put("exampleList", list); topLevelMap.put("inspectit", inspectit); - Collection mockData = Arrays.asList(topLevelMap); - when(configurationFilesCache.getParsedConfigurationFiles()).thenReturn(mockData); + Collection mockData = Collections.singletonList(topLevelMap); + when(configurationFilesCache.getParsedContents()).thenReturn(mockData); - List output = spyAutoCompleter.getKeysForPath(propertyPath); + List output = configurationQueryHelper.getKeysForPath(propertyPath); assertThat(output).hasSize(0); } @@ -179,16 +172,15 @@ public void nonNumberListIndex() { @Test public void indexTooSmall() { List propertyPath = Arrays.asList("inspectit", "exampleList", "-5"); - ConfigurationQueryHelper spyAutoCompleter = Mockito.spy(autoCompleter); HashMap topLevelMap = new HashMap<>(); HashMap inspectit = new HashMap<>(); List list = Arrays.asList("Hello", "there!"); inspectit.put("exampleList", list); topLevelMap.put("inspectit", inspectit); - Collection mockData = Arrays.asList(topLevelMap); - when(configurationFilesCache.getParsedConfigurationFiles()).thenReturn(mockData); + Collection mockData = Collections.singletonList(topLevelMap); + when(configurationFilesCache.getParsedContents()).thenReturn(mockData); - List output = spyAutoCompleter.getKeysForPath(propertyPath); + List output = configurationQueryHelper.getKeysForPath(propertyPath); assertThat(output).hasSize(0); } @@ -196,16 +188,15 @@ public void indexTooSmall() { @Test public void indexTooBig() { List propertyPath = Arrays.asList("inspectit", "exampleList", "50000"); - ConfigurationQueryHelper spyAutoCompleter = Mockito.spy(autoCompleter); HashMap topLevelMap = new HashMap<>(); HashMap inspectit = new HashMap<>(); List list = Arrays.asList("Hello", "there!"); inspectit.put("exampleList", list); topLevelMap.put("inspectit", inspectit); - Collection mockData = Arrays.asList(topLevelMap); - when(configurationFilesCache.getParsedConfigurationFiles()).thenReturn(mockData); + Collection mockData = Collections.singletonList(topLevelMap); + when(configurationFilesCache.getParsedContents()).thenReturn(mockData); - List output = spyAutoCompleter.getKeysForPath(propertyPath); + List output = configurationQueryHelper.getKeysForPath(propertyPath); assertThat(output).hasSize(0); } diff --git a/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/search/FileContentSearchEngineTest.java b/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/search/FileContentSearchEngineTest.java new file mode 100644 index 0000000000..9c4dd3729a --- /dev/null +++ b/components/inspectit-ocelot-configurationserver/src/test/java/rocks/inspectit/ocelot/search/FileContentSearchEngineTest.java @@ -0,0 +1,131 @@ +package rocks.inspectit.ocelot.search; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import rocks.inspectit.ocelot.file.FileInfo; +import rocks.inspectit.ocelot.file.FileManager; +import rocks.inspectit.ocelot.file.accessor.git.RevisionAccess; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class FileContentSearchEngineTest { + + @InjectMocks + FileContentSearchEngine searchEngine; + + @Mock + FileManager fileManager; + + @Nested + class SearchLines { + + @Test + void findSingleStringPerLine() { + RevisionAccess mockAccess = mock(RevisionAccess.class); + when(fileManager.getWorkspaceRevision()).thenReturn(mockAccess); + FileInfo fileInfo1 = FileInfo.builder().name("file_test1").type(FileInfo.Type.FILE).build(); + FileInfo fileInfo2 = FileInfo.builder().name("file_test2").type(FileInfo.Type.FILE).build(); + List testFiles = Arrays.asList(fileInfo1, fileInfo2); + when(mockAccess.listConfigurationFiles("")).thenReturn(testFiles); + doReturn(java.util.Optional.of("i am the test1 content"), java.util.Optional.of("i am the test2 content")).when(mockAccess) + .readConfigurationFile(any()); + + List output = searchEngine.search("test1", 100); + + assertThat(output).extracting(SearchResult::getFile, SearchResult::getStartLine, SearchResult::getStartColumn, SearchResult::getEndLine, SearchResult::getEndColumn) + .containsExactly(tuple("file_test1", 0, 9, 0, 14)); + } + + @Test + void findMultipleStringsInLine() { + RevisionAccess mockAccess = mock(RevisionAccess.class); + when(fileManager.getWorkspaceRevision()).thenReturn(mockAccess); + FileInfo fileInfo = FileInfo.builder().name("file_test1").type(FileInfo.Type.FILE).build(); + List testFiles = Collections.singletonList(fileInfo); + when(mockAccess.listConfigurationFiles("")).thenReturn(testFiles); + when(mockAccess.readConfigurationFile(any())).thenReturn(java.util.Optional.of("test1test1test1")); + + List output = searchEngine.search("test1", 100); + + assertThat(output).extracting(SearchResult::getFile, SearchResult::getStartLine, SearchResult::getStartColumn, SearchResult::getEndLine, SearchResult::getEndColumn) + .containsExactly(tuple("file_test1", 0, 0, 0, 5), tuple("file_test1", 0, 5, 0, 10), tuple("file_test1", 0, 10, 0, 15)); + } + + @Test + void queryOverLine() { + RevisionAccess mockAccess = mock(RevisionAccess.class); + when(fileManager.getWorkspaceRevision()).thenReturn(mockAccess); + FileInfo fileInfo = FileInfo.builder().name("file_test1").type(FileInfo.Type.FILE).build(); + List testFiles = Collections.singletonList(fileInfo); + when(mockAccess.listConfigurationFiles("")).thenReturn(testFiles); + when(mockAccess.readConfigurationFile(any())).thenReturn(java.util.Optional.of("foo\nbar")); + + List output = searchEngine.search("foo\nbar", 100); + + assertThat(output).extracting(SearchResult::getFile, SearchResult::getStartLine, SearchResult::getStartColumn, SearchResult::getEndLine, SearchResult::getEndColumn) + .containsExactly(tuple("file_test1", 0, 0, 1, 3)); + } + + @Test + void withLimit() { + RevisionAccess mockAccess = mock(RevisionAccess.class); + when(fileManager.getWorkspaceRevision()).thenReturn(mockAccess); + FileInfo fileInfo = FileInfo.builder().name("file_test1").type(FileInfo.Type.FILE).build(); + List testFiles = Collections.singletonList(fileInfo); + when(mockAccess.listConfigurationFiles("")).thenReturn(testFiles); + when(mockAccess.readConfigurationFile(any())).thenReturn(java.util.Optional.of("testtesttest")); + + List output = searchEngine.search("test", 1); + + assertThat(output).extracting(SearchResult::getFile, SearchResult::getStartLine, SearchResult::getStartColumn, SearchResult::getEndLine, SearchResult::getEndColumn) + .containsExactly(tuple("file_test1", 0, 0, 0, 4)); + } + + @Test + void stringNotPresent() { + RevisionAccess mockAccess = mock(RevisionAccess.class); + when(fileManager.getWorkspaceRevision()).thenReturn(mockAccess); + FileInfo fileInfo = FileInfo.builder().name("file_test1").type(FileInfo.Type.FILE).build(); + List testFiles = Collections.singletonList(fileInfo); + when(mockAccess.listConfigurationFiles("")).thenReturn(testFiles); + when(mockAccess.readConfigurationFile(any())).thenReturn(java.util.Optional.of("test1 \n abc \n test1")); + + List output = searchEngine.search("foo", 100); + + assertThat(output).isEmpty(); + } + + @Test + void matchingStringNotInFirstLine() { + RevisionAccess mockAccess = mock(RevisionAccess.class); + when(fileManager.getWorkspaceRevision()).thenReturn(mockAccess); + FileInfo fileInfo = FileInfo.builder().name("file_test1").type(FileInfo.Type.FILE).build(); + List testFiles = Collections.singletonList(fileInfo); + when(mockAccess.listConfigurationFiles("")).thenReturn(testFiles); + when(mockAccess.readConfigurationFile(any())).thenReturn(java.util.Optional.of("test2\ntest1\ntest2 next line\nand another one\nits here: test2")); + + List output = searchEngine.search("test2", 100); + + assertThat(output).extracting(SearchResult::getFile, SearchResult::getStartLine, SearchResult::getStartColumn, SearchResult::getEndLine, SearchResult::getEndColumn) + .containsExactly(tuple("file_test1", 0, 0, 0, 5), tuple("file_test1", 2, 0, 2, 5), tuple("file_test1", 4, 10, 4, 15)); + } + + @Test + void emptyQuery() { + List output = searchEngine.search("", 100); + + assertThat(output).isEmpty(); + } + } +}