Skip to content

Commit

Permalink
Merge branch 'dev'
Browse files Browse the repository at this point in the history
  • Loading branch information
cgendreau committed Dec 13, 2023
2 parents 47b617d + 4bb4080 commit 6b21173
Show file tree
Hide file tree
Showing 23 changed files with 540 additions and 39 deletions.
20 changes: 14 additions & 6 deletions dina-base-api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<parent>
<groupId>io.github.aafc-bicoe</groupId>
<artifactId>dina-base-parent</artifactId>
<version>0.111</version>
<version>0.112</version>
</parent>

<artifactId>dina-base-api</artifactId>
Expand All @@ -26,9 +26,10 @@
<javers.version>6.13.0</javers.version>
<mybatis.version>2.2.2</mybatis.version>
<aafc.search.messaging.version>0.20</aafc.search.messaging.version>
<hibernate-types-55.version>2.21.1</hibernate-types-55.version>
<hypersistence-utils-hibernate-55.version>3.6.1</hypersistence-utils-hibernate-55.version>
<postgresql.version>42.4.3</postgresql.version>
<jsoup.version>1.15.3</jsoup.version>
<commons-io.version>2.15.1</commons-io.version>
</properties>

<dependencies>
Expand Down Expand Up @@ -129,13 +130,19 @@
<dependency>
<groupId>io.github.aafc-bicoe</groupId>
<artifactId>dina-test-support</artifactId>
<version>0.111</version>
<version>0.112</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-core</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>

<!-- Spring mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
Expand All @@ -151,10 +158,11 @@
<artifactId>search-messaging</artifactId>
<version>${aafc.search.messaging.version}</version>
</dependency>

<dependency>
<groupId>com.vladmihalcea</groupId>
<artifactId>hibernate-types-55</artifactId>
<version>${hibernate-types-55.version}</version>
<groupId>io.hypersistence</groupId>
<artifactId>hypersistence-utils-hibernate-55</artifactId>
<version>${hypersistence-utils-hibernate-55.version}</version>
</dependency>

<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@

import io.micrometer.core.aop.TimedAspect;
import io.micrometer.core.instrument.MeterRegistry;

import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController;
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
Expand All @@ -20,11 +25,14 @@
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;

import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;

import java.util.Locale;

import ca.gc.aafc.dina.crkn.PatchedCrnkErrorController;

@Configuration
// Must explicitly depend on "querySpecUrlMapper" so Spring can inject it into this class'
// initQuerySpecUrlMapper method.
Expand All @@ -50,6 +58,14 @@ public void initTransactionOperationFilter(OperationsModule module) {
module.setResumeOnError(true);
}

/**
* override Crnk provided ErrorController so it can work with SpringBoot >= 2.5
*/
@Bean
public BasicErrorController jsonapiErrorController(ErrorAttributes errorAttributes, ServerProperties serverProperties, List<ErrorViewResolver> errorViewResolvers) {
return new PatchedCrnkErrorController(errorAttributes, serverProperties.getError(), errorViewResolvers);
}

@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ public class DevSettings {
private Map<String, Set<String>> groupRole;

public Map<String, Set<DinaRole>> getRolesPerGroup() {

if(groupRole == null || groupRole.isEmpty()) {
return Map.of();
}

Map<String, Set<DinaRole>> groupDinaRole = new HashMap<>(groupRole.size());

for(var entry : groupRole.entrySet()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package ca.gc.aafc.dina.crkn;

import io.crnk.core.engine.document.Document;
import io.crnk.core.engine.document.ErrorData;
import io.crnk.core.engine.document.ErrorDataBuilder;
import io.crnk.core.engine.http.HttpHeaders;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController;
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorViewResolver;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

/**
* CrnkErrorController but patched for SpringBoot >= 2.5
* Source: https://github.com/crnk-project/crnk-framework/commit/0ed1721159943f6ffc5260ac502252efbbcc39c8
*
* We can't upgrade Crnk at this point since it creates other (more complex) issues and the project is not maintained anymore.
*
*/
public class PatchedCrnkErrorController extends BasicErrorController {

public PatchedCrnkErrorController(ErrorAttributes errorAttributes,
ErrorProperties errorProperties) {
super(errorAttributes, errorProperties);
}

public PatchedCrnkErrorController(ErrorAttributes errorAttributes,
ErrorProperties errorProperties,
List<ErrorViewResolver> errorViewResolvers) {
super(errorAttributes, errorProperties, errorViewResolvers);
}

// TODO for whatever reason this is not called directly
@RequestMapping(produces = HttpHeaders.JSONAPI_CONTENT_TYPE)
@ResponseBody
public ResponseEntity<Document> errorToJsonApi(HttpServletRequest request) {
Map<String, Object> body = getErrorAttributes(request,
getErrorAttributeOptions(request, MediaType.ALL));
HttpStatus status = getStatus(request);

ErrorDataBuilder errorDataBuilder = ErrorData.builder();
for (Map.Entry<String, Object> attribute : body.entrySet()) {
if (attribute.getKey().equals("status")) {
errorDataBuilder.setStatus(attribute.getValue().toString());
} else if (attribute.getKey().equals("error")) {
errorDataBuilder.setTitle(attribute.getValue().toString());
} else if (attribute.getKey().equals("message")) {
errorDataBuilder.setDetail(attribute.getValue().toString());
} else {
errorDataBuilder.addMetaField(attribute.getKey(), attribute.getValue());
}
}
Document document = new Document();
document.setErrors(Arrays.asList(errorDataBuilder.build()));
return new ResponseEntity<>(document, status);
}


@RequestMapping
@ResponseBody
public ResponseEntity error(HttpServletRequest request) {
return errorToJsonApi(request);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package ca.gc.aafc.dina.exceptionmapping;

import io.crnk.core.engine.document.ErrorData;
import io.crnk.core.engine.error.ErrorResponse;
import io.crnk.core.engine.error.ExceptionMapper;
import io.crnk.core.engine.http.HttpStatus;
import java.util.Collections;
import javax.inject.Named;

@Named
public class IllegalStateExceptionMapper implements ExceptionMapper<IllegalStateException> {

private static final Integer STATUS_ON_ERROR = HttpStatus.BAD_REQUEST_400;

@Override
public ErrorResponse toErrorResponse(IllegalStateException exception) {
return new ErrorResponse(
Collections.singletonList(
ErrorData.builder()
.setStatus(STATUS_ON_ERROR.toString())
.setTitle("BAD_REQUEST")
.setDetail(exception.getMessage())
.build()
),
STATUS_ON_ERROR
);
}

@Override
public IllegalStateException fromErrorResponse(ErrorResponse errorResponse) {
throw new UnsupportedOperationException("Crnk client not supported");
}

@Override
public boolean accepts(ErrorResponse errorResponse) {
throw new UnsupportedOperationException("Crnk client not supported");
}
}
155 changes: 155 additions & 0 deletions dina-base-api/src/main/java/ca/gc/aafc/dina/file/FileCleaner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package ca.gc.aafc.dina.file;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.TemporalUnit;
import java.util.EnumSet;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;

import lombok.SneakyThrows;

/**
* Deletes file, recursively, from a root path if the provided predicate returns true.
* By default, only files will be checked (no folders/no symlinks).
*
* Some checks are also included to avoid file system root or non-existing folder.
*/
public final class FileCleaner {

public enum Options { ALLOW_NON_TMP }

private static final String TMP_DIR_PROPERTY = "java.io.tmpdir";

private final Path rootPath;
private final Predicate<Path> predicate;

/**
* Creates a default instance where the provided predicate will be combined with the buildFileOnlyPredicate.
* Folders and symlinks will be ignored.
* @param rootPath
* @param predicate
* @return
*/
public static FileCleaner newInstance(Path rootPath, Predicate<Path> predicate) {
return new FileCleaner(rootPath, buildFileOnlyPredicate().and(predicate), null);
}

/**
* Creates an instance with specific options.
* Use carefully, options gives more flexibility but requires the caller to do more checks to avoid
* unwanted destructive (file delete) operations.
* @param rootPath
* @param predicate
* @param options
* @return
*/
public static FileCleaner newInstance(Path rootPath, Predicate<Path> predicate, EnumSet<Options> options) {
Objects.requireNonNull(options);
return new FileCleaner(rootPath, buildFileOnlyPredicate().and(predicate), options);
}

/**
* Private constructor to avoid misuse of always true predicate.
* @param rootPath
* @param predicate
*/
private FileCleaner(Path rootPath, Predicate<Path> predicate, EnumSet<Options> options) {
// sanity checks
Objects.requireNonNull(rootPath);
Objects.requireNonNull(predicate);

Path normalizedRootPath = rootPath.normalize();
if (!normalizedRootPath.toFile().isDirectory() || !normalizedRootPath.toFile().exists()) {
throw new IllegalArgumentException(
"FileCleaner can only be initialized on an existing directory");
}

// by default (no options provided) we restrict to tmp directory
boolean restrictToTmpDirectory = options == null || !options.contains(Options.ALLOW_NON_TMP);

if (restrictToTmpDirectory && !normalizedRootPath.startsWith(System.getProperty(TMP_DIR_PROPERTY))) {
throw new IllegalArgumentException(
"FileCleaner can only be initialized on a directory under " +
System.getProperty(TMP_DIR_PROPERTY));
}

if (StreamSupport.stream(normalizedRootPath.getFileSystem().getRootDirectories().spliterator(), false)
.anyMatch(p -> p.equals(normalizedRootPath))) {
throw new IllegalArgumentException("can't initialize FileCleaner on a root directory");
}

this.rootPath = normalizedRootPath;
this.predicate = predicate;
}

/**
* Build a predicate that is checking for the maximum age of a file based on its lastModifiedTime.
* @param unit
* @param maxAge
* @return
*/
public static Predicate<Path> buildMaxAgePredicate(TemporalUnit unit, long maxAge) {
return path -> {
Duration interval = Duration.between(getLastModifiedTime(path).toInstant(), Instant.now());
return interval.get(unit) > maxAge;
};
}

/**
* Build a predicate for checking for a specific file extension of a file.
*
* The check for the extension is case-insensitive.
*
* @param extension the extension without the leading "." (eg. "txt", "md").
* @return predicate checking the extension based on the file extension
* provided.
*/
public static Predicate<Path> buildFileExtensionPredicate(String extension) {
if (StringUtils.isBlank(extension)) {
throw new IllegalArgumentException("Extension must not be null or empty.");
}

return path -> FilenameUtils.isExtension(path.getFileName().toString().toLowerCase(), extension.toLowerCase());
}

/**
* Excludes folder and symlinks
* @return
*/
public static Predicate<Path> buildFileOnlyPredicate() {
return path -> Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS);
}

/**
* Clean folder recursively by deleting all files that are matching the predicate.
* @throws IOException
*/
public void clean() throws IOException {
try (Stream<Path> p = Files.walk(rootPath)) {
p.filter(predicate)
.forEach(FileCleaner::delete);
}
}

@SneakyThrows
public static FileTime getLastModifiedTime(Path path) {
return Files.getLastModifiedTime(path);
}

@SneakyThrows
public static void delete(Path path) {
Files.delete(path);
}

}
Loading

0 comments on commit 6b21173

Please sign in to comment.