diff --git a/webapp/core/bower.json b/webapp/core/bower.json index 098f62e419..582d468f0b 100644 --- a/webapp/core/bower.json +++ b/webapp/core/bower.json @@ -54,7 +54,8 @@ "datatables.net-fixedcolumns-bs": "3.2.2", "hp-autonomy-about-page": "0.3.0", "bootstrap": "3.3.7", - "moment-timezone": "0.5.11" + "moment-timezone": "0.5.11", + "html2canvas": "~0.4.1" }, "devDependencies": { "hp-autonomy-js-testing-utils": "2.2.0", diff --git a/webapp/core/pom.xml b/webapp/core/pom.xml index 4fa9528c15..104a5cbbdd 100644 --- a/webapp/core/pom.xml +++ b/webapp/core/pom.xml @@ -138,6 +138,11 @@ haven-search-components-core ${haven.search.components.version} + + com.hp.autonomy.frontend.reports.powerpoint + powerpoint-report + ${powerpoint.report.version} + org.springframework.boot diff --git a/webapp/core/src/main/java/com/hp/autonomy/frontend/find/core/beanconfiguration/AppConfiguration.java b/webapp/core/src/main/java/com/hp/autonomy/frontend/find/core/beanconfiguration/AppConfiguration.java index 67c2b79485..457b64ba4f 100644 --- a/webapp/core/src/main/java/com/hp/autonomy/frontend/find/core/beanconfiguration/AppConfiguration.java +++ b/webapp/core/src/main/java/com/hp/autonomy/frontend/find/core/beanconfiguration/AppConfiguration.java @@ -19,6 +19,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.web.filter.CharacterEncodingFilter; import org.springframework.web.servlet.LocaleResolver; @@ -65,12 +67,19 @@ public EmbeddedServletContainerCustomizer containerCustomizer() { * since that's apparently what the servlet spec specifies, * It's required despite URIEncoding="UTF-8" on the connector since that only works on GET parameters. * Jetty doesn't have this problem, it seems to use UTF-8 as the default. + * It also has to be a FilterRegistrationBean and be explicitly marked HIGHEST-PRECEDENCE otherwise it'll have no + * effect if other filters run getParameter() before this filter is called. */ @Bean - public CharacterEncodingFilter characterEncodingFilter() { + @Order(Ordered.HIGHEST_PRECEDENCE) + public FilterRegistrationBean characterEncodingFilter() { final CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter(); characterEncodingFilter.setEncoding("UTF-8"); - return characterEncodingFilter; + + final FilterRegistrationBean frb = new FilterRegistrationBean(characterEncodingFilter); + frb.addUrlPatterns("/*"); + + return frb; } @Bean diff --git a/webapp/core/src/main/java/com/hp/autonomy/frontend/find/core/beanconfiguration/TomcatConfig.java b/webapp/core/src/main/java/com/hp/autonomy/frontend/find/core/beanconfiguration/TomcatConfig.java index c65053c7dc..fc9184930e 100644 --- a/webapp/core/src/main/java/com/hp/autonomy/frontend/find/core/beanconfiguration/TomcatConfig.java +++ b/webapp/core/src/main/java/com/hp/autonomy/frontend/find/core/beanconfiguration/TomcatConfig.java @@ -25,6 +25,9 @@ public class TomcatConfig { @Value("${server.tomcat.resources.max-cache-kb}") private long webResourcesCacheSize; + @Value("${server.tomcat.connector.max-post-size}") + private int connectorMaxPostSize; + @Bean public EmbeddedServletContainerFactory servletContainer() { final TomcatEmbeddedServletContainerFactory tomcat = new TomcatEmbeddedServletContainerFactory(); @@ -40,6 +43,10 @@ public EmbeddedServletContainerFactory servletContainer() { context.setResources(resources); }); + tomcat.addConnectorCustomizers(connector -> { + connector.setMaxPostSize(connectorMaxPostSize); + }); + return tomcat; } @@ -47,6 +54,7 @@ private Connector createAjpConnector() { final Connector connector = new Connector("AJP/1.3"); connector.setPort(ajpPort); connector.setAttribute("tomcatAuthentication", false); + connector.setMaxPostSize(connectorMaxPostSize); return connector; } } diff --git a/webapp/core/src/main/java/com/hp/autonomy/frontend/find/core/configuration/FindConfig.java b/webapp/core/src/main/java/com/hp/autonomy/frontend/find/core/configuration/FindConfig.java index 310e9f5e89..b475996f0f 100644 --- a/webapp/core/src/main/java/com/hp/autonomy/frontend/find/core/configuration/FindConfig.java +++ b/webapp/core/src/main/java/com/hp/autonomy/frontend/find/core/configuration/FindConfig.java @@ -31,4 +31,6 @@ public interface FindConfig, B extends FindConfigBuil Integer getTopicMapMaxResults(); B toBuilder(); + + PowerPointConfig getPowerPoint(); } diff --git a/webapp/core/src/main/java/com/hp/autonomy/frontend/find/core/configuration/PowerPointConfig.java b/webapp/core/src/main/java/com/hp/autonomy/frontend/find/core/configuration/PowerPointConfig.java new file mode 100644 index 0000000000..8030866a74 --- /dev/null +++ b/webapp/core/src/main/java/com/hp/autonomy/frontend/find/core/configuration/PowerPointConfig.java @@ -0,0 +1,159 @@ +/* + * Copyright 2015-2017 Hewlett-Packard Enterprise Development Company, L.P. + * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. + */ + +package com.hp.autonomy.frontend.find.core.configuration; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.hp.autonomy.frontend.configuration.ConfigException; +import com.hp.autonomy.frontend.configuration.validation.OptionalConfigurationComponent; +import com.hp.autonomy.frontend.configuration.validation.ValidationResult; +import com.hp.autonomy.frontend.reports.powerpoint.PowerPointServiceImpl; +import com.hp.autonomy.frontend.reports.powerpoint.TemplateLoadException; +import com.hp.autonomy.frontend.reports.powerpoint.TemplateSettingsSource; +import com.hp.autonomy.frontend.reports.powerpoint.dto.Anchor; +import java.io.File; +import java.io.FileInputStream; +import lombok.Data; +import lombok.Setter; +import lombok.experimental.Accessors; +import org.apache.commons.lang.StringUtils; + +import static com.hp.autonomy.frontend.find.core.configuration.PowerPointConfig.Validation.INVALID_MARGINS; + +@JsonDeserialize(builder = PowerPointConfig.Builder.class) +@Data +public class PowerPointConfig implements OptionalConfigurationComponent { + private final String templateFile; + + private final Double marginTop, marginLeft, marginRight, marginBottom; + + private PowerPointConfig(final Builder builder) { + templateFile = builder.templateFile; + marginTop = builder.marginTop; + marginLeft = builder.marginLeft; + marginRight = builder.marginRight; + marginBottom = builder.marginBottom; + } + + @Override + public PowerPointConfig merge(final PowerPointConfig savedSearchConfig) { + return savedSearchConfig != null ? + new PowerPointConfig.Builder() + .setTemplateFile(templateFile == null ? savedSearchConfig.templateFile : templateFile) + .setMarginTop(marginTop == null ? savedSearchConfig.marginTop : marginTop) + .setMarginLeft(marginLeft == null ? savedSearchConfig.marginLeft : marginLeft) + .setMarginRight(marginRight == null ? savedSearchConfig.marginRight : marginRight) + .setMarginBottom(marginBottom == null ? savedSearchConfig.marginBottom : marginBottom) + .build() + : this; + } + + @Override + public void basicValidate(final String section) throws ConfigException { + final ValidationResult result = validate(); + + if (!result.isValid()) { + throw new ConfigException(section, String.valueOf(result.getData())); + } + } + + @JsonIgnore + public Anchor getAnchor() { + final Anchor anchor = new Anchor(); + + final double tmpMarginTop = marginTop == null ? 0 : marginTop; + final double tmpMarginLeft = marginLeft == null ? 0 : marginLeft; + final double tmpMarginRight = marginRight == null ? 0 : marginRight; + final double tmpMarginBottom = marginBottom == null ? 0 : marginBottom; + + anchor.setX(tmpMarginLeft); + anchor.setY(tmpMarginTop); + anchor.setWidth(1 - tmpMarginLeft - tmpMarginRight); + anchor.setHeight(1 - tmpMarginTop - tmpMarginBottom); + + return anchor; + } + + public ValidationResult validate() { + if(rangeIsInvalid(marginTop)) { + return new ValidationResult<>(false, INVALID_MARGINS); + } + if(rangeIsInvalid(marginLeft)) { + return new ValidationResult<>(false, INVALID_MARGINS); + } + if(rangeIsInvalid(marginRight)) { + return new ValidationResult<>(false, INVALID_MARGINS); + } + if(rangeIsInvalid(marginBottom)) { + return new ValidationResult<>(false, INVALID_MARGINS); + } + if(differenceIsInvalid(marginLeft, marginRight)) { + return new ValidationResult<>(false, INVALID_MARGINS); + } + if(differenceIsInvalid(marginTop, marginBottom)) { + return new ValidationResult<>(false, INVALID_MARGINS); + } + + if(StringUtils.isNotBlank(templateFile)) { + final File file = new File(templateFile); + + if (!file.exists()) { + return new ValidationResult<>(false, Validation.TEMPLATE_FILE_NOT_FOUND); + } + + final PowerPointServiceImpl service = new PowerPointServiceImpl(() -> new FileInputStream(file), TemplateSettingsSource.DEFAULT); + + try { + service.validateTemplate(); + } + catch(TemplateLoadException e) { + return new ValidationResult<>(false, Validation.TEMPLATE_INVALID); + } + } + + return new ValidationResult<>(true, null); + } + + private boolean differenceIsInvalid(final Double min, final Double max) { + double realMin = min == null ? 0 : min; + double realMax = max == null ? 0 : max; + if (realMin + realMax >= 1) { + return true; + } + return false; + } + + private static boolean rangeIsInvalid(final Double value) { + return value != null && (value >= 1 || value < 0); + } + + @Override + public Boolean getEnabled() { + return true; + } + + @Setter + @Accessors(chain = true) + @JsonPOJOBuilder(withPrefix = "set") + public static class Builder { + private String templateFile; + private Double marginTop, marginLeft, marginRight, marginBottom; + + public PowerPointConfig build() { + return new PowerPointConfig(this); + } + } + + public enum Validation { + TEMPLATE_FILE_NOT_FOUND, + TEMPLATE_INVALID, + INVALID_MARGINS; + + private Validation() { + } + } +} diff --git a/webapp/core/src/main/java/com/hp/autonomy/frontend/find/core/configuration/PowerPointConfigValidator.java b/webapp/core/src/main/java/com/hp/autonomy/frontend/find/core/configuration/PowerPointConfigValidator.java new file mode 100644 index 0000000000..99fb23afcd --- /dev/null +++ b/webapp/core/src/main/java/com/hp/autonomy/frontend/find/core/configuration/PowerPointConfigValidator.java @@ -0,0 +1,24 @@ +/* + * Copyright 2015-2017 Hewlett-Packard Enterprise Development Company, L.P. + * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. + */ + +package com.hp.autonomy.frontend.find.core.configuration; + +import com.hp.autonomy.frontend.configuration.ConfigException; +import com.hp.autonomy.frontend.configuration.validation.ValidationResult; +import com.hp.autonomy.frontend.configuration.validation.Validator; +import org.springframework.stereotype.Component; + +@Component +public class PowerPointConfigValidator implements Validator { + @Override + public ValidationResult validate(final PowerPointConfig config) { + return config.validate(); + } + + @Override + public Class getSupportedClass() { + return PowerPointConfig.class; + } +} diff --git a/webapp/core/src/main/java/com/hp/autonomy/frontend/find/core/configuration/SamplePowerPointController.java b/webapp/core/src/main/java/com/hp/autonomy/frontend/find/core/configuration/SamplePowerPointController.java new file mode 100644 index 0000000000..95f46a531d --- /dev/null +++ b/webapp/core/src/main/java/com/hp/autonomy/frontend/find/core/configuration/SamplePowerPointController.java @@ -0,0 +1,32 @@ +/* + * Copyright 2017 Hewlett-Packard Enterprise Development Company, L.P. + * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. + */ + +package com.hp.autonomy.frontend.find.core.configuration; + +import com.hp.autonomy.frontend.reports.powerpoint.TemplateSource; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import org.apache.commons.io.IOUtils; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +@Controller +@RequestMapping({"/api/admin/config", "/api/config/config"}) +public class SamplePowerPointController { + + @RequestMapping(value = "/template.pptx", method = RequestMethod.GET) + public HttpEntity template() throws IOException { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + IOUtils.copyLarge(TemplateSource.DEFAULT.getInputStream(), baos); + final HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.presentationml.presentation")); + headers.set("Content-Disposition", "inline; filename=template.pptx"); + return new HttpEntity<>(baos.toByteArray(), headers); + } +} diff --git a/webapp/core/src/main/java/com/hp/autonomy/frontend/find/core/export/ExportController.java b/webapp/core/src/main/java/com/hp/autonomy/frontend/find/core/export/ExportController.java index c14d389065..b4e4e733d8 100644 --- a/webapp/core/src/main/java/com/hp/autonomy/frontend/find/core/export/ExportController.java +++ b/webapp/core/src/main/java/com/hp/autonomy/frontend/find/core/export/ExportController.java @@ -5,11 +5,32 @@ package com.hp.autonomy.frontend.find.core.export; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.hp.autonomy.frontend.configuration.ConfigService; +import com.hp.autonomy.frontend.find.core.configuration.FindConfig; +import com.hp.autonomy.frontend.find.core.configuration.PowerPointConfig; import com.hp.autonomy.frontend.find.core.web.ControllerUtils; import com.hp.autonomy.frontend.find.core.web.ErrorModelAndViewInfo; import com.hp.autonomy.frontend.find.core.web.RequestMapper; +import com.hp.autonomy.frontend.reports.powerpoint.PowerPointService; +import com.hp.autonomy.frontend.reports.powerpoint.PowerPointServiceImpl; +import com.hp.autonomy.frontend.reports.powerpoint.TemplateLoadException; +import com.hp.autonomy.frontend.reports.powerpoint.TemplateSettings; +import com.hp.autonomy.frontend.reports.powerpoint.TemplateSettingsSource; +import com.hp.autonomy.frontend.reports.powerpoint.TemplateSource; +import com.hp.autonomy.frontend.reports.powerpoint.dto.DategraphData; +import com.hp.autonomy.frontend.reports.powerpoint.dto.ListData; +import com.hp.autonomy.frontend.reports.powerpoint.dto.MapData; +import com.hp.autonomy.frontend.reports.powerpoint.dto.ReportData; +import com.hp.autonomy.frontend.reports.powerpoint.dto.SunburstData; +import com.hp.autonomy.frontend.reports.powerpoint.dto.TableData; +import com.hp.autonomy.frontend.reports.powerpoint.dto.TopicMapData; import com.hp.autonomy.searchcomponents.core.search.QueryRequest; +import java.io.FileInputStream; import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.poi.xslf.usermodel.XMLSlideShow; +import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -33,16 +54,47 @@ public abstract class ExportController, E extends Exception> { static final String EXPORT_PATH = "/api/bi/export"; static final String CSV_PATH = "/csv"; + static final String PPT_TOPICMAP_PATH = "/ppt/topicmap"; + static final String PPT_SUNBURST_PATH = "/ppt/sunburst"; + static final String PPT_TABLE_PATH = "/ppt/table"; + static final String PPT_MAP_PATH = "/ppt/map"; + static final String PPT_LIST_PATH = "/ppt/list"; + static final String PPT_DATEGRAPH_PATH = "/ppt/dategraph"; + static final String PPT_REPORT_PATH = "/ppt/report"; static final String SELECTED_EXPORT_FIELDS_PARAM = "selectedFieldIds"; static final String QUERY_REQUEST_PARAM = "queryRequest"; private static final String EXPORT_FILE_NAME = "query-results"; private final RequestMapper requestMapper; private final ControllerUtils controllerUtils; + private final ObjectMapper objectMapper; + + private final PowerPointService pptService; protected ExportController(final RequestMapper requestMapper, - final ControllerUtils controllerUtils) { + final ControllerUtils controllerUtils, + final ObjectMapper objectMapper, + final ConfigService configService) { this.requestMapper = requestMapper; this.controllerUtils = controllerUtils; + this.objectMapper = objectMapper; + + this.pptService = new PowerPointServiceImpl(() -> { + final PowerPointConfig powerPoint = configService.getConfig().getPowerPoint(); + + if (powerPoint != null) { + final String template = powerPoint.getTemplateFile(); + + if (StringUtils.isNotBlank(template)) { + return new FileInputStream(template); + } + } + + return TemplateSource.DEFAULT.getInputStream(); + }, () -> { + final PowerPointConfig powerPoint = configService.getConfig().getPowerPoint(); + + return powerPoint == null ? TemplateSettingsSource.DEFAULT.getSettings() : new TemplateSettings(powerPoint.getAnchor()); + }); } @RequestMapping(value = CSV_PATH, method = RequestMethod.POST) @@ -98,4 +150,96 @@ private ResponseEntity outputStreamToResponseEntity(final ExportFormat e return new ResponseEntity<>(output, headers, HttpStatus.OK); } -} + + @RequestMapping(value = PPT_TOPICMAP_PATH, method = RequestMethod.POST) + public HttpEntity topicmap( + @RequestParam("data") final String topicMapStr + ) throws IOException, TemplateLoadException { + final TopicMapData data = objectMapper.readValue(topicMapStr, TopicMapData.class); + return writePPT(pptService.topicmap(data), "topicmap.pptx"); + } + + private HttpEntity writePPT(final XMLSlideShow ppt, final String filename) throws IOException { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ppt.write(baos); + ppt.close(); + + final HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.presentationml.presentation")); + headers.set("Content-Disposition", "inline; filename=" + filename); + return new HttpEntity<>(baos.toByteArray(), headers); + } + + @RequestMapping(value = PPT_SUNBURST_PATH, method = RequestMethod.POST) + public HttpEntity sunburst( + @RequestParam("data") final String dataStr + ) throws IOException, TemplateLoadException { + final SunburstData data = objectMapper.readValue(dataStr, SunburstData.class); + + final XMLSlideShow ppt = pptService.sunburst(data); + + return writePPT(ppt, "sunburst.pptx"); + } + + @RequestMapping(value = PPT_TABLE_PATH, method = RequestMethod.POST) + public HttpEntity table( + @RequestParam("title") final String title, + @RequestParam("data") final String dataStr + + ) throws IOException, TemplateLoadException { + final TableData tableData = objectMapper.readValue(dataStr, TableData.class); + + final XMLSlideShow ppt = pptService.table(tableData, title); + + return writePPT(ppt, "table.pptx"); + } + + @RequestMapping(value = PPT_MAP_PATH, method = RequestMethod.POST) + public HttpEntity map( + @RequestParam("title") final String title, + @RequestParam("data") final String markerStr + ) throws IOException, TemplateLoadException { + final MapData map = objectMapper.readValue(markerStr, MapData.class); + + final XMLSlideShow ppt = pptService.map(map, title); + + return writePPT(ppt, "map.pptx"); + } + + @RequestMapping(value = PPT_LIST_PATH, method = RequestMethod.POST) + public HttpEntity list( + @RequestParam("results") final String results, + @RequestParam("sortBy") final String sortBy, + @RequestParam("data") final String docsStr + ) throws IOException, TemplateLoadException { + final ListData documentList = objectMapper.readValue(docsStr, ListData.class); + + final XMLSlideShow ppt = pptService.list(documentList, results, sortBy); + + return writePPT(ppt, "list.pptx"); + } + + @RequestMapping(value = PPT_DATEGRAPH_PATH, method = RequestMethod.POST) + public HttpEntity graph( + @RequestParam("data") final String dataStr + ) throws IOException, TemplateLoadException { + final DategraphData data = objectMapper.readValue(dataStr, DategraphData.class); + + final XMLSlideShow ppt = pptService.graph(data); + + return writePPT(ppt, "dategraph.pptx"); + } + + @RequestMapping(value = PPT_REPORT_PATH, method = RequestMethod.POST) + public HttpEntity report( + @RequestParam("data") final String dataStr, + @RequestParam(value = "multipage", defaultValue = "false") final boolean multipage + ) throws IOException, TemplateLoadException { + final ReportData report = objectMapper.readValue(dataStr, ReportData.class); + + final XMLSlideShow ppt = pptService.report(report, multipage); + + return writePPT(ppt, "report.pptx"); + } + +} \ No newline at end of file diff --git a/webapp/core/src/main/java/com/hp/autonomy/frontend/find/core/map/MapController.java b/webapp/core/src/main/java/com/hp/autonomy/frontend/find/core/map/MapController.java new file mode 100644 index 0000000000..02bfbf3d0b --- /dev/null +++ b/webapp/core/src/main/java/com/hp/autonomy/frontend/find/core/map/MapController.java @@ -0,0 +1,68 @@ +/* + * Copyright 2017 Hewlett-Packard Development Company, L.P. + * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. + */ + +package com.hp.autonomy.frontend.find.core.map; + +import com.hp.autonomy.frontend.configuration.ConfigService; +import com.hp.autonomy.frontend.find.core.configuration.FindConfig; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.io.IOUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +@Controller +@RequestMapping(MapController.MAP_PATH) +public class MapController { + public static final String MAP_PATH = "/api/public/map"; + public static final String PROXY_PATH = "/proxy"; + + @Autowired + public MapController(final ConfigService configService) { + this.configService = configService; + } + + private final ConfigService configService; + + @RequestMapping(value = PROXY_PATH, method = RequestMethod.GET) + @ResponseBody + public String tile( + @RequestParam("url") final String url, + @RequestParam("callback") final String callback + ) { + if(!callback.matches("\\w+")) { + throw new IllegalArgumentException("Invalid callback function name"); + } + + try { + final String tileUrlTemplate = configService.getConfig().getMap().getTileUrlTemplate(); + + final URL target = new URL(url), validate = new URL(tileUrlTemplate); + + if (!validate.getProtocol().equals(target.getProtocol()) || !validate.getHost().equals(target.getHost()) || validate.getPort() != target.getPort()) { + throw new IllegalArgumentException("We only allow proxying to the tile server"); + } + + final URLConnection urlConnection = target.openConnection(); + final String contentType = urlConnection.getContentType(); + try (final InputStream is = urlConnection.getInputStream(); final ByteArrayOutputStream baos = new ByteArrayOutputStream();) { + IOUtils.copyLarge(is, baos); + + return callback + "(\"data:" + contentType + ";base64," + new String(Base64.encodeBase64(baos.toByteArray(), false, false)) + "\")"; + } + } + catch(IOException e) { + return callback + "(\"error:Application error\")"; + } + } +} \ No newline at end of file diff --git a/webapp/core/src/main/less/app-include.less b/webapp/core/src/main/less/app-include.less index 5e0d931cc5..7a31c4b669 100644 --- a/webapp/core/src/main/less/app-include.less +++ b/webapp/core/src/main/less/app-include.less @@ -2019,6 +2019,7 @@ h4.similar-dates-message { &:hover { transform: scale(1.015); transition: 0.3s; + z-index: 1; } .title { @@ -2254,3 +2255,76 @@ h4.similar-dates-message { height: @lgHeightScreenTopicMapHeight; } } + +.results-view-pptx { + margin-left: 5px; +} + +.report-pptx-group { + position: absolute; + z-index: 2; + top: 0; + right: 0; + opacity: 0.1; + transition: 0.5s opacity; +} + +.report-pptx-group:hover { + opacity: 1; +} + +.powerpoint-margins { + position: relative; + border: 1px lightgray solid; + width: 250px; + height: 140px; + margin-bottom: 5px; +} + +.powerpoint-margins-docbox { + font-size: 54px; + position: absolute; + left: 0; + right: 0; + top: 25%; + bottom: 0; + text-align: center; +} + +.powerpoint-margins > .template-input-margin { + position: absolute; + width: 70px; +} + +.template-input-marginTop { + left: 50%; + top: 2px; + text-align: center; + margin-left: -35px; +} + +.template-input-marginBottom { + left: 50%; + bottom: 2px; + text-align: center; + margin-left: -35px; +} + +.template-input-marginLeft { + left: 2px; + top: 50%; + transform: translateY(-50%); + text-align: right; +} + +.template-input-marginRight { + right: 2px; + top: 50%; + transform: translateY(-50%); + text-align: left; +} + +.powerpoint-margins-indicator { + position:absolute; + border: 1px solid #60798d; +} \ No newline at end of file diff --git a/webapp/core/src/main/public/static/js/find/app/page/abstract-find-settings-page.js b/webapp/core/src/main/public/static/js/find/app/page/abstract-find-settings-page.js index 6275a2abd2..04a38b5532 100644 --- a/webapp/core/src/main/public/static/js/find/app/page/abstract-find-settings-page.js +++ b/webapp/core/src/main/public/static/js/find/app/page/abstract-find-settings-page.js @@ -30,6 +30,7 @@ define([ urlRoot: urlRoot, vent: vent, validateUrl: urlRoot + 'config-validation', + pptxTemplateUrl: urlRoot + 'template.pptx', widgetGroupParent: 'form .row', strings: { @@ -79,6 +80,11 @@ define([ validatePortInvalid: i18n['settings.test.portInvalid'], validateUsernameBlank: i18n['settings.test.usernameBlank'], validateSuccess: i18n['settings.test.ok'], + templateFile: i18n['settings.powerpoint.template.file'], + templateFileDefault: i18n['settings.powerpoint.template.file.default'], + templateSampleDownload: i18n['settings.powerpoint.template.sample.download'], + templateValidate: i18n['settings.powerpoint.template.file.validate'], + templateMargins: i18n['settings.powerpoint.template.margins'], CONNECTION_ERROR: i18n['settings.CONNECTION_ERROR'], FETCH_PORT_ERROR: i18n['settings.FETCH_PORT_ERROR'], FETCH_SERVICE_PORT_ERROR: i18n['settings.FETCH_SERVICE_PORT_ERROR'], @@ -87,7 +93,10 @@ define([ REQUIRED_FIELD_MISSING: i18n['settings.REQUIRED_FIELD_MISSING'], REGULAR_EXPRESSION_MATCH_ERROR: i18n['settings.REGULAR_EXPRESSION_MATCH_ERROR'], SERVICE_AND_INDEX_PORT_ERROR: i18n['settings.SERVICE_AND_INDEX_PORT_ERROR'], - SERVICE_PORT_ERROR: i18n['settings.SERVICE_PORT_ERROR'] + SERVICE_PORT_ERROR: i18n['settings.SERVICE_PORT_ERROR'], + TEMPLATE_FILE_NOT_FOUND: i18n['settings.powerpoint.template.error.TEMPLATE_FILE_NOT_FOUND'], + TEMPLATE_INVALID: i18n['settings.powerpoint.template.error.TEMPLATE_INVALID'], + INVALID_MARGINS: i18n['settings.powerpoint.template.error.INVALID_MARGINS'] }; }, diff --git a/webapp/core/src/main/public/static/js/find/app/page/search/results/entity-topic-map-view.js b/webapp/core/src/main/public/static/js/find/app/page/search/results/entity-topic-map-view.js index 9e9af7c297..c60af65b49 100644 --- a/webapp/core/src/main/public/static/js/find/app/page/search/results/entity-topic-map-view.js +++ b/webapp/core/src/main/public/static/js/find/app/page/search/results/entity-topic-map-view.js @@ -46,9 +46,35 @@ define([ var maxResults = event.value; this.model.set('maxResults', maxResults); + }, + 'click .entity-topic-map-pptx': function(evt){ + evt.preventDefault() + + var data = this.exportPPTData(); + + if (data) { + // We need to append the temporary form to the document.body or Firefox and IE11 won't download the file. + // Previously used GET; but IE11 has a limited GET url length and loses data. + var $form = $('
'); + $form[0].data.value = JSON.stringify(data) + $form.appendTo(document.body).submit().remove() + } } }, + exportPPTData: function(){ + var paths = this.topicMap.exportPaths(); + + paths.forEach(function(polygons, idx){ + var depthFromLeaf = paths.length - 1 - idx; + polygons.forEach(function(path){ path.level = depthFromLeaf }) + }) + + return paths ? { + paths: _.flatten(paths.slice(1).reverse()) + } : null + }, + initialize: function(options) { this.queryState = options.queryState; @@ -152,6 +178,7 @@ define([ this.handleTopicMapError(); this.$('.entity-topic-map-empty').toggleClass('hide', state !== ViewState.EMPTY); this.$('.entity-topic-map-loading').toggleClass('hide', state !== ViewState.LOADING); + this.$('.entity-topic-map-pptx').toggleClass('disabled', state !== ViewState.MAP); }, generateErrorMessage: function(xhr) { diff --git a/webapp/core/src/main/public/static/js/find/app/page/search/results/map-results-view.js b/webapp/core/src/main/public/static/js/find/app/page/search/results/map-results-view.js index 961edae71a..9d128afad5 100644 --- a/webapp/core/src/main/public/static/js/find/app/page/search/results/map-results-view.js +++ b/webapp/core/src/main/public/static/js/find/app/page/search/results/map-results-view.js @@ -29,6 +29,10 @@ define([ }, 'click .map-popup-title': function (e) { vent.navigateToDetailRoute(this.documentsCollection.get(e.currentTarget.getAttribute('cid'))); + }, + 'click .map-pptx': function(e){ + e.preventDefault(); + this.mapResultsView.exportPPT('Showing field ' + this.fieldSelectionView.model.get('displayName')) } }, diff --git a/webapp/core/src/main/public/static/js/find/app/page/search/results/map-view.js b/webapp/core/src/main/public/static/js/find/app/page/search/results/map-view.js index 3a39cd76d1..d891a93c05 100644 --- a/webapp/core/src/main/public/static/js/find/app/page/search/results/map-view.js +++ b/webapp/core/src/main/public/static/js/find/app/page/search/results/map-view.js @@ -6,13 +6,20 @@ define([ 'find/app/vent', 'leaflet', 'Leaflet.awesome-markers', - 'leaflet.markercluster' - + 'leaflet.markercluster', + 'html2canvas' ], function (Backbone, _, $, configuration, vent, leaflet) { 'use strict'; var INITIAL_ZOOM = 3; + var leafletMarkerColorMap = { + 'green': '#70ad25', + 'orange': '#f0932f', + 'red': '#d33d2a', + 'blue': '#37a8da' + } + return Backbone.View.extend({ initialize: function (options) { this.addControl = options.addControl || false; @@ -148,6 +155,127 @@ define([ if (this.map) { this.map.remove(); } + }, + + exportPPTData: function() { + var deferred = $.Deferred(); + + var map = this.map, + mapSize = map.getSize(), + $mapEl = $(map.getContainer()), + markers = []; + + function lPad(str) { + return str.length < 2 ? '0' + str : str + } + + function hexColor(str){ + var match; + if (match = /rgba\((\d+),\s*(\d+),\s*(\d+),\s*([0-9.]+)\)/.exec(str)) { + return '#' + lPad(Number(match[1]).toString(16)) + + lPad(Number(match[2]).toString(16)) + + lPad(Number(match[3]).toString(16)) + } + else if (match = /rgb\((\d+),\s*(\d+),\s*(\d+)\)/.exec(str)) { + return '#' + lPad(Number(match[1]).toString(16)) + + lPad(Number(match[2]).toString(16)) + + lPad(Number(match[3]).toString(16)) + } + return str + } + + map.eachLayer(function(layer){ + if (layer instanceof leaflet.Marker) { + var pos = map.latLngToContainerPoint(layer.getLatLng()) + + var isCluster = layer.getChildCount + + var xFraction = pos.x / mapSize.x; + var yFraction = pos.y / mapSize.y; + var tolerance = 0.001; + + if (xFraction > -tolerance && xFraction < 1 + tolerance && yFraction > -tolerance && yFraction < 1 + tolerance) { + var fontColor = '#000000', + color = '#37a8da', + match, + fade = false, + text = ''; + + var $iconEl = $(layer._icon); + if (isCluster) { + color = hexColor($iconEl.css('background-color')); + fontColor = hexColor($iconEl.children('div').css('color')) + fade = +$iconEl.css('opacity') < 1 + text = layer.getChildCount(); + } else if (match=/awesome-marker-icon-(\w+)/.exec(layer._icon.classList)) { + if (leafletMarkerColorMap.hasOwnProperty(match[1])) { + color = leafletMarkerColorMap[match[1]] + } + + var popup = layer.getPopup(); + if (popup && popup._content) { + text = $(popup._content).find('.map-popup-title').text() + } + } + + var marker = { + x: xFraction, + y: yFraction, + text: text, + cluster: !!isCluster, + color: color, + fontColor: fontColor, + fade: fade, + z: +$iconEl.css('z-index') + }; + + markers.push(marker) + } + } + }) + + var $objs = $mapEl.find('.leaflet-objects-pane').addClass('hide') + + html2canvas($mapEl, { + // This seems to avoid issues with IE11 only rendering a small portion of the map the size of the window + // If width and height are undefined, Firefox sometimes renders black areas. + // If width and height are equal to the $mapEl.width()/height(), then Chrome has the same problem as IE11. + width: $(document).width(), + height: $(document).height(), + proxy: 'api/public/map/proxy', + useCORS: true, + onrendered: function(canvas) { + $objs.removeClass('hide') + + deferred.resolve({ + // ask for lossless PNG image + image: canvas.toDataURL('image/png'), + markers: markers.sort(function(a, b){ + return a.z - b.z; + }).map(function(a){ + return _.omit(a, 'z') + }) + }); + } + }); + + return deferred.promise(); + }, + + exportPPT: function(title){ + this.exportPPTData().done(function(data){ + // We use a textarea for the title so we can have newlines, and a textarea for the image to work + // around a hard 524288 limit imposed by a WebKit bug (affects Chrome 55). + // See https://bugs.webkit.org/show_bug.cgi?id=44883 + // We open in _self (despite the chance of having errors) since otherwise the popup blocker + /// will block it, since it's a javascript object which doesn't originate directly from a user event. + var $form = $('
'); + $form[0].title.value = title + + $form[0].data.value = JSON.stringify(data) + + $form.appendTo(document.body).submit().remove() + }) } }); }); diff --git a/webapp/core/src/main/public/static/js/find/app/page/search/results/parametric-results-view.js b/webapp/core/src/main/public/static/js/find/app/page/search/results/parametric-results-view.js index 45959c56ae..456d85e323 100644 --- a/webapp/core/src/main/public/static/js/find/app/page/search/results/parametric-results-view.js +++ b/webapp/core/src/main/public/static/js/find/app/page/search/results/parametric-results-view.js @@ -80,6 +80,7 @@ define([ this.$message = this.$('.parametric-view-message'); this.$errorMessage = this.$('.parametric-view-error-message'); this.$parametricSelections = this.$('.parametric-selections').addClass('hide'); + this.$pptxButton = this.$('.parametric-pptx'); this.listenTo(this.fieldsCollection.at(0), 'change:field', this.secondSelection); this.listenTo(this.model, 'change:loading', this.toggleLoading); @@ -114,6 +115,10 @@ define([ this.$content.toggleClass('invisible', loading); this.$parametricSelections.toggleClass('hide', this.noMoreParametricFields()); this.updateMessage(); + + if (loading) { + this.$pptxButton.addClass('disabled'); + } }, swapFields: function() { diff --git a/webapp/core/src/main/public/static/js/find/app/page/search/results/results-number-view.js b/webapp/core/src/main/public/static/js/find/app/page/search/results/results-number-view.js index 62d80ea328..bab51d3895 100644 --- a/webapp/core/src/main/public/static/js/find/app/page/search/results/results-number-view.js +++ b/webapp/core/src/main/public/static/js/find/app/page/search/results/results-number-view.js @@ -39,6 +39,10 @@ define([ this.$totalNumber.text(this.documentsCollection.totalResults || 0); this.$firstNumber.text(this.documentsCollection.length ? 1 : 0); } + }, + + getText: function() { + return $.trim(this.$el.text().replace(/\s+/g, ' ')); } }); }); diff --git a/webapp/core/src/main/public/static/js/find/app/page/search/results/results-view.js b/webapp/core/src/main/public/static/js/find/app/page/search/results/results-view.js index d931731a25..768521192e 100644 --- a/webapp/core/src/main/public/static/js/find/app/page/search/results/results-view.js +++ b/webapp/core/src/main/public/static/js/find/app/page/search/results/results-view.js @@ -70,6 +70,28 @@ define([ documentModel = this.promotionsCollection.get(cid); } vent.navigateToSuggestRoute(documentModel); + }, + 'click .results-view-pptx': function(evt) { + evt.preventDefault(); + + var $form = $('
'); + + $form[0].sortBy.value = this.sortView.getText(); + $form[0].results.value = this.resultsNumberView.getText(); + + $form[0].data.value = JSON.stringify({ + docs: this.documentsCollection.map(function(model){ + return { + title: model.get('title'), + date: model.has('date') ? model.get('date').fromNow() : '', + ref: model.get('reference'), + summary: model.get('summary'), + thumbnail: model.get('thumbnail') + } + }) + }) + + $form.appendTo(document.body).submit().remove() } }; @@ -152,6 +174,10 @@ define([ this.sortView.setElement(this.$('.sort-container')).render(); this.resultsNumberView.setElement(this.$('.results-number-container')).render(); + if (!_.contains(configuration().roles, 'ROLE_BI')) { + this.$('.results-view-pptx').addClass('hide'); + } + if(this.questionsView) { this.questionsView.setElement(this.$('.main-results-content .answered-questions')).render(); } @@ -177,6 +203,8 @@ define([ this.listenTo(this.documentsCollection, 'add', function(model) { this.formatResult(model, false); + + this.$('.results-view-pptx').removeClass('disabled'); }); this.listenTo(this.documentsCollection, 'sync reset', function() { @@ -190,6 +218,8 @@ define([ } else if(this.documentsCollection.isEmpty()) { this.$('.main-results-content .results').append(this.messageTemplate({message: i18n["search.noResults"]})); } + + this.$('.results-view-pptx').toggleClass('disabled', this.documentsCollection.isEmpty()); }); this.listenTo(this.documentsCollection, 'error', function(collection, xhr) { diff --git a/webapp/core/src/main/public/static/js/find/app/page/search/results/sunburst-view.js b/webapp/core/src/main/public/static/js/find/app/page/search/results/sunburst-view.js index 0770560fc4..9a3068fabd 100644 --- a/webapp/core/src/main/public/static/js/find/app/page/search/results/sunburst-view.js +++ b/webapp/core/src/main/public/static/js/find/app/page/search/results/sunburst-view.js @@ -111,6 +111,37 @@ define([ } return ParametricResultsView.extend({ + + events: _.extend({ + 'click .parametric-pptx': function(evt) { + evt.preventDefault(); + + var data = this.exportPPTData(); + + if (data) { + var $form = $('
'); + $form[0].data.value = JSON.stringify(data); + $form.appendTo(document.body).submit().remove(); + } + } + }, ParametricResultsView.prototype.events), + + exportPPTData: function(){ + var categories = []; + var values = []; + + this.dependentParametricCollection.each(function(model){ + categories.push(model.get('text') || i18n['search.resultsView.sunburst.others']); + values.push(model.get('count')); + }); + + return values.length && categories.length ? { + categories: categories, + values: values, + title: i18n['search.resultsView.sunburst.breakdown.by'](this.fieldsCollection.at(0).get('displayValue')) + } : null + }, + initialize: function(options) { ParametricResultsView.prototype.initialize.call(this, _.defaults({ emptyDependentMessage: i18n['search.resultsView.sunburst.error.noDependentParametricValues'], @@ -127,6 +158,8 @@ define([ }, update: function() { + var disableExport = true; + if(!this.parametricCollection.isEmpty()) { const data = generateDataRoot(this.dependentParametricCollection.toJSON()); @@ -151,8 +184,11 @@ define([ this.$message.text(i18n['search.resultsView.sunburst.error.noSecondFieldValues']); } else { this.$message.empty(); + disableExport = false; } } + + this.$pptxButton.toggleClass('disabled', disableExport); }, render: function() { diff --git a/webapp/core/src/main/public/static/js/find/app/page/search/results/table/table-view.js b/webapp/core/src/main/public/static/js/find/app/page/search/results/table/table-view.js index 450213f704..d02645bccc 100644 --- a/webapp/core/src/main/public/static/js/find/app/page/search/results/table/table-view.js +++ b/webapp/core/src/main/public/static/js/find/app/page/search/results/table/table-view.js @@ -31,6 +31,43 @@ define([ tableTemplate: _.template(tableTemplate), + events: _.extend({ + 'click .parametric-pptx': function(evt) { + evt.preventDefault(); + + var data = this.exportPPTData(); + + if (data) { + var $form = $('
'); + $form[0].title.value = i18n['search.resultsView.table.breakdown.by'](this.fieldsCollection.at(0).get('displayValue')); + $form[0].data.value = JSON.stringify(data); + $form.appendTo(document.body).submit().remove(); + } + }, + + }, ParametricResultsView.prototype.events), + + exportPPTData: function(){ + var rows = this.$table.find('tr'), nCols = 0; + + var cells = []; + + rows.each(function(idx, el){ + var tds = $(el).find('th,td'); + nCols = tds.length; + + tds.each(function (idx, el) { + cells.push($(el).text()); + }) + }); + + return rows.length ? { + rows: rows.length, + cols: nCols, + cells: cells + } : null; + }, + initialize: function(options) { ParametricResultsView.prototype.initialize.call(this, _.defaults({ dependentParametricCollection: new TableCollection(), @@ -41,7 +78,7 @@ define([ }, render: function() { - ParametricResultsView.prototype.render.apply(this); + ParametricResultsView.prototype.render.apply(this, arguments); this.$content.html(this.tableTemplate()); @@ -58,6 +95,8 @@ define([ // if parametric collection is empty then nothing has loaded and datatables will fail if(!this.parametricCollection.isEmpty()) { + this.$pptxButton.removeClass('disabled'); + // columnNames will be empty if only one field is selected if(_.isEmpty(this.dependentParametricCollection.columnNames)) { this.$table.dataTable({ diff --git a/webapp/core/src/main/public/static/js/find/app/page/search/sort-view.js b/webapp/core/src/main/public/static/js/find/app/page/search/sort-view.js index dbafcb4770..2f5cdcae30 100644 --- a/webapp/core/src/main/public/static/js/find/app/page/search/sort-view.js +++ b/webapp/core/src/main/public/static/js/find/app/page/search/sort-view.js @@ -42,6 +42,13 @@ define([ if(this.$currentSort) { this.$currentSort.text(i18n['search.resultsSort.' + this.queryModel.get('sort')]); } + }, + + getText: function() { + if (this.$currentSort) { + return i18n['search.resultsSort'] + ' ' + i18n['search.resultsSort.' + this.queryModel.get('sort')]; + } + return ''; } }); }); diff --git a/webapp/core/src/main/public/static/js/find/app/page/settings/powerpoint-widget.js b/webapp/core/src/main/public/static/js/find/app/page/settings/powerpoint-widget.js new file mode 100644 index 0000000000..a7e0bfcc2d --- /dev/null +++ b/webapp/core/src/main/public/static/js/find/app/page/settings/powerpoint-widget.js @@ -0,0 +1,107 @@ +/* + * Copyright 2016 Hewlett-Packard Enterprise Development Company, L.P. + * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. + */ + +define([ + 'jquery', + 'settings/js/widget', + 'text!find/templates/app/page/settings/widget.html', + 'text!find/templates/app/page/settings/powerpoint-widget.html', + 'underscore' +], function($, Widget, widgetTemplate, template, _) { + 'use strict'; + + return Widget.extend({ + widgetTemplate: _.template(widgetTemplate), + template: _.template(template), + + className: 'panel-group', + controlGroupClass: 'form-group', + formControlClass: 'form-control', + errorClass: 'has-error', + successClass: 'has-success', + + events: _.extend({ + 'click button[name=validate]': 'triggerValidation', + 'change .template-input-margin': 'updateMarginIndicator', + 'input .template-input-margin': 'updateMarginIndicator' + }, Widget.prototype.events), + + initialize: function(options) { + Widget.prototype.initialize.apply(this, arguments); + this.pptxTemplateUrl = options.pptxTemplateUrl; + }, + + render: function() { + Widget.prototype.render.apply(this, arguments); + + this.$content.html(this.template({ + strings: this.strings, + pptxTemplateUrl: this.pptxTemplateUrl + })); + + this.$templateFile = this.$('.template-file-input'); + this.$validity = this.$('.settings-client-validation'); + + this.$marginLeft = this.$('.template-input-marginLeft'); + this.$marginTop = this.$('.template-input-marginTop'); + this.$marginRight = this.$('.template-input-marginRight'); + this.$marginBottom = this.$('.template-input-marginBottom'); + + this.$marginIndicator = this.$('.powerpoint-margins-indicator'); + }, + + handleValidation: function(config, response) { + if (response.valid) { + this.setValidationFormatting(this.successClass); + this.hideValidationInfo(); + } else { + this.setValidationFormatting(this.errorClass); + this.$validity.text(this.strings[response.data]) + .stop() + .animate({opacity: 1}) + .removeClass('hide'); + } + }, + + triggerValidation: function() { + this.setValidationFormatting('clear'); + this.hideValidationInfo(); + + if (this.validateInputs()) { + this.trigger('validate'); + } + }, + + getConfig: function() { + return { + templateFile: this.$templateFile.val(), + marginLeft: this.$marginLeft.val(), + marginTop: this.$marginTop.val(), + marginRight: this.$marginRight.val(), + marginBottom: this.$marginBottom.val() + } + }, + + updateConfig: function(config) { + if (config) { + this.$templateFile.val(config.templateFile); + this.$marginLeft.val(config.marginLeft); + this.$marginTop.val(config.marginTop); + this.$marginRight.val(config.marginRight); + this.$marginBottom.val(config.marginBottom); + this.updateMarginIndicator(); + } + }, + + updateMarginIndicator: function() { + this.$marginIndicator.css({ + left: 100 * Math.min(1, Math.max(0, this.$marginLeft.val())) + '%', + top: 100 * Math.min(1, Math.max(0, this.$marginTop.val())) + '%', + right: 100 * Math.min(1, Math.max(0, this.$marginRight.val())) + '%', + bottom: 100 * Math.min(1, Math.max(0, this.$marginBottom.val())) + '%' + }) + } + }); +}); diff --git a/webapp/core/src/main/public/static/js/find/app/util/topic-map-view.js b/webapp/core/src/main/public/static/js/find/app/util/topic-map-view.js index 30080b48c9..38bdb490d8 100644 --- a/webapp/core/src/main/public/static/js/find/app/util/topic-map-view.js +++ b/webapp/core/src/main/public/static/js/find/app/util/topic-map-view.js @@ -74,6 +74,10 @@ define([ size: 1.0, children: this.data }); + }, + + exportPaths: function(){ + return this.$el.topicmap('exportPaths'); } }); }); diff --git a/webapp/core/src/main/public/static/js/find/nls/root/bundle.js b/webapp/core/src/main/public/static/js/find/nls/root/bundle.js index 52c010a096..97af59b0c8 100644 --- a/webapp/core/src/main/public/static/js/find/nls/root/bundle.js +++ b/webapp/core/src/main/public/static/js/find/nls/root/bundle.js @@ -89,6 +89,10 @@ define([ 'placeholder.hostname': 'hostname', 'placeholder.ip': 'IP', 'placeholder.port': 'port', + 'powerpoint.export.single': 'Single slide', + 'powerpoint.export.multiple': 'Multislide', + 'powerpoint.export.labels': 'Labels', + 'powerpoint.export.padding': 'Padding', 'search.answeredQuestion.question': 'Question: ', 'search.answeredQuestion.answer': 'Answer: ', 'search.alsoSearchingFor': 'Also searching for', @@ -191,6 +195,8 @@ define([ 'search.resultsView.sunburst.error.query': 'Error: could not display Sunburst View', 'search.resultsView.sunburst.error.noDependentParametricValues': 'There are too many parametric fields to display in Sunburst View', 'search.resultsView.sunburst.error.noSecondFieldValues': 'There are no documents with values for both fields. Showing results for only first field.', + 'search.resultsView.sunburst.others': 'Others', + 'search.resultsView.sunburst.breakdown.by': 'Breakdown by {0}', 'search.resultsView.map': 'Map', 'search.resultsView.map.show.more': 'Show More', 'search.resultsView.table': 'Table', @@ -205,6 +211,7 @@ define([ 'search.resultsView.table.previous': 'Previous', 'search.resultsView.table.searchInResults': 'Search in Results', 'search.resultsView.table.zeroRecords': 'No matching records found', + 'search.resultsView.table.breakdown.by': 'Breakdown by {0}', 'search.resultsView.amount.shown': 'Showing {0} to {1} of {2} results', 'search.resultsView.amount.shown.no.increment': 'Showing the top {0} results of {1}', 'search.resultsView.amount.shown.no.results': 'There are no results with the location field selected', @@ -306,6 +313,16 @@ define([ 'settings.password': 'Password', 'settings.password.description': 'Password will be stored encrypted', 'settings.password.redacted': '(redacted)', + 'settings.powerpoint': 'PowerPoint', + 'settings.powerpoint.description': 'A custom two-slide PowerPoint .pptx file can be optionally provided; with a doughnut chart on the first slide and a line chart with a date x-axis and two numeric y-axes on the second slide. To reserve space for your own logos etc. on other visualizations, customize the margins below.', + 'settings.powerpoint.template.file': 'Template File', + 'settings.powerpoint.template.sample.download': 'Download Sample Template', + 'settings.powerpoint.template.file.default': 'default', + 'settings.powerpoint.template.file.validate': 'Test Template', + 'settings.powerpoint.template.error.TEMPLATE_FILE_NOT_FOUND': 'PowerPoint template file not found', + 'settings.powerpoint.template.error.TEMPLATE_INVALID': 'PowerPoint template file has invalid format', + 'settings.powerpoint.template.error.INVALID_MARGINS': 'Invalid margins supplied', + 'settings.powerpoint.template.margins': 'Margins', 'settings.queryManipulation': 'Query Manipulation', 'settings.queryManipulation.blacklist': 'Blacklist Name', 'settings.queryManipulation.description': 'Enable query manipulation with QMS', diff --git a/webapp/core/src/main/public/static/js/find/templates/app/page/search/results/entity-topic-map-view.html b/webapp/core/src/main/public/static/js/find/templates/app/page/search/results/entity-topic-map-view.html index a97ea7a79a..77abcaa12f 100644 --- a/webapp/core/src/main/public/static/js/find/templates/app/page/search/results/entity-topic-map-view.html +++ b/webapp/core/src/main/public/static/js/find/templates/app/page/search/results/entity-topic-map-view.html @@ -1,5 +1,6 @@
<% if (showSlider) { %> + PPTX
diff --git a/webapp/core/src/main/public/static/js/find/templates/app/page/search/results/map-results-view.html b/webapp/core/src/main/public/static/js/find/templates/app/page/search/results/map-results-view.html index 44f8878bbe..33b8a902c5 100644 --- a/webapp/core/src/main/public/static/js/find/templates/app/page/search/results/map-results-view.html +++ b/webapp/core/src/main/public/static/js/find/templates/app/page/search/results/map-results-view.html @@ -1,4 +1,5 @@ + PPTX

\ No newline at end of file diff --git a/webapp/core/src/main/public/static/js/find/templates/app/page/search/results/parametric-results-view.html b/webapp/core/src/main/public/static/js/find/templates/app/page/search/results/parametric-results-view.html index 73820357b6..14a9b132a2 100644 --- a/webapp/core/src/main/public/static/js/find/templates/app/page/search/results/parametric-results-view.html +++ b/webapp/core/src/main/public/static/js/find/templates/app/page/search/results/parametric-results-view.html @@ -1,4 +1,5 @@
+ PPTX
+ <%-strings.templateSampleDownload%> +
\ No newline at end of file diff --git a/webapp/core/src/main/public/static/js/leaflet.notransform/leaflet.notransform.js b/webapp/core/src/main/public/static/js/leaflet.notransform/leaflet.notransform.js new file mode 100644 index 0000000000..9cae46531a --- /dev/null +++ b/webapp/core/src/main/public/static/js/leaflet.notransform/leaflet.notransform.js @@ -0,0 +1,7 @@ +define([], function(){ + // Disables 3d transforms on leaflet, since they interfere with proper operation of html2canvas in Firefox/IE11 + // which can be observed by zooming in on the map, panning, then exporting the map as a PPT + if (!/Chrome/.test(navigator.userAgent)) { + window.L_DISABLE_3D=true + } +}) \ No newline at end of file diff --git a/webapp/core/src/main/public/static/js/require-config.js b/webapp/core/src/main/public/static/js/require-config.js index fd202c9fc4..28ab7389aa 100644 --- a/webapp/core/src/main/public/static/js/require-config.js +++ b/webapp/core/src/main/public/static/js/require-config.js @@ -28,6 +28,7 @@ require.config({ json2: '../bower_components/json/json2', 'login-page': '../bower_components/hp-autonomy-login-page/src', leaflet: '../bower_components/leaflet/dist/leaflet-src', + 'leaflet.notransform': 'leaflet.notransform/leaflet.notransform', 'Leaflet.awesome-markers': '../bower_components/Leaflet.awesome-markers/dist/leaflet.awesome-markers', 'leaflet.markercluster': '../bower_components/leaflet.markercluster/dist/leaflet.markercluster-src', moment: '../bower_components/moment/moment', @@ -39,7 +40,8 @@ require.config({ sunburst: '../bower_components/hp-autonomy-sunburst/src', topicmap: '../bower_components/hp-autonomy-topic-map/src', underscore: '../bower_components/underscore/underscore', - typeahead: '../bower_components/corejs-typeahead/dist/typeahead.jquery' + typeahead: '../bower_components/corejs-typeahead/dist/typeahead.jquery', + 'html2canvas': '../bower_components/html2canvas/build/html2canvas' }, shim: { 'backbone': { @@ -59,6 +61,7 @@ require.config({ exports: '_' }, 'Leaflet.awesome-markers': ['leaflet'], - 'leaflet.markercluster': ['leaflet'] + 'leaflet.markercluster': ['leaflet'], + 'leaflet': ['leaflet.notransform'] } }); diff --git a/webapp/core/src/test/java/com/hp/autonomy/frontend/find/core/configuration/PowerPointConfigTest.java b/webapp/core/src/test/java/com/hp/autonomy/frontend/find/core/configuration/PowerPointConfigTest.java new file mode 100644 index 0000000000..ca930d0275 --- /dev/null +++ b/webapp/core/src/test/java/com/hp/autonomy/frontend/find/core/configuration/PowerPointConfigTest.java @@ -0,0 +1,147 @@ +/* + * Copyright 2017 Hewlett-Packard Development Company, L.P. + * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. + */ + +package com.hp.autonomy.frontend.find.core.configuration; + +import com.hp.autonomy.frontend.configuration.ConfigException; +import com.hp.autonomy.frontend.reports.powerpoint.PowerPointServiceImpl; +import com.hp.autonomy.frontend.reports.powerpoint.TemplateSource; +import com.hp.autonomy.frontend.reports.powerpoint.dto.Anchor; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import org.apache.commons.io.IOUtils; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class PowerPointConfigTest { + private PowerPointConfig powerPointConfig; + private static final double EPSILON = 1e-10; + + @Before + public void setUp() { + powerPointConfig = new PowerPointConfig.Builder() + .setMarginTop(0.0) + .setMarginBottom(0.0) + .setMarginLeft(0.0) + .setMarginRight(0.0) + .setTemplateFile(null) + .build(); + } + + @Test + public void merge() { + assertEquals(powerPointConfig, new PowerPointConfig.Builder().build().merge(powerPointConfig)); + } + + @Test + public void mergeNoDefaults() { + assertEquals(powerPointConfig, powerPointConfig.merge(null)); + } + + @Test + public void basicValidate() throws ConfigException { + powerPointConfig.basicValidate(null); + + final Anchor anchor = powerPointConfig.getAnchor(); + assertEquals("Wrong left edge", anchor.getX(), 0, EPSILON); + assertEquals("Wrong top edge", anchor.getY(), 0, EPSILON); + assertEquals("Wrong width", anchor.getWidth(), 1, EPSILON); + assertEquals("Wrong height", anchor.getHeight(), 1, EPSILON); + } + + @Test + public void basicValidateAnchors() throws ConfigException { + final PowerPointConfig config = new PowerPointConfig.Builder() + .setMarginTop(0.01) + .setMarginBottom(0.02) + .setMarginLeft(0.03) + .setMarginRight(0.04) + .build(); + + config.basicValidate(null); + + final Anchor anchor = config.getAnchor(); + assertEquals("Wrong left edge", anchor.getX(), 0.03, EPSILON); + assertEquals("Wrong top edge", anchor.getY(), 0.01, EPSILON); + assertEquals("Wrong width", anchor.getWidth(), 0.93, EPSILON); + assertEquals("Wrong height", anchor.getHeight(), 0.97, EPSILON); + } + + @Test + public void basicValidateFile() throws ConfigException, IOException { + final File tempFile = File.createTempFile("temp", ".pptx"); + tempFile.deleteOnExit(); + + try(final FileOutputStream os = new FileOutputStream(tempFile)) { + IOUtils.copyLarge(TemplateSource.DEFAULT.getInputStream(), os); + } + + final PowerPointConfig config = new PowerPointConfig.Builder() + .setTemplateFile(tempFile.getAbsolutePath()) + .build(); + + config.basicValidate(null); + } + + @Test(expected = ConfigException.class) + public void basicValidateInvalidTop() throws ConfigException { + new PowerPointConfig.Builder().setMarginTop(2.0).build().basicValidate(null); + } + + @Test(expected = ConfigException.class) + public void basicValidateInvalidBottom() throws ConfigException { + new PowerPointConfig.Builder().setMarginBottom(-0.1).build().basicValidate(null); + } + + @Test(expected = ConfigException.class) + public void basicValidateInvalidLeft() throws ConfigException { + new PowerPointConfig.Builder().setMarginLeft(-1.0).build().basicValidate(null); + } + + @Test(expected = ConfigException.class) + public void basicValidateInvalidRight() throws ConfigException { + new PowerPointConfig.Builder().setMarginRight(1.0).build().basicValidate(null); + } + + @Test(expected = ConfigException.class) + public void basicValidateInvalidTopBottom() throws ConfigException { + new PowerPointConfig.Builder() + .setMarginBottom(0.4) + .setMarginTop(0.7) + .build() + .basicValidate(null); + } + + @Test(expected = ConfigException.class) + public void basicValidateInvalidLeftRight() throws ConfigException { + new PowerPointConfig.Builder() + .setMarginLeft(0.5) + .setMarginRight(0.5) + .build() + .basicValidate(null); + } + + @Test(expected = ConfigException.class) + public void basicValidateInvalidTemplateFilePath() throws ConfigException { + new PowerPointConfig.Builder() + .setTemplateFile("no/such/file.exists") + .build() + .basicValidate(null); + } + + @Test(expected = ConfigException.class) + public void basicValidateBlankFile() throws ConfigException, IOException { + final File tmpFile = File.createTempFile("temp", "pptx"); + tmpFile.deleteOnExit(); + + new PowerPointConfig.Builder() + .setTemplateFile(tmpFile.getAbsolutePath()) + .build() + .basicValidate(null); + } +} diff --git a/webapp/core/src/test/java/com/hp/autonomy/frontend/find/core/configuration/PowerPointConfigValidatorTest.java b/webapp/core/src/test/java/com/hp/autonomy/frontend/find/core/configuration/PowerPointConfigValidatorTest.java new file mode 100644 index 0000000000..720bbd4dd2 --- /dev/null +++ b/webapp/core/src/test/java/com/hp/autonomy/frontend/find/core/configuration/PowerPointConfigValidatorTest.java @@ -0,0 +1,41 @@ +/* + * Copyright 2017 Hewlett-Packard Development Company, L.P. + * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. + */ + +package com.hp.autonomy.frontend.find.core.configuration; + +import com.hp.autonomy.frontend.configuration.ConfigException; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class PowerPointConfigValidatorTest { + private PowerPointConfigValidator powerPointConfigValidator; + + @Before + public void setUp() { + powerPointConfigValidator = new PowerPointConfigValidator(); + } + + @Test + public void basicValidate() { + assertTrue(powerPointConfigValidator.validate(new PowerPointConfig.Builder().build()).isValid()); + } + + @Test + public void basicValidateWhenInvalid() throws ConfigException { + assertFalse(powerPointConfigValidator.validate(new PowerPointConfig.Builder() + .setMarginBottom(10.0) + .build() + ).isValid()); + } + + @Test + public void getSupportedClass() { + assertEquals(PowerPointConfig.class, powerPointConfigValidator.getSupportedClass()); + } +} diff --git a/webapp/core/src/test/java/com/hp/autonomy/frontend/find/core/export/ExportControllerTest.java b/webapp/core/src/test/java/com/hp/autonomy/frontend/find/core/export/ExportControllerTest.java index db14962025..d5babf4d68 100644 --- a/webapp/core/src/test/java/com/hp/autonomy/frontend/find/core/export/ExportControllerTest.java +++ b/webapp/core/src/test/java/com/hp/autonomy/frontend/find/core/export/ExportControllerTest.java @@ -5,6 +5,7 @@ package com.hp.autonomy.frontend.find.core.export; +import com.fasterxml.jackson.databind.ObjectMapper; import com.hp.autonomy.frontend.find.core.web.ControllerUtils; import com.hp.autonomy.frontend.find.core.web.ErrorModelAndViewInfo; import com.hp.autonomy.frontend.find.core.web.RequestMapper; @@ -33,6 +34,8 @@ public abstract class ExportControllerTest, E extends protected RequestMapper requestMapper; @Mock protected ControllerUtils controllerUtils; + @Mock + protected ObjectMapper objectMapper; private ExportController controller; diff --git a/webapp/hod/pom.xml b/webapp/hod/pom.xml index ccde578741..aabd1b2bd0 100644 --- a/webapp/hod/pom.xml +++ b/webapp/hod/pom.xml @@ -118,6 +118,11 @@ full provided + + com.hp.autonomy.frontend.reports.powerpoint + powerpoint-report + ${powerpoint.report.version} + com.hp.autonomy.hod.redis diff --git a/webapp/hod/src/main/java/com/hp/autonomy/frontend/find/hod/configuration/HodFindConfig.java b/webapp/hod/src/main/java/com/hp/autonomy/frontend/find/hod/configuration/HodFindConfig.java index 63e6905dc9..7bc1a591e1 100644 --- a/webapp/hod/src/main/java/com/hp/autonomy/frontend/find/hod/configuration/HodFindConfig.java +++ b/webapp/hod/src/main/java/com/hp/autonomy/frontend/find/hod/configuration/HodFindConfig.java @@ -17,6 +17,7 @@ import com.hp.autonomy.frontend.find.core.configuration.FindConfig; import com.hp.autonomy.frontend.find.core.configuration.FindConfigBuilder; import com.hp.autonomy.frontend.find.core.configuration.MapConfiguration; +import com.hp.autonomy.frontend.find.core.configuration.PowerPointConfig; import com.hp.autonomy.frontend.find.core.configuration.SavedSearchConfig; import com.hp.autonomy.frontend.find.core.configuration.UiCustomization; import com.hp.autonomy.hod.client.api.authentication.ApiKey; @@ -51,6 +52,7 @@ public class HodFindConfig extends AbstractConfig implements HodS private final UiCustomization uiCustomization; private final Integer minScore; private final Integer topicMapMaxResults; + private final PowerPointConfig powerPoint; @JsonProperty("savedSearches") private final SavedSearchConfig savedSearchConfig; @@ -71,6 +73,7 @@ public HodFindConfig merge(final HodFindConfig config) { .savedSearchConfig(savedSearchConfig == null ? config.savedSearchConfig : savedSearchConfig.merge(config.savedSearchConfig)) .minScore(minScore == null ? config.minScore : minScore) .topicMapMaxResults(topicMapMaxResults == null ? config.topicMapMaxResults : topicMapMaxResults) + .powerPoint(powerPoint == null ? config.powerPoint : powerPoint.merge(config.powerPoint)) .build() : this; } @@ -101,6 +104,10 @@ public void basicValidate(final String section) throws ConfigException { queryManipulation.basicValidate(SECTION); savedSearchConfig.basicValidate(SECTION); + if (powerPoint != null) { + powerPoint.basicValidate("powerPoint"); + } + if (map != null) { map.basicValidate("map"); } diff --git a/webapp/hod/src/main/java/com/hp/autonomy/frontend/find/hod/export/HodExportController.java b/webapp/hod/src/main/java/com/hp/autonomy/frontend/find/hod/export/HodExportController.java index 6bb12a6411..a600d0c05c 100644 --- a/webapp/hod/src/main/java/com/hp/autonomy/frontend/find/hod/export/HodExportController.java +++ b/webapp/hod/src/main/java/com/hp/autonomy/frontend/find/hod/export/HodExportController.java @@ -5,11 +5,14 @@ package com.hp.autonomy.frontend.find.hod.export; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.hp.autonomy.frontend.configuration.ConfigService; import com.hp.autonomy.frontend.find.core.export.ExportController; import com.hp.autonomy.frontend.find.core.export.ExportFormat; import com.hp.autonomy.frontend.find.core.export.ExportService; import com.hp.autonomy.frontend.find.core.web.ControllerUtils; import com.hp.autonomy.frontend.find.core.web.RequestMapper; +import com.hp.autonomy.frontend.find.hod.configuration.HodFindConfig; import com.hp.autonomy.hod.client.api.textindex.query.search.Print; import com.hp.autonomy.hod.client.error.HodErrorException; import com.hp.autonomy.searchcomponents.hod.search.HodDocumentsService; @@ -33,8 +36,10 @@ class HodExportController extends ExportController requestMapper, final ControllerUtils controllerUtils, final HodDocumentsService documentsService, - final ExportService exportService) { - super(requestMapper, controllerUtils); + final ExportService exportService, + final ObjectMapper objectMapper, + final ConfigService configService) { + super(requestMapper, controllerUtils, objectMapper, configService); this.documentsService = documentsService; this.exportService = exportService; } diff --git a/webapp/hod/src/main/public/static/js/find/app/page/find-settings-page.js b/webapp/hod/src/main/public/static/js/find/app/page/find-settings-page.js index bac4e8e82e..4f4bdd83eb 100644 --- a/webapp/hod/src/main/public/static/js/find/app/page/find-settings-page.js +++ b/webapp/hod/src/main/public/static/js/find/app/page/find-settings-page.js @@ -4,11 +4,13 @@ */ define([ + 'i18n!find/nls/bundle', 'find/app/page/abstract-find-settings-page', 'find/app/page/settings/iod-widget', + 'find/app/page/settings/powerpoint-widget', 'settings/js/widgets/single-user-widget', 'underscore' -], function(SettingsPage, IodWidget, SingleUserWidget, _) { +], function(i18n, SettingsPage, IodWidget, PowerPointWidget, SingleUserWidget, _) { 'use strict'; return SettingsPage.extend({ @@ -50,6 +52,15 @@ define([ }, title: i18n['settings.adminUser'] }) + ], [ + new PowerPointWidget({ + configItem: 'powerPoint', + description: i18n['settings.powerpoint.description'], + isOpened: true, + title: i18n['settings.powerpoint'], + strings: this.serverStrings(), + pptxTemplateUrl: this.pptxTemplateUrl + }) ] ]; } diff --git a/webapp/hod/src/main/resources/application.properties b/webapp/hod/src/main/resources/application.properties index d9171d84e5..6b27e6eb6a 100644 --- a/webapp/hod/src/main/resources/application.properties +++ b/webapp/hod/src/main/resources/application.properties @@ -32,3 +32,6 @@ spring.jpa.properties.hibernate.default_schema=find spring.jpa.hibernate.ddl-auto=none spring.main.banner-mode=off spring.messages.basename=i18n/hod-errors,i18n/errors +# Increase the default max file upload size from 1MB, since we use large base64-encoded images for map .pptx export +spring.http.multipart.max-file-size=16Mb +spring.http.multipart.max-request-size=16Mb diff --git a/webapp/hod/src/main/resources/custom-application.properties b/webapp/hod/src/main/resources/custom-application.properties index 3f3c8f8dc8..3bdc320c6a 100644 --- a/webapp/hod/src/main/resources/custom-application.properties +++ b/webapp/hod/src/main/resources/custom-application.properties @@ -9,4 +9,6 @@ hp.find.enableBi=true server.reverseProxy=false # Only used if server.reverseProxy is true server.ajp.port=8009 -server.tomcat.resources.max-cache-kb=20480 \ No newline at end of file +server.tomcat.resources.max-cache-kb=20480 +# Increase the connector max post size from 2097152, since we use large base64-encoded images for map .pptx export +server.tomcat.connector.max-post-size=16777216 \ No newline at end of file diff --git a/webapp/hod/src/main/resources/defaultHodConfigFile.json b/webapp/hod/src/main/resources/defaultHodConfigFile.json index 1502d5c483..08ecc89590 100644 --- a/webapp/hod/src/main/resources/defaultHodConfigFile.json +++ b/webapp/hod/src/main/resources/defaultHodConfigFile.json @@ -56,6 +56,9 @@ } ] }, + "powerpoint": { + "templateFile": "" + }, "uiCustomization": { "options": { "directAccessLink": { diff --git a/webapp/hod/src/test/java/com/hp/autonomy/frontend/find/hod/export/HodExportControllerTest.java b/webapp/hod/src/test/java/com/hp/autonomy/frontend/find/hod/export/HodExportControllerTest.java index 0962f1897a..df09134819 100644 --- a/webapp/hod/src/test/java/com/hp/autonomy/frontend/find/hod/export/HodExportControllerTest.java +++ b/webapp/hod/src/test/java/com/hp/autonomy/frontend/find/hod/export/HodExportControllerTest.java @@ -5,8 +5,10 @@ package com.hp.autonomy.frontend.find.hod.export; +import com.hp.autonomy.frontend.configuration.ConfigService; import com.hp.autonomy.frontend.find.core.export.ExportController; import com.hp.autonomy.frontend.find.core.export.ExportControllerTest; +import com.hp.autonomy.frontend.find.hod.configuration.HodFindConfig; import com.hp.autonomy.hod.client.error.HodErrorException; import com.hp.autonomy.searchcomponents.hod.search.HodDocumentsService; import com.hp.autonomy.searchcomponents.hod.search.HodQueryRequest; @@ -29,6 +31,8 @@ public class HodExportControllerTest extends ExportControllerTest hodFindConfig; @Override protected ExportController constructController() throws IOException { @@ -41,7 +45,7 @@ protected ExportController constructControll when(queryRequest.getMaxResults()).thenReturn(Integer.MAX_VALUE); - return new HodExportController(requestMapper, controllerUtils, documentsService, exportService); + return new HodExportController(requestMapper, controllerUtils, documentsService, exportService, objectMapper, hodFindConfig); } @Override diff --git a/webapp/idol/pom.xml b/webapp/idol/pom.xml index 8e75ffe680..c0b6c26661 100644 --- a/webapp/idol/pom.xml +++ b/webapp/idol/pom.xml @@ -123,6 +123,11 @@ haven-search-components-idol ${haven.search.components.version} + + com.hp.autonomy.frontend.reports.powerpoint + powerpoint-report + ${powerpoint.report.version} + commons-io @@ -132,7 +137,7 @@ org.apache.commons commons-collections4 - 4.0 + 4.1 diff --git a/webapp/idol/src/main/java/com/hp/autonomy/frontend/find/idol/configuration/IdolFindConfig.java b/webapp/idol/src/main/java/com/hp/autonomy/frontend/find/idol/configuration/IdolFindConfig.java index 2d676df8ef..70ffe1c532 100644 --- a/webapp/idol/src/main/java/com/hp/autonomy/frontend/find/idol/configuration/IdolFindConfig.java +++ b/webapp/idol/src/main/java/com/hp/autonomy/frontend/find/idol/configuration/IdolFindConfig.java @@ -19,6 +19,7 @@ import com.hp.autonomy.frontend.find.core.configuration.FindConfig; import com.hp.autonomy.frontend.find.core.configuration.FindConfigBuilder; import com.hp.autonomy.frontend.find.core.configuration.MapConfiguration; +import com.hp.autonomy.frontend.find.core.configuration.PowerPointConfig; import com.hp.autonomy.frontend.find.core.configuration.SavedSearchConfig; import com.hp.autonomy.frontend.find.core.configuration.UiCustomization; import com.hp.autonomy.frontend.find.idol.configuration.IdolFindConfig.IdolFindConfigBuilder; @@ -61,6 +62,7 @@ public class IdolFindConfig extends AbstractConfig implements Us private final Integer minScore; private final StatsServerConfig statsServer; private final Integer topicMapMaxResults; + private final PowerPointConfig powerPoint; @JsonIgnore private volatile Map> productMap; @@ -82,6 +84,7 @@ public IdolFindConfig merge(final IdolFindConfig maybeOther) { .minScore(minScore == null ? other.minScore : minScore) .statsServer(statsServer == null ? other.statsServer : statsServer.merge(other.statsServer)) .topicMapMaxResults(topicMapMaxResults == null ? other.topicMapMaxResults : topicMapMaxResults) + .powerPoint(powerPoint == null ? other.powerPoint : powerPoint.merge(other.powerPoint)) .build()) .orElse(this); } @@ -124,6 +127,10 @@ public void basicValidate(final String section) throws ConfigException { content.basicValidate("content"); savedSearchConfig.basicValidate(SECTION); + if (powerPoint != null) { + powerPoint.basicValidate("powerPoint"); + } + if (map != null) { map.basicValidate("map"); } diff --git a/webapp/idol/src/main/java/com/hp/autonomy/frontend/find/idol/export/IdolExportController.java b/webapp/idol/src/main/java/com/hp/autonomy/frontend/find/idol/export/IdolExportController.java index 38d8e54ba0..08d63c312c 100644 --- a/webapp/idol/src/main/java/com/hp/autonomy/frontend/find/idol/export/IdolExportController.java +++ b/webapp/idol/src/main/java/com/hp/autonomy/frontend/find/idol/export/IdolExportController.java @@ -6,11 +6,14 @@ package com.hp.autonomy.frontend.find.idol.export; import com.autonomy.aci.client.services.AciErrorException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.hp.autonomy.frontend.configuration.ConfigService; import com.hp.autonomy.frontend.find.core.export.ExportController; import com.hp.autonomy.frontend.find.core.export.ExportFormat; import com.hp.autonomy.frontend.find.core.export.ExportService; import com.hp.autonomy.frontend.find.core.web.ControllerUtils; import com.hp.autonomy.frontend.find.core.web.RequestMapper; +import com.hp.autonomy.frontend.find.idol.configuration.IdolFindConfig; import com.hp.autonomy.searchcomponents.core.search.StateTokenAndResultCount; import com.hp.autonomy.searchcomponents.idol.search.IdolDocumentsService; import com.hp.autonomy.searchcomponents.idol.search.IdolQueryRequest; @@ -31,8 +34,10 @@ class IdolExportController extends ExportController requestMapper, final ControllerUtils controllerUtils, final IdolDocumentsService documentsService, - final ExportService exportService) { - super(requestMapper, controllerUtils); + final ExportService exportService, + final ObjectMapper objectMapper, + final ConfigService configService) { + super(requestMapper, controllerUtils, objectMapper, configService); this.documentsService = documentsService; this.exportService = exportService; } diff --git a/webapp/idol/src/main/public/static/js/find/app/page/find-settings-page.js b/webapp/idol/src/main/public/static/js/find/app/page/find-settings-page.js index 8e78fb2284..117ca0b4c0 100644 --- a/webapp/idol/src/main/public/static/js/find/app/page/find-settings-page.js +++ b/webapp/idol/src/main/public/static/js/find/app/page/find-settings-page.js @@ -15,10 +15,11 @@ define([ 'find/app/page/settings/saved-search-widget', 'find/app/page/settings/stats-server-widget', 'find/app/page/settings/view-widget', + 'find/app/page/settings/powerpoint-widget', 'i18n!find/nls/bundle', 'text!find/templates/app/page/settings/community-widget.html' ], function (_, SettingsPage, AciWidget, AnswerServerWidget, CommunityWidget, MapWidget, MmapWidget, QueryManipulationWidget, - SavedSearchWidget, StatsServerWidget, ViewWidget, i18n, dropdownTemplate) { + SavedSearchWidget, StatsServerWidget, ViewWidget, PowerPointWidget, i18n, dropdownTemplate) { return SettingsPage.extend({ initializeWidgets: function () { @@ -45,6 +46,14 @@ define([ loginTypeLabel: i18n['settings.community.login.type'], validateFailed: i18n['settings.test.failed'] }) + }), + new PowerPointWidget({ + configItem: 'powerPoint', + description: i18n['settings.powerpoint.description'], + isOpened: true, + title: i18n['settings.powerpoint'], + strings: this.serverStrings(), + pptxTemplateUrl: this.pptxTemplateUrl }) ], [ new QueryManipulationWidget({ diff --git a/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard-page.js b/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard-page.js index d24e15f18c..5ea2fc9b45 100644 --- a/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard-page.js +++ b/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard-page.js @@ -10,12 +10,73 @@ define([ 'find/app/vent', 'find/idol/app/page/dashboard/widget-registry', './dashboard/widgets/widget-not-found', - './dashboard/update-tracker-model' -], function(_, $, BasePage, vent, widgetRegistry, WidgetNotFoundWidget, UpdateTrackerModel) { + './dashboard/update-tracker-model', + 'text!find/idol/templates/page/dashboard-page.html', + 'i18n!find/nls/bundle' +], function(_, $, BasePage, vent, widgetRegistry, WidgetNotFoundWidget, UpdateTrackerModel, template, i18n) { 'use strict'; return BasePage.extend({ className: 'dashboard', + template: _.template(template), + + events: { + 'click .report-pptx-checkbox': function(evt){ + evt.stopPropagation(); + }, + 'click .report-pptx': function(evt){ + evt.preventDefault(); + evt.stopPropagation(); + + var reports = [], + scaleX = 0.01 * this.widthPerUnit, + scaleY = 0.01 * this.heightPerUnit, + $el = $(evt.currentTarget), + multipage = $el.is('.report-pptx-multipage'), + $group = $el.closest('.btn-group'), + labels = $group.find('.report-pptx-labels:checked').length, + padding = $group.find('.report-pptx-padding:checked').length; + + _.each(this.widgetViews, function(widget) { + if (widget.view.exportPPTData) { + var data = widget.view.exportPPTData(); + + // this may be a promise, or an actual object + if (data) { + reports.push($.when(data).then(function(data){ + var pos = widget.position; + + return _.defaults(data, { + title: labels ? widget.view.name : undefined, + x: multipage ? 0 : pos.x * scaleX, + y: multipage ? 0 : pos.y * scaleY, + width: multipage ? 1 : pos.width * scaleX, + height: multipage ? 1 : pos.height * scaleY, + margin: padding ? 3 : 0 + }) + })); + } + } + }, this); + + if (reports.length) { + $.when.apply($, reports).done(function(){ + var children = _.compact(arguments); + + // Since it's an async action, we have to keep it as target: _self to avoid the popup blocker. + var $form = $(''); + + $form[0].data.value = JSON.stringify({ + children: children + }) + + $form[0].multipage.value = multipage; + + $form.appendTo(document.body).submit().remove() + }) + } + } + }, initialize: function(options) { _.bindAll(this, 'update'); @@ -50,7 +111,13 @@ define([ }, render: function() { - this.$el.empty(); + this.$el.html(this.template({ + dashboardName: this.dashboardName, + powerpointSingle: i18n['powerpoint.export.single'], + powerpointMultiple: i18n['powerpoint.export.multiple'], + powerpointLabels: i18n['powerpoint.export.labels'], + powerpointPadding: i18n['powerpoint.export.padding'], + })); _.each(this.widgetViews, function(widget) { const $div = this.generateWidgetDiv(widget.position); @@ -58,6 +125,14 @@ define([ widget.view.setElement($div).render(); }.bind(this)); + var $exportBtn = this.$('.report-pptx-group'); + + $.when.apply($, _.map(this.widgetViews, function(widget){ + return widget.view.initialiseWidgetPromise + })).done(function(){ + $exportBtn.removeClass('hide'); + }) + this.listenTo(vent, 'vent:resize', this.onResize); this.listenTo(this.sidebarModel, 'change:collapsed', this.onResize); }, diff --git a/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/current-time.js b/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/current-time.js index 49662eb86d..d0a3a87e95 100644 --- a/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/current-time.js +++ b/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/current-time.js @@ -42,6 +42,22 @@ define([ this.$time.text(time.format(this.timeFormat)); this.$day.text(time.format('dddd')); this.$date.text(time.format(this.dateFormat)); + }, + + exportPPTData: function(){ + const fontScale = 10 / 16; + + return { + data: { + text: _.map([this.$time, this.$day, this.$date], function($el){ + return { + text: $el.text().toUpperCase() + '\n', + fontSize: Math.round(parseInt($el.css('font-size')) * fontScale) + } + }) + }, + type: 'text' + }; } }); }); diff --git a/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/map-widget.js b/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/map-widget.js index a8530b3d87..9ab4551ad6 100644 --- a/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/map-widget.js +++ b/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/map-widget.js @@ -44,6 +44,10 @@ define([ return this.mapView.getIcon(locationField.iconName, locationField.iconColor, locationField.markerColor); }, + postInitialize: function(){ + return this.getData(); + }, + getData: function() { if(!this.hasRendered) { return $.when(); @@ -69,8 +73,8 @@ define([ max_results: this.maxResults, indexes: this.queryModel.get('indexes'), field_text: newFieldText, - min_date: this.queryModel.get('minDate'), - max_date: this.queryModel.get('maxDate'), + min_date: this.queryModel.getIsoDate('minDate'), + max_date: this.queryModel.getIsoDate('maxDate'), sort: 'relevance', summary: 'context', queryType: 'MODIFIED' @@ -92,6 +96,15 @@ define([ this.mapView.addMarkers(this.markers, this.clusterMarkers); } }.bind(this)); + }, + + exportPPTData: function(){ + return this.mapView.exportPPTData().then(function(data){ + return { + data: data, + type: 'map' + } + }); } }); }); diff --git a/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/results-list-widget.js b/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/results-list-widget.js index aeb3002619..5331e4382f 100644 --- a/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/results-list-widget.js +++ b/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/results-list-widget.js @@ -51,6 +51,10 @@ define([ _.defer(_.bind(this.hideOverflow, this)); }, + postInitialize: function(){ + return this.getData(); + }, + getData: function() { return this.documentsCollection.fetch({ data: { @@ -58,8 +62,8 @@ define([ max_results: this.maxResults, indexes: this.queryModel.get('indexes'), field_text: this.queryModel.get('fieldText'), - min_date: this.queryModel.get('minDate'), - max_date: this.queryModel.get('maxDate'), + min_date: this.queryModel.getIsoDate('minDate'), + max_date: this.queryModel.getIsoDate('maxDate'), sort: this.sort, summary: 'context', queryType: 'MODIFIED', @@ -79,6 +83,22 @@ define([ ? boundingClientRect.right > containerBounds.right : boundingClientRect.bottom > containerBounds.bottom); }.bind(this)); + }, + + exportPPTData: function(){ + return { + data: { + drawIcons: false, + docs: this.documentsCollection.map(function(model){ + return { + title: model.get('title'), + summary: model.get('summary'), + thumbnail: model.get('thumbnail') + } + }) + }, + type: 'list' + } } }); }); diff --git a/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/static-content.js b/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/static-content.js index 6f819482f1..de882c9e3a 100644 --- a/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/static-content.js +++ b/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/static-content.js @@ -19,6 +19,56 @@ define([ Widget.prototype.render.apply(this); this.$content.html(this.html); + }, + + exportPPTData: function(){ + var nodes = []; + + var fontScale = 10 / 16; + + // Try to traverse the content in a similar way that HTML would parse it, making bold text etc. + // We can't just use $el.text() since that loses all the formatting. + function traverse($el) { + _.each(_.filter($el.contents(), function(dom){ + return dom.nodeType === Node.TEXT_NODE || dom.nodeType === Node.ELEMENT_NODE + }), function(dom, idx, all){ + if (dom.nodeType === Node.TEXT_NODE) { + var last = _.last(nodes); + if (last && last.text.slice(-1) !== '\n' && $el.css('display') !== 'inline') { + last.text += '\n'; + } + + var trimmedText = dom.nodeValue.trim(); + + if (trimmedText) { + // text node, add with the current CSS + var fontFamily = $el.css('font-family'); + var fontWeight = $el.css('font-weight'); + nodes.push({ + // we want to coalesce all whitespace into a single whitespace (to match HTML) + text: dom.nodeValue.replace(/\s+/, ' '), + fontSize: Math.round(parseInt($el.css('font-size')) * fontScale), + // font weight might be a number > 400, bold/bolder etc. or built into font family + bold: fontWeight > 400 || /bold/.test(fontWeight) || /Bold/i.test(fontFamily), + // italic may be a property of the font style, font weight or font-family + italic: /italic/.test($el.css('font-style')) || /Italic/i.test(fontFamily) + }) + } + } + else if (dom.nodeType === Node.ELEMENT_NODE) { + traverse($(dom)) + } + }) + } + + traverse(this.$content) + + return { + data: { + text: nodes + }, + type: 'text' + }; } }); }); diff --git a/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/static-image-widget.js b/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/static-image-widget.js index 1196590850..d7eafb9d19 100644 --- a/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/static-image-widget.js +++ b/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/static-image-widget.js @@ -5,7 +5,8 @@ define([ 'jquery', - './widget' + './widget', + 'html2canvas' ], function($, Widget) { 'use strict'; @@ -22,6 +23,38 @@ define([ const html = $('
'); this.$content.html(html); + }, + + exportPPTData: function(){ + var $imageEl = this.$('.static-image'); + + if (!$imageEl.length) { + return + } + + var deferred = $.Deferred(); + html2canvas($imageEl[0], { + useCORS: true, + onrendered: function(canvas) { + try { + deferred.resolve({ + image: canvas.toDataURL('image/jpeg'), + markers: [] + }); + } + catch (e) { + // canvas.toDataURL can throw exceptions in IE11 even if there's CORS headers on the background-image + deferred.resolve(null) + } + } + }); + + return deferred.then(function(data){ + return data && { + data: data, + type: 'map' + } + }); } }); }); diff --git a/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/sunburst-widget.js b/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/sunburst-widget.js index f1f86672b3..97b47085dd 100644 --- a/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/sunburst-widget.js +++ b/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/sunburst-widget.js @@ -89,6 +89,35 @@ define([ this.listenTo(this.legendColorCollection, 'update reset', this.updateSunburstAndLegend); }, + exportPPTData: function(){ + const data = this.legendColorCollection.map(function(model){ + return { + category: model.get('text') || i18n['search.resultsView.sunburst.others'], + value: model.get('count'), + color: model.get('color') || HIDDEN_COLOR + } + }).sort(function(a, b){ + return d3.ascending(a.category, b.category) + }); + + return data.length ? { + type: 'sunburst', + data: { + categories: _.pluck(data, 'category'), + values: _.pluck(data, 'value'), + title: prettyOrNull(this.firstField), + colors: _.pluck(data, 'color'), + strokeColors: ['#000000'], + showInLegend: _.reduce(data, function (acc, val, idx) { + if (val.color !== HIDDEN_COLOR) { + acc.push(idx); + } + return acc; + }, []) + } + } : null + }, + render: function() { SavedSearchWidget.prototype.render.apply(this); diff --git a/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/time-last-refreshed-widget.js b/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/time-last-refreshed-widget.js index 7dd699d5f2..ff4b05d0fd 100644 --- a/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/time-last-refreshed-widget.js +++ b/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/time-last-refreshed-widget.js @@ -85,6 +85,20 @@ define([ formatDate: function(date) { return date.format(this.dateFormat); + }, + + exportPPTData: function(){ + return { + data: { + text: _.map([this.$lastRefresh, this.$nextRefresh], function($el){ + return { + text: $el.text() + '\n', + fontSize: 15 + } + }) + }, + type: 'text' + }; } }); }); diff --git a/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/topic-map-widget.js b/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/topic-map-widget.js index 7afc0101b6..5448165351 100644 --- a/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/topic-map-widget.js +++ b/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/topic-map-widget.js @@ -53,6 +53,19 @@ define([ getData: function() { return this.entityTopicMap.fetchRelatedConcepts(); + }, + + exportPPTData: function() { + if (this.entityTopicMap) { + var data = this.entityTopicMap.exportPPTData(); + + if (data) { + return { + data: data, + type: 'topicmap' + } + } + } } }); }); diff --git a/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/video-widget.js b/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/video-widget.js index e46c1ad539..19c3435aef 100644 --- a/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/video-widget.js +++ b/webapp/idol/src/main/public/static/js/find/idol/app/page/dashboard/widgets/video-widget.js @@ -68,14 +68,48 @@ define([ max_results: this.searchResultNumber, indexes: this.queryModel.get('indexes'), field_text: fieldText, - min_date: this.queryModel.get('minDate'), - max_date: this.queryModel.get('maxDate'), + min_date: this.queryModel.getIsoDate('minDate'), + max_date: this.queryModel.getIsoDate('maxDate'), sort: 'relevance', summary: 'context', queryType: 'MODIFIED' }, reset: false }); + }, + + exportPPTData: function(){ + const videoEl = this.$('video'); + + if (!videoEl.length) { + return + } + + try { + const canvas = document.createElement('canvas'); + const videoDom = videoEl[0]; + // Compensate for the video element's auto-crop to preserve aspect ratio, jQuery doesn't include this. + const aspectRatio = videoDom.videoWidth / videoDom.videoHeight; + const width = videoEl.width(); + const height = videoEl.height(); + const actualWidth = Math.min(width, height * aspectRatio); + const actualHeight = Math.min(height, width / aspectRatio); + canvas.width = actualWidth; + canvas.height = actualHeight; + const ctx = canvas.getContext('2d'); + ctx.drawImage(videoDom, 0, 0, canvas.width, canvas.height); + + return { + data: { + // Note: this might not work if the video is hosted elsewhere + image: canvas.toDataURL('image/jpeg'), + markers: [] + }, + type: 'map' + } + } catch (e) { + // If there's an error, e.g. if the video is external and we're not allowed access, just skip it + } } }); }); diff --git a/webapp/idol/src/main/public/static/js/find/idol/app/page/search/results/comparison-map.js b/webapp/idol/src/main/public/static/js/find/idol/app/page/search/results/comparison-map.js index 281ccaac94..09ef419695 100644 --- a/webapp/idol/src/main/public/static/js/find/idol/app/page/search/results/comparison-map.js +++ b/webapp/idol/src/main/public/static/js/find/idol/app/page/search/results/comparison-map.js @@ -45,6 +45,15 @@ define([ 'click .map-popup-title': function (e) { const allCollections = _.chain(this.comparisons).pluck('collection').pluck('models').flatten().value(); vent.navigateToDetailRoute(_.findWhere(allCollections, {cid: e.currentTarget.getAttribute('cid')})); + }, + 'click .map-pptx': function(e){ + e.preventDefault(); + this.mapView.exportPPT( + '\'' + this.searchModels.first.get('title') + '\' v.s. \'' + this.searchModels.second.get('title') + '\'' + + '\n' + '(' + _.unique(_.map([this.firstSelectionView, this.bothSelectionView, this.secondSelectionView], function(view){ + return view.model.get('displayName'); + })).join(', ') + ')' + ) } }, diff --git a/webapp/idol/src/main/public/static/js/find/idol/templates/comparison/map-comparison-view.html b/webapp/idol/src/main/public/static/js/find/idol/templates/comparison/map-comparison-view.html index 65fd1cabde..0fba26cbfd 100644 --- a/webapp/idol/src/main/public/static/js/find/idol/templates/comparison/map-comparison-view.html +++ b/webapp/idol/src/main/public/static/js/find/idol/templates/comparison/map-comparison-view.html @@ -16,7 +16,7 @@

<%-secondLabel%>

- + PPTX
diff --git a/webapp/idol/src/main/public/static/js/find/idol/templates/page/dashboard-page.html b/webapp/idol/src/main/public/static/js/find/idol/templates/page/dashboard-page.html new file mode 100644 index 0000000000..a969f912c3 --- /dev/null +++ b/webapp/idol/src/main/public/static/js/find/idol/templates/page/dashboard-page.html @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/webapp/idol/src/main/resources/application.properties b/webapp/idol/src/main/resources/application.properties index ace93051ad..dc29a395e0 100644 --- a/webapp/idol/src/main/resources/application.properties +++ b/webapp/idol/src/main/resources/application.properties @@ -31,3 +31,6 @@ spring.jpa.properties.hibernate.default_schema=find spring.jpa.hibernate.ddl-auto=none spring.main.banner-mode=off spring.messages.basename=i18n/idol-errors,i18n/errors +# Increase the default max file upload size from 1MB, since we use large base64-encoded images for map .pptx export +spring.http.multipart.max-file-size=16Mb +spring.http.multipart.max-request-size=16Mb diff --git a/webapp/idol/src/main/resources/custom-application.properties b/webapp/idol/src/main/resources/custom-application.properties index 8675b0c725..6ebde724d3 100644 --- a/webapp/idol/src/main/resources/custom-application.properties +++ b/webapp/idol/src/main/resources/custom-application.properties @@ -12,4 +12,6 @@ idol.log.timing.enabled=true # Only used if server.reverseProxy is true server.ajp.port=8009 server.tomcat.resources.max-cache-kb=20480 +# Increase the connector max post size from 2097152, since we use large base64-encoded images for map .pptx export +server.tomcat.connector.max-post-size=16777216 find.reverse-proxy.pre-authenticated-roles=FindUser diff --git a/webapp/idol/src/main/resources/defaultIdolConfigFile.json b/webapp/idol/src/main/resources/defaultIdolConfigFile.json index 82b041abe0..1af0121fc2 100644 --- a/webapp/idol/src/main/resources/defaultIdolConfigFile.json +++ b/webapp/idol/src/main/resources/defaultIdolConfigFile.json @@ -107,6 +107,9 @@ "enabled": false, "baseUrl": "" }, + "powerpoint": { + "templateFile": "" + }, "uiCustomization": { "options": { "directAccessLink": { diff --git a/webapp/idol/src/test/java/com/hp/autonomy/frontend/find/idol/configuration/IdolFindConfigTest.java b/webapp/idol/src/test/java/com/hp/autonomy/frontend/find/idol/configuration/IdolFindConfigTest.java index 14256b1e0c..3a583731e3 100644 --- a/webapp/idol/src/test/java/com/hp/autonomy/frontend/find/idol/configuration/IdolFindConfigTest.java +++ b/webapp/idol/src/test/java/com/hp/autonomy/frontend/find/idol/configuration/IdolFindConfigTest.java @@ -9,6 +9,7 @@ import com.hp.autonomy.frontend.configuration.ConfigException; import com.hp.autonomy.frontend.configuration.authentication.CommunityAuthentication; import com.hp.autonomy.frontend.configuration.server.ServerConfig; +import com.hp.autonomy.frontend.find.core.configuration.PowerPointConfig; import com.hp.autonomy.frontend.find.core.configuration.SavedSearchConfig; import com.hp.autonomy.searchcomponents.idol.configuration.QueryManipulation; import com.hp.autonomy.searchcomponents.idol.view.configuration.ViewConfig; @@ -39,6 +40,9 @@ public class IdolFindConfigTest { @Mock private ViewConfig viewConfig; + @Mock + private PowerPointConfig powerPointConfig; + private IdolFindConfig idolFindConfig; @Before @@ -49,6 +53,7 @@ public void setUp() { .queryManipulation(queryManipulation) .savedSearchConfig(savedSearchConfig) .view(viewConfig) + .powerPoint(powerPointConfig) .build(); } @@ -70,6 +75,7 @@ public void merge() { when(queryManipulation.merge(any(QueryManipulation.class))).thenReturn(queryManipulation); when(savedSearchConfig.merge(any(SavedSearchConfig.class))).thenReturn(savedSearchConfig); when(viewConfig.merge(any(ViewConfig.class))).thenReturn(viewConfig); + when(powerPointConfig.merge(any(PowerPointConfig.class))).thenReturn(powerPointConfig); final IdolFindConfig defaults = IdolFindConfig.builder().content(mock(ServerConfig.class)).build(); final IdolFindConfig mergedConfig = idolFindConfig.merge(defaults); @@ -78,6 +84,7 @@ public void merge() { assertEquals(queryManipulation, mergedConfig.getQueryManipulation()); assertEquals(savedSearchConfig, mergedConfig.getSavedSearchConfig()); assertEquals(viewConfig, mergedConfig.getViewConfig()); + assertEquals(powerPointConfig, mergedConfig.getPowerPoint()); assertEquals(idolFindConfig, mergedConfig); } diff --git a/webapp/idol/src/test/java/com/hp/autonomy/frontend/find/idol/export/IdolExportControllerTest.java b/webapp/idol/src/test/java/com/hp/autonomy/frontend/find/idol/export/IdolExportControllerTest.java index 2153c5f567..62a1f473a0 100644 --- a/webapp/idol/src/test/java/com/hp/autonomy/frontend/find/idol/export/IdolExportControllerTest.java +++ b/webapp/idol/src/test/java/com/hp/autonomy/frontend/find/idol/export/IdolExportControllerTest.java @@ -6,8 +6,10 @@ package com.hp.autonomy.frontend.find.idol.export; import com.autonomy.aci.client.services.AciErrorException; +import com.hp.autonomy.frontend.configuration.ConfigService; import com.hp.autonomy.frontend.find.core.export.ExportController; import com.hp.autonomy.frontend.find.core.export.ExportControllerTest; +import com.hp.autonomy.frontend.find.idol.configuration.IdolFindConfig; import com.hp.autonomy.searchcomponents.core.search.StateTokenAndResultCount; import com.hp.autonomy.searchcomponents.core.search.TypedStateToken; import com.hp.autonomy.searchcomponents.idol.search.IdolDocumentsService; @@ -33,6 +35,8 @@ public class IdolExportControllerTest extends ExportControllerTest idolFindConfig; @Override protected ExportController constructController() throws IOException { @@ -49,7 +53,7 @@ protected ExportController constructControl when(queryRestrictionsBuilder.stateMatchId(anyString())).thenReturn(queryRestrictionsBuilder); when(queryRestrictionsBuilder.build()).thenReturn(queryRestrictions); - return new IdolExportController(requestMapper, controllerUtils, documentsService, exportService); + return new IdolExportController(requestMapper, controllerUtils, documentsService, exportService, objectMapper, idolFindConfig); } @Override diff --git a/webapp/pom.xml b/webapp/pom.xml index c7faa2e581..6ef7aae029 100644 --- a/webapp/pom.xml +++ b/webapp/pom.xml @@ -29,6 +29,7 @@ HEAD 0.42.0 + 1.0.0-SNAPSHOT ${project.build.outputDirectory} true