Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added new renderer carbone #50

Merged
merged 11 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,3 +254,35 @@ For other fonts you need to explicitly add them to classpath of the application:
2. Export them as `.jar` via [Jaspersoft Studio](https://community.jaspersoft.com/documentation/tibco-jaspersoft-studio-user-guide/v640/working-font-extensions).
3. Add jar file to classpath of the application.
4. Now you can use them in your documents.

### How to generate documents with [Carbon](https://carbone.io/documentation.html)
Carbone allow to build reports templates using LibreOffice, Microsoft Word, or Google Docs.
Carbone could be used as cloud solution ([pricing](https://carbone.io/pricing.html)) or as on-premise installation.

On-premise installation is free to use (Community Edition). "Community Edition" features free of charge and without limits. Please note that advanced functions will not work.

[Community vs Enterprise](https://carbone.io/documentation.html#supported-files-and-features-list)

[Official docker](https://hub.docker.com/r/carbone/carbone-ee)


To set up carbon based renderer do the following:
1. Describe document specification in the documents YAML file with `renderer: CARBONE`
2. Add tenant specific configuration into `tenant-config.yml`

```yml
renderer:
carbone:
url: "http://carbone:4000"
headers:
Authorization: "Bearer YOUR_API_TOKEN"
```
Where:
* url - address of carbone installation (https://api.carbone.io in case of using cloud)
* headers.Authorization - optional parameter, used for cloud version ([more info](https://carbone.io/documentation/developer/http-api/introduction.html#authentication))

3. Add your template file to `templates/carbone` with document specification key in lower case as a filename (e.g. key = _TEST_DOCUMENT_ - file = _test_document.docx_).

You can create and edit [template](https://carbone.io/documentation.html#building-a-template) using LibreOffice, Microsoft Word, or Google Docs.

or use [Carbon Studio](https://studio.carbone.io/#/studio)
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
rootProject.name=documents
profile=dev
version=2.0.16
version=2.0.17

# Build properties
node_version=12.13.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ public class ApplicationProperties {
private List<String> tenantIgnoredPathList = Collections.emptyList();
private final DocumentGeneration documentGeneration = new DocumentGeneration();

private Carbone carbone = new Carbone();

@Getter
@Setter
public static class Lep {
Expand All @@ -50,6 +52,14 @@ private static class Retry {
public static class DocumentGeneration {
private String specificationPathPattern;
private String jasperTemplatesPathPattern;
private String carboneTemplatesPathPattern;
}

@Getter
@Setter
public static class Carbone {
private String apiVersionKey;
private String apiVersionValue;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.icthh.xm.tmf.ms.document.domain;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import java.util.Map;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({"renderer"})
@Getter
@Setter
@ToString
public class TenantConfigDocumentProperties {

@JsonProperty("renderer")
private Renderer renderer = new Renderer();

@Getter
@Setter
@ToString
public static class Renderer {

@JsonProperty("carbone")
private Carbone carbone;

@Getter
@Setter
@ToString
public static class Carbone {

@JsonProperty("url")
private String url;

@JsonProperty("headers")
private Map<String, String> headers;

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM;
import static org.springframework.http.MediaType.APPLICATION_PDF;
import static org.springframework.http.MediaType.APPLICATION_XML;
import static org.springframework.http.MediaType.IMAGE_PNG;
import static org.springframework.http.MediaType.TEXT_XML;

import java.util.Set;
Expand All @@ -15,7 +16,9 @@
@RequiredArgsConstructor
@Getter
public enum DocumentRendererType {
JASPER_REPORTS(of(APPLICATION_PDF, TEXT_XML, APPLICATION_XML, APPLICATION_OCTET_STREAM, APPLICATION_DOCX));
JASPER_REPORTS(of(APPLICATION_PDF, TEXT_XML, APPLICATION_XML, APPLICATION_OCTET_STREAM, APPLICATION_DOCX)),

CARBONE(of(APPLICATION_PDF, IMAGE_PNG));

private final Set<MediaType> supportedMimeTypes;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package com.icthh.xm.tmf.ms.document.service.generation.rendering.carbone;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.icthh.xm.commons.config.client.service.TenantConfigService;
import com.icthh.xm.tmf.ms.document.config.ApplicationProperties;
import com.icthh.xm.tmf.ms.document.domain.TenantConfigDocumentProperties;
import com.icthh.xm.tmf.ms.document.service.generation.DocumentGenerationSpec;
import com.icthh.xm.tmf.ms.document.service.generation.DocumentRenderer;
import com.icthh.xm.tmf.ms.document.service.generation.DocumentRendererType;
import com.icthh.xm.tmf.ms.document.service.generation.rendering.carbone.dto.AddRenderTemplateRequest;
import com.icthh.xm.tmf.ms.document.service.generation.rendering.carbone.dto.AddRenderTemplateResponse;
import com.icthh.xm.tmf.ms.document.service.generation.rendering.exception.DocumentRenderingException;
import lombok.RequiredArgsConstructor;
v-kyrychenko marked this conversation as resolved.
Show resolved Hide resolved
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.stream.Collectors;

import static java.util.Objects.requireNonNullElse;
import static org.springframework.util.CollectionUtils.toMultiValueMap;

@Slf4j
@Component
@RequiredArgsConstructor
public class CarboneDocumentRenderer implements DocumentRenderer {

private static final String URL_PATH_SEGMENT_RENDER = "render";
private static final String URL_PATH_SEGMENT_TEMPLATE = "template";

private static final String TENANT_CONFIG_KEY = "document";

private final CarboneTemplateHolder templateHolder;
private final TenantConfigService tenantConfigService;
private final RestTemplate vanillaRestTemplate;
private final ApplicationProperties applicationProperties;

private final ObjectMapper objectMapper = new ObjectMapper();

@Override
public byte[] render(String key,
MediaType mediaType,
Object data,
List<DocumentGenerationSpec.SubDocument> subDocuments) throws DocumentRenderingException {
TenantConfigDocumentProperties tenantProperties = resolveTenantProperties();
String template = templateHolder.getTemplateByKey(key);

String baseUrl = tenantProperties.getRenderer().getCarbone().getUrl();
HttpHeaders headers = mapCarboneHeaders(tenantProperties.getRenderer().getCarbone().getHeaders());

AddRenderTemplateRequest requestBody = mapRenderRequestBody(key, mediaType, data, template);
AddRenderTemplateResponse renderResponse = callAddRender(baseUrl, requestBody, headers);

return callGetDocument(baseUrl, renderResponse.getData().getRenderId(), headers);
}

@Override
public DocumentRendererType getType() {
return DocumentRendererType.CARBONE;
}

private TenantConfigDocumentProperties resolveTenantProperties() {
Object properties = tenantConfigService.getConfig().get(TENANT_CONFIG_KEY);
return objectMapper.convertValue(properties, TenantConfigDocumentProperties.class);
}

private HttpHeaders mapCarboneHeaders(Map<String, String> headers) {
headers = enrichWithDefaultCarboneHeaders(headers);

Map<String, List<String>> collect = headers.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, v -> Arrays.asList(v.getValue().split(", "))));

return new HttpHeaders(toMultiValueMap(collect));
}

private Map<String, String> enrichWithDefaultCarboneHeaders(Map<String, String> headers) {
headers = requireNonNullElse(headers, new HashMap<>());
headers.putIfAbsent(
applicationProperties.getCarbone().getApiVersionKey(),
applicationProperties.getCarbone().getApiVersionValue()
);
return headers;
}

private AddRenderTemplateRequest mapRenderRequestBody(String key,
MediaType mediaType,
Object data,
String template) {
var request = objectMapper.convertValue(data, AddRenderTemplateRequest.class);

request.setReportName(requireNonNullElse(request.getConvertTo(), key));
request.setConvertTo(requireNonNullElse(request.getConvertTo(), mediaType.getSubtype()));
request.setTimezone(requireNonNullElse(request.getConvertTo(), TimeZone.getDefault().toZoneId().getId()));

request.setTemplate(template);
return request;
}

private AddRenderTemplateResponse callAddRender(String baseUrl,
AddRenderTemplateRequest requestBody,
HttpHeaders headers) {
String url = collectUrl(baseUrl, URL_PATH_SEGMENT_RENDER, URL_PATH_SEGMENT_TEMPLATE);
HttpEntity<AddRenderTemplateRequest> request = new HttpEntity<>(requestBody, headers);
return vanillaRestTemplate.exchange(url, HttpMethod.POST, request, AddRenderTemplateResponse.class).getBody();
}

private byte[] callGetDocument(String baseUrl, String renderId, HttpHeaders headers) {
String url = collectUrl(baseUrl, URL_PATH_SEGMENT_RENDER, renderId);
HttpEntity<Map<String, Object>> request = new HttpEntity<>(headers);
return vanillaRestTemplate.exchange(url, HttpMethod.GET, request, byte[].class).getBody();
}

private String collectUrl(String baseUrl, String... pathSegments) {
return UriComponentsBuilder.fromHttpUrl(baseUrl).pathSegment(pathSegments).toUriString();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package com.icthh.xm.tmf.ms.document.service.generation.rendering.carbone;

import com.icthh.xm.commons.config.client.api.RefreshableConfiguration;
import com.icthh.xm.commons.tenant.TenantContextHolder;
import com.icthh.xm.commons.tenant.TenantContextUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import static com.icthh.xm.tmf.ms.document.config.Constants.TENANT_NAME;

@Component
@RequiredArgsConstructor
@Slf4j
sbaginskiy marked this conversation as resolved.
Show resolved Hide resolved
public class CarboneTemplateHolder implements RefreshableConfiguration {

private static final String FILENAME_PATTERN_VAR_NAME = "filename";

private final TenantContextHolder tenantContextHolder;

private final Map<String, Map<String, String>> tenantCarboneTemplateMap = new ConcurrentHashMap<>();
private final AntPathMatcher matcher = new AntPathMatcher();

@Value("${application.document-generation.carbone-templates-path-pattern}")
private String carboneTemplatesPathPattern;

/**
* Get JasperReports template bytes by key.
*
* @param key the specification key
* @return bytes of a templates
* @throws IllegalArgumentException if template found by key
*/
public String getTemplateByKey(String key) {
String templateBytes = getTenantTemplates().get(key);
if (templateBytes == null) {
throw new IllegalArgumentException("Template not found for key: " + key);
}
return templateBytes;
}

@Override
public void onInit(String configKey, String configValue) {
onRefresh(configKey, configValue);
}

@Override
public void onRefresh(String key, String config) {
String docKey = extractFromConfigKey(key, FILENAME_PATTERN_VAR_NAME).toUpperCase();

try {
doRefresh(key, config);
} catch (Exception ex) {
log.warn("Unable to process carbone template: {}, error: {}", docKey, ex.getMessage());
}
}

private void doRefresh(String key, String config) {
String docKey = extractFromConfigKey(key, FILENAME_PATTERN_VAR_NAME).toUpperCase();
String tenant = extractFromConfigKey(key, TENANT_NAME);

if (StringUtils.isBlank(config)) {
tenantCarboneTemplateMap.computeIfPresent(tenant, (t, templates) -> {
templates.remove(docKey);
return templates;
});
log.info("Template '{}' for tenant {} was removed", docKey, tenant);
} else {
tenantCarboneTemplateMap.compute(tenant, (t, templates) -> {
templates = templates == null ? new HashMap<>() : templates;
templates.put(docKey, config);
return templates;
});
log.info("Template '{}' for tenant {} was updated", docKey, tenant);
}
}

private Map<String, String> getTenantTemplates() {
String tenantKeyValue = getTenantKeyValue();
Map<String, String> templates = tenantCarboneTemplateMap.get(tenantKeyValue);
if (MapUtils.isEmpty(templates)) {
return Collections.emptyMap();
}
return templates;
}

private String getTenantKeyValue() {
return TenantContextUtils.getRequiredTenantKeyValue(tenantContextHolder);
}

private String extractFromConfigKey(String key, String varName) {
return matcher
.extractUriTemplateVariables(carboneTemplatesPathPattern, key)
.get(varName);
}

@Override
public boolean isListeningConfiguration(String updatedKey) {
return matcher.match(carboneTemplatesPathPattern, updatedKey);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.icthh.xm.tmf.ms.document.service.generation.rendering.carbone.dto;

import lombok.Getter;
import lombok.Setter;

import java.util.Map;

@Getter
@Setter
public class AddRenderTemplateRequest {
private String reportName;
private String convertTo;
private String timezone;
private String template;
private Map<String, Object> data;
}
Loading