Commit 1e48c78f authored by Valentin Suhnjov's avatar Valentin Suhnjov

Merge branch 'releases/release-0.8.0' into master

parents b802ab9e 742de038
......@@ -5,20 +5,9 @@ variables:
ARTIFACT_NAME: "ROOT.war"
stages:
- test
- build
- deploy
test:
stage: test
before_script:
- node -v
- npm -v
script:
- ./test.sh
tags:
- riha
build:
stage: build
script:
......
......@@ -12,7 +12,7 @@
<parent>
<groupId>ee.ria.riha</groupId>
<artifactId>browser</artifactId>
<version>0.7.0</version>
<version>0.8.0</version>
</parent>
<dependencies>
......@@ -48,6 +48,12 @@
<groupId>com.github.fge</groupId>
<artifactId>json-schema-validator</artifactId>
<version>2.2.6</version>
<exclusions>
<exclusion>
<groupId>javax.mail</groupId>
<artifactId>mailapi</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
......@@ -69,10 +75,18 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
<groupId>ee.ria.riha</groupId>
<artifactId>storage-client</artifactId>
<version>0.4.0</version>
<version>0.5.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
......
package ee.ria.riha.conf;
import com.github.fge.jackson.JsonLoader;
import ee.ria.riha.domain.InfoSystemRepository;
import ee.ria.riha.domain.RihaStorageInfoSystemRepository;
import ee.ria.riha.service.JsonValidationService;
import ee.ria.riha.storage.client.StorageClient;
import ee.ria.riha.storage.domain.CommentRepository;
import ee.ria.riha.storage.domain.MainResourceRepository;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
......@@ -19,28 +16,14 @@ import java.io.IOException;
*/
@Configuration
@EnableConfigurationProperties(ApplicationProperties.class)
@EnableScheduling
public class ApplicationConfiguration {
@Bean
public MainResourceRepository mainResourceRepository(ApplicationProperties applicationProperties) {
return new MainResourceRepository(getStorageClient(applicationProperties));
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder.build();
}
private StorageClient getStorageClient(ApplicationProperties applicationProperties) {
RestTemplate restTemplate = new RestTemplate();
return new StorageClient(restTemplate, applicationProperties.getStorageClient().getBaseUrl());
}
@Bean
public InfoSystemRepository infoSystemRepository(MainResourceRepository mainResourceRepository) {
return new RihaStorageInfoSystemRepository(mainResourceRepository);
}
@Bean
public CommentRepository commentRepository(ApplicationProperties applicationProperties) {
return new CommentRepository(getStorageClient(applicationProperties));
}
@Bean
public JsonValidationService jsonValidationService(ApplicationProperties applicationProperties) throws IOException {
return new JsonValidationService(
......
......@@ -19,6 +19,10 @@ public class ApplicationProperties {
private final StorageClientProperties storageClient = new StorageClientProperties();
private final AuthenticationProperties authentication = new AuthenticationProperties();
private final ValidationProperties validation = new ValidationProperties();
private final NotificationProperties notification = new NotificationProperties();
@Setter
private String baseUrl;
@Getter
@Setter
......@@ -43,4 +47,19 @@ public class ApplicationProperties {
public static class ValidationProperties {
private String jsonSchemaUrl;
}
@Getter
@Setter
public static class NotificationProperties {
private final CreatedInfoSystemsOverview createdInfoSystemsOverview = new CreatedInfoSystemsOverview();
private String from;
}
@Getter
@Setter
public static class CreatedInfoSystemsOverview {
private String[] to;
private String[] cc;
private String[] bcc;
}
}
package ee.ria.riha.conf;
import ee.ria.riha.domain.InfoSystemRepository;
import ee.ria.riha.domain.RihaStorageInfoSystemRepository;
import ee.ria.riha.storage.client.StorageClient;
import ee.ria.riha.storage.domain.CommentRepository;
import ee.ria.riha.storage.domain.FileRepository;
import ee.ria.riha.storage.domain.MainResourceRepository;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class StorageConfiguration {
@Bean
public StorageClient getStorageClient(RestTemplate restTemplate, ApplicationProperties applicationProperties) {
return new StorageClient(restTemplate, applicationProperties.getStorageClient().getBaseUrl());
}
@Bean
public MainResourceRepository mainResourceRepository(StorageClient storageClient) {
return new MainResourceRepository(storageClient);
}
@Bean
public InfoSystemRepository infoSystemRepository(MainResourceRepository mainResourceRepository) {
return new RihaStorageInfoSystemRepository(mainResourceRepository);
}
@Bean
public CommentRepository commentRepository(StorageClient storageClient) {
return new CommentRepository(storageClient);
}
@Bean
public FileRepository fileRepository(RestTemplate restTemplate, ApplicationProperties applicationProperties) {
return new FileRepository(restTemplate, applicationProperties.getStorageClient().getBaseUrl());
}
}
......@@ -22,6 +22,10 @@ public class InfoSystem {
private static final String OWNER_NAME_KEY = "name";
private static final String OWNER_CODE_KEY = "code";
private static final String SHORT_NAME_KEY = "short_name";
private static final String FULL_NAME_KEY = "name";
private static final String META_KEY = "meta";
private static final String META_CREATION_TIMESTAMP_KEY = "creation_timestamp";
private static final String META_UPDATE_TIMESTAMP_KEY = "update_timestamp";
private JSONObject jsonObject = new JSONObject();
......@@ -30,6 +34,9 @@ public class InfoSystem {
private String ownerName;
private String ownerCode;
private String shortName;
private String fullName;
private String creationTimestamp;
private String updateTimestamp;
public InfoSystem() {
this("{}");
......@@ -45,10 +52,14 @@ public class InfoSystem {
this.uuid = hasText(uuidString) ? UUID.fromString(uuidString) : null;
this.shortName = ((String) getPath(SHORT_NAME_KEY).queryFrom(jsonObject));
this.fullName = ((String) getPath(FULL_NAME_KEY).queryFrom(jsonObject));
JSONObject owner = getOwner();
this.ownerName = ((String) getPath(OWNER_NAME_KEY).queryFrom(owner));
this.ownerCode = ((String) getPath(OWNER_CODE_KEY).queryFrom(owner));
this.creationTimestamp = (String) getPath(META_CREATION_TIMESTAMP_KEY).queryFrom(getMeta());
this.updateTimestamp = (String) getPath(META_UPDATE_TIMESTAMP_KEY).queryFrom(getMeta());
}
public InfoSystem(String json) {
......@@ -127,4 +138,42 @@ public class InfoSystem {
this.shortName = shortName;
jsonObject.putOpt(SHORT_NAME_KEY, shortName);
}
public String getFullName() {
return fullName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
jsonObject.putOpt(FULL_NAME_KEY, fullName);
}
private JSONObject getMeta() {
JSONObject meta = ((JSONObject) getPath(META_KEY).queryFrom(jsonObject));
if (meta == null) {
meta = new JSONObject();
jsonObject.put(META_KEY, meta);
}
return meta;
}
public String getCreationTimestamp() {
return this.creationTimestamp;
}
public void setCreationTimestamp(String creationTimestamp) {
this.creationTimestamp = creationTimestamp;
getMeta().putOpt(META_CREATION_TIMESTAMP_KEY, creationTimestamp);
}
public String getUpdateTimestamp() {
return updateTimestamp;
}
public void setUpdateTimestamp(String updateTimestamp) {
this.updateTimestamp = updateTimestamp;
getMeta().putOpt(META_UPDATE_TIMESTAMP_KEY, updateTimestamp);
}
}
package ee.ria.riha.service;
import ee.ria.riha.conf.ApplicationProperties;
import ee.ria.riha.domain.InfoSystemRepository;
import ee.ria.riha.domain.model.InfoSystem;
import ee.ria.riha.storage.util.FilterRequest;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.mail.MailPreparationException;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.ui.freemarker.FreeMarkerTemplateUtils;
import org.springframework.util.Assert;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.*;
@Component
@Slf4j
public class CreatedInfoSystemsOverviewNotificationJob {
private static final String MESSAGE_TEMPLATE = "new-IS-broadcast-template.ftl";
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
private final String baseUrl;
private final String from;
private final String[] to;
private final String[] cc;
private final String[] bcc;
private JavaMailSenderImpl mailSender;
private Configuration freeMarkerConfiguration;
private InfoSystemRepository infoSystemRepository;
private MessageSource messageSource;
@Autowired
public CreatedInfoSystemsOverviewNotificationJob(ApplicationProperties applicationProperties) {
baseUrl = applicationProperties.getBaseUrl();
Assert.hasText(baseUrl, "Base URL must be defined");
from = applicationProperties.getNotification().getFrom();
Assert.hasText(from, "Notification email sender must be defined");
to = applicationProperties.getNotification().getCreatedInfoSystemsOverview().getTo();
Assert.notEmpty(to, "At least one recipient must be defined in the list of recipients");
cc = applicationProperties.getNotification().getCreatedInfoSystemsOverview().getCc();
bcc = applicationProperties.getNotification().getCreatedInfoSystemsOverview().getBcc();
}
@Scheduled(cron = "${browser.notification.createdInfoSystemsOverview.cron}")
public void sendCreatedInfoSystemsOverviewNotification() {
try {
List<InfoSystem> infoSystems = getListOfCreatedInfoSystems();
sendNewInfoSystemsBroadcastMessage(infoSystems);
} catch (Exception e) {
log.warn("Job execution failed", e);
}
}
private List<InfoSystem> getListOfCreatedInfoSystems() {
FilterRequest filter = new FilterRequest();
LocalDateTime endDateTime = LocalDateTime.now().truncatedTo(ChronoUnit.HOURS);
filter.addFilter("j_creation_timestamp,<," + DATE_TIME_FORMATTER.format(endDateTime));
LocalDateTime startDateTime = endDateTime.minusDays(1);
filter.addFilter("j_creation_timestamp,>=," + DATE_TIME_FORMATTER.format(startDateTime));
if (log.isDebugEnabled()) {
log.debug("Searching for info systems created between {} and {}", startDateTime, endDateTime);
}
List<InfoSystem> infoSystems = infoSystemRepository.find(filter);
if (log.isDebugEnabled()) {
log.debug("{} info system(s) found", infoSystems.size());
}
return infoSystems;
}
private void sendNewInfoSystemsBroadcastMessage(List<InfoSystem> infoSystems) {
if (infoSystems.isEmpty()) {
log.info("Info system list is empty, nothing to send");
return;
}
MimeMessage message;
try {
message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message);
helper.setFrom(from);
helper.setTo(to);
if (cc != null) {
helper.setCc(cc);
}
if (bcc != null) {
helper.setBcc(bcc);
}
helper.setSubject(getMessageSubject());
helper.setSentDate(new Date());
helper.setText(getMessageText(infoSystems), true);
if (log.isDebugEnabled()) {
log.debug("Sending notification message from '{}' to '{}' with subject '{}'", from, to, message.getSubject());
}
} catch (MessagingException e) {
throw new MailPreparationException("Error preparing notification message", e);
}
mailSender.send(message);
log.info("Created info systems overview notification message has been successfully sent");
}
private String getMessageSubject() {
return messageSource.getMessage("notifications.createdInfoSystemsOverview.subject", null, Locale.getDefault());
}
private String getMessageText(List<InfoSystem> infoSystems) {
try {
Template template = freeMarkerConfiguration.getTemplate(MESSAGE_TEMPLATE);
Map<String, Object> model = new HashMap<>();
model.put("infosystems", infoSystems);
model.put("baseUrl", baseUrl);
return FreeMarkerTemplateUtils.processTemplateIntoString(template, model);
} catch (IOException | TemplateException e) {
throw new MailPreparationException("Error generating notification message text template " + MESSAGE_TEMPLATE, e);
}
}
@Autowired
public void setInfoSystemRepository(InfoSystemRepository infoSystemRepository) {
this.infoSystemRepository = infoSystemRepository;
}
@Autowired
public void setMailSender(JavaMailSenderImpl mailSender) {
this.mailSender = mailSender;
}
@Autowired
public void setFreeMarkerConfiguration(Configuration freeMarkerConfiguration) {
this.freeMarkerConfiguration = freeMarkerConfiguration;
}
@Autowired
public void setMessageSource(MessageSource messageSource) {
this.messageSource = messageSource;
}
}
package ee.ria.riha.service;
import ee.ria.riha.storage.domain.FileRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;
@Service
@Slf4j
public class FileService {
@Autowired
private FileRepository fileRepository;
public UUID upload(InputStream inputStream, String fileName, String contentType) {
log.info("Uploading file '{}' to storage", fileName);
UUID fileUuid = fileRepository.upload(inputStream, fileName, contentType);
log.info("File uploaded with uuid: {}", fileUuid);
return fileUuid;
}
public ResponseEntity download(UUID fileUuid) throws IOException {
return fileRepository.download(fileUuid);
}
}
......@@ -11,6 +11,8 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.UUID;
......@@ -30,6 +32,8 @@ public class InfoSystemService {
@Autowired
private JsonValidationService infoSystemValidationService;
private DateTimeFormatter isoDateTimeFormatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
public PagedResponse<InfoSystem> list(Pageable pageable, Filterable filterable) {
return infoSystemRepository.list(pageable, filterable);
}
......@@ -49,12 +53,15 @@ public class InfoSystemService {
InfoSystem infoSystem = new InfoSystem(model.getJsonObject());
log.info("User '{}' with active organization '{}'" +
" is creating new info system with short name '{}'",
getRihaUserPersonalCode(), organization, infoSystem.getShortName());
" is creating new info system with short name '{}'",
getRihaUserPersonalCode(), organization, infoSystem.getShortName());
validateInfoSystemShortName(infoSystem.getShortName());
infoSystem.setUuid(UUID.randomUUID());
infoSystem.setOwnerCode(organization.getCode());
infoSystem.setOwnerName(organization.getName());
String creationTimestamp = isoDateTimeFormatter.format(ZonedDateTime.now());
infoSystem.setCreationTimestamp(creationTimestamp);
infoSystem.setUpdateTimestamp(creationTimestamp);
infoSystemValidationService.validate(infoSystem.asJson());
......@@ -91,9 +98,9 @@ public class InfoSystemService {
public InfoSystem update(String shortName, InfoSystem model) {
InfoSystem existingInfoSystem = get(shortName);
log.info("User '{}' with active organization '{}'" +
" is updating info system with id {}, owner code '{}' and short name '{}'",
getRihaUserPersonalCode(), getActiveOrganization(), existingInfoSystem.getId(),
existingInfoSystem.getOwnerCode(), existingInfoSystem.getShortName());
" is updating info system with id {}, owner code '{}' and short name '{}'",
getRihaUserPersonalCode(), getActiveOrganization(), existingInfoSystem.getId(),
existingInfoSystem.getOwnerCode(), existingInfoSystem.getShortName());
InfoSystem updatedInfoSystem = new InfoSystem(model.getJsonObject());
if (!shortName.equals(updatedInfoSystem.getShortName())) {
......@@ -102,6 +109,8 @@ public class InfoSystemService {
updatedInfoSystem.setUuid(existingInfoSystem.getUuid());
updatedInfoSystem.setOwnerCode(existingInfoSystem.getOwnerCode());
updatedInfoSystem.setOwnerName(existingInfoSystem.getOwnerName());
updatedInfoSystem.setCreationTimestamp(existingInfoSystem.getCreationTimestamp());
updatedInfoSystem.setUpdateTimestamp(isoDateTimeFormatter.format(ZonedDateTime.now()));
infoSystemValidationService.validate(updatedInfoSystem.asJson());
......
......@@ -5,12 +5,13 @@ package ee.ria.riha.service;
*/
public class ValidationException extends BrowserException {
private String code;
private Object[] args;
private final String code;
private final Object[] args;
public ValidationException(String code) {
super(code);
this.code = code;
this.args = new Object[0];
}
public ValidationException(String code, Object... args) {
......
package ee.ria.riha.web;
import ee.ria.riha.service.FileService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;
import static ee.ria.riha.conf.ApplicationProperties.API_V1_PREFIX;
@RestController
@RequestMapping(API_V1_PREFIX + "/files")
@Slf4j
@Api("File resources")
public class FileController {
@Autowired
private FileService fileService;
@PostMapping
@PreAuthorize("hasRole('ROLE_KIRJELDAJA')")
@ApiOperation("Upload file")
public ResponseEntity upload(@RequestParam("file") MultipartFile file) throws IOException {
log.info("Receiving file '{}' [{}] with size {}b",
file.getOriginalFilename(), file.getContentType(), file.getSize());
UUID fileUuid = fileService.upload(file.getInputStream(), file.getOriginalFilename(), file.getContentType());
return ResponseEntity.ok(fileUuid.toString());
}
@GetMapping("/{uuid}")
@ApiOperation("Download file")
public ResponseEntity download(@PathVariable("uuid") UUID fileUuid) throws IOException {
log.info("Downloading file {}", fileUuid);
return fileService.download(fileUuid);
}
}
......@@ -20,6 +20,8 @@ import static ee.ria.riha.service.SecurityContextUtil.isUserAuthenticated;
@Component
public class InfoSystemModelMapper {
private static final Predicate<String> EVICTED_NAMES_PREDICATE = name -> name.equalsIgnoreCase("contacts");
/**
* Maps {@link InfoSystem} to {@link InfoSystemModel} performing additional transformations that depend on user
* authentication. Unauthenticated users will not see some json properties like contacts.
......@@ -44,15 +46,11 @@ public class InfoSystemModelMapper {
JSONObject originalJsonObject = infoSystem.getJsonObject();
List<String> filteredNames = Arrays.stream(JSONObject.getNames(originalJsonObject))
.filter(getEvictedNamesPredicate().negate())
.filter(EVICTED_NAMES_PREDICATE.negate())
.collect(Collectors.toList());
JSONObject shallowJsonObjectCopy = new JSONObject(originalJsonObject, filteredNames.toArray(new String[0]));
return new InfoSystem(shallowJsonObjectCopy);
}
private Predicate<String> getEvictedNamesPredicate() {
return name -> name.equalsIgnoreCase("contacts");
}
}
......@@ -8,7 +8,26 @@ spring.http.encoding.force=true
## Use full date during serialization
spring.jackson.serialization.write-dates-as-timestamps=false
spring.jackson.timeZone=Europe/Tallinn
spring.jackson.dateFormat=yyyy-MM-dd'T'HH:mm:ss.SSSZ
spring.jackson.dateFormat=yyyy-MM-dd'T'HH:mm:ss
## File upload limits
spring.http.multipart.max-file-size=10240MB
spring.http.multipart.max-request-size=10240MB
## SMTP Server parameters
### Connection parameters
spring.mail.host=localhost
spring.mail.port=25
#spring.mail.username=
#spring.mail.password=
### Force UTF-8 enconding of messages
spring.mail.default-encoding=utf-8
## RIHA-Browser URL
browser.baseUrl=https://riha.eesti.ee
## RIHA-Storage client API URL
......@@ -32,4 +51,18 @@ browser.authentication.userSearchFilter=(uid={0})
## JSON validation schema URL. Used to validate information system details
browser.validation.jsonSchemaUrl=/infosystem_schema.json
\ No newline at end of file
browser.validation.jsonSchemaUrl=/infosystem_schema.json
## Notification service properties
### Default notification sender
browser.notification.from=no-reply@ria.ee
## Created info system overview notification properties
### Comma separated recipients of notification message
browser.notification.createdInfoSystemsOverview.to=riha-kooskolastajad@your_domain_name_here
#browser.notification.createdInfoSystemsOverview.cc
#browser.notification.createdInfoSystemsOverview.bcc
### Schedule for daily notification of info systems created during previous 24 hours. Every day at 22:00:00
browser.notification.createdInfoSystemsOverview.cron=0 0 22 * * *
\ No newline at end of file
......@@ -11,16 +11,41 @@
"maxItems": 10,
"type": "array"
},
"data_files": {
"items": {
"properties": {
"url": {
"description": "URL, mis viitab infosüsteemi faili avalikule asukohale",
"type": "string",
"minLength": 1
},
"name": {
"description": "Dokumendi pealkiri. Näiteks - infosüsteemi teenuste liidestusjuhend",
"type": "string",
"minLength": 1
}
},
"required": [
"url",