Skip to content

Commit

Permalink
Closes #560 - Added endpoint to configuration server to find files co…
Browse files Browse the repository at this point in the history
…ntaining specific text (#659)

Co-authored-by: Marius Oehler <[email protected]>
  • Loading branch information
MariusBrill and mariusoe authored Aug 12, 2020
1 parent 41170f3 commit e840d16
Show file tree
Hide file tree
Showing 7 changed files with 413 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public class ConfigurationFilesCache {
/**
* The current parsed contents of all configuration files.
*/
private Collection<Object> currentParsedFiles = Collections.emptyList();
private Collection<Object> parsedContents = Collections.emptyList();

/**
* Returns the most recently loaded .yaml and .yml files as a list of Objects. Each Object resembles the corresponding
Expand All @@ -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<Object> getParsedConfigurationFiles() {
return currentParsedFiles;
public Collection<Object> getParsedContents() {
return parsedContents;
}

/**
Expand All @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> getKeysForPath(List<String> path) {
return configurationFilesCache.getParsedConfigurationFiles()
return configurationFilesCache.getParsedContents()
.stream()
.flatMap(root -> extractKeys(root, path).stream())
.collect(Collectors.toList());
Expand All @@ -42,6 +43,7 @@ public List<String> getKeysForPath(List<String> 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<String> extractKeys(Object o, List<String> mapPath) {
Expand All @@ -65,6 +67,7 @@ private List<String> extractKeys(Object o, List<String> 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<String> toStringList(Collection<?> collection) {
Expand All @@ -82,6 +85,7 @@ private List<String> 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<String> extractKeysFromList(List<?> list, List<String> mapPath) {
Expand Down Expand Up @@ -120,6 +124,7 @@ private List<String> extractKeysFromList(List<?> list, List<String> 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<String> extractKeysFromMap(Map<?, ?> map, List<String> mapPath) {
Expand All @@ -129,7 +134,8 @@ private List<String> extractKeysFromMap(Map<?, ?> map, List<String> mapPath) {
String currentLiteral = mapPath.get(0);
List<String> 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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;
Expand All @@ -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<String> contentOptional = fileManager.getWorkingDirectory().readConfigurationFile(path);

Expand All @@ -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:" + "<p>" + "<b>file:</b> a String resembling the name of the file the match was found in." + "<p>" + "<b>startLine:</b> the number of the line in this file where the found match starts as " + "integer." + "<p>" + "<b>endLine:</b> the number of the line in this file where the found match ends as integer." + "<p>" + "<b>startColumn:</b> the number of the column where the found found match starts as " + "integer." + "<p>" + "<b>endColumn:</b> 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<SearchResult> searchForContent(@RequestParam String query, @RequestParam(defaultValue = "100") int limit) {
return fileContentSearchEngine.search(query, limit);
}
}
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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<SearchResult> 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<SearchResult> search(String query, int limit, RevisionAccess revisionAccess) {
Pattern queryPattern = Pattern.compile(Pattern.quote(query));
List<FileInfo> files = revisionAccess.listConfigurationFiles("");

AtomicInteger limitCounter = new AtomicInteger(limit);

List<SearchResult> result = files.stream()
.map(fileInfo -> fileInfo.getAbsoluteFilePaths(""))
.reduce(Stream.empty(), Stream::concat)
.map(filename -> {
Optional<String> 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<SearchResult> findQuery(String fileName, String content, Pattern queryPattern, AtomicInteger limitCounter) {
if (StringUtils.isEmpty(content)) {
return Collections.emptyList();
}

List<SearchResult> results = new ArrayList<>();

List<Line> lines = getLines(content);

ListIterator<Line> 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<Line> getLines(String content) {
if (StringUtils.isEmpty(content)) {
return Collections.emptyList();
}

List<Line> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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;

}
Loading

0 comments on commit e840d16

Please sign in to comment.