com.metaeffekt.artifact.analysis.dashboard.Dashboard Maven / Gradle / Ivy
The newest version!
/* * Copyright 2021-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.metaeffekt.artifact.analysis.dashboard; import com.metaeffekt.artifact.analysis.utils.FileAppendable; import j2html.tags.ContainerTag; import j2html.tags.DomContent; import j2html.tags.Tag; import j2html.tags.specialized.*; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.json.JSONObject; import org.metaeffekt.core.util.ColorScheme; import java.io.*; import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.util.*; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.zip.GZIPOutputStream; import static j2html.TagCreator.*; @Slf4j @Getter @Setter public class Dashboard { private String title, subtitle, description, author, authorUrl, version; private String[] contentSheetEntryNames = {"Sheet Name", "Sheet Names"}; private LinkTag favicon; private final List
if none. * @return The html generated from the markdown. */ public static SpanTag markdownToHtml(String markdown, String linkTarget) { if (markdown == null) { return span(); } final SpanTag markdownContent = span(); try { final String[] lines = markdown .replaceAll(" {2}\n", "\n\n") .split("(\n|\\\\n)(\r|\\\\r)?"); UlTag currentUlList = null; boolean lastLineWasHeader = false, lastLineWasEmpty = false; for (int i = 0; i < lines.length; i++) { lines[i] = lines[i] .replace("\\\"", "\""); // if line is empty, insert linebreak and skip line if ((lines[i].isEmpty() || lines[i].matches(" +")) && currentUlList == null && i + 1 < lines.length && !lastLineWasHeader) { markdownContent.with(br()); lastLineWasEmpty = true; continue; } else if (i + 1 < lines.length) { markdownContent.with(text(" ")); } // check if line is a header final Matcher headerMatcher = MD_HEADER_PATTERN.matcher(lines[i]); if (headerMatcher.matches()) { if (lastLineWasEmpty) { markdownContent.with(br()); } int headerSize = headerMatcher.group(1).length(); String headerText = headerMatcher.group(2); if (headerSize == 1) { markdownContent.with(h2(headerText)); } else if (headerSize == 2) { markdownContent.with(h3(headerText)); } else if (headerSize == 3) { markdownContent.with(h4(headerText)); } else if (headerSize == 4) { markdownContent.with(h5(headerText)); } lastLineWasHeader = true; continue; } ContainerTag> appendTo = markdownContent; // check if line is a list element final Matcher ulMatcher = MD_UL_PATTERN.matcher(lines[i]); final boolean ulMatches = ulMatcher.matches(); if (!ulMatches && currentUlList != null) { appendTo.with(currentUlList); currentUlList = null; } else if (ulMatches) { lines[i] = ulMatcher.group(1); if (currentUlList == null) { currentUlList = ul(); } appendTo = currentUlList; } // find and store links final Matcher linkMatcher = MD_LINK_PATTERN.matcher(lines[i]); final Mapkeywords = new ArrayList<>(); private final List sheets = new ArrayList<>(); private final List modals = new ArrayList<>(); private final List sidebarElements = new ArrayList<>(); private final List navigationColumnHeaders = new ArrayList<>(); private final SpanTag additionalContent = span(); private final List additionalBottomLeftBadges = new ArrayList<>(); private final List additionalInformationModalContent = new ArrayList<>(); private boolean enableChartJs = false; private final List dashboardLicenseNotices = new ArrayList<>(); private final int topRightIconSize = 22; private final Map > navigationFilterTemplates = new LinkedHashMap<>(); private final StringBuilder onFilterApplied = new StringBuilder(); private final StringBuilder onThemeChange = new StringBuilder(); private String emptyDashboardText = "There are no sheets to display."; private int emptyDashboardTextSize = 141; private String emptyDashboardIcon = ""; // checkmark public Dashboard() { try { final List bootstrapIconsLicense = readResourceAsStringList(Dashboard.class, "dashboard/licenses/bootstrap-icons.txt"); this.addLicenseText("Bootstrap Icons", String.join("\n", bootstrapIconsLicense), d -> true); final List chartJsLicense = readResourceAsStringList(Dashboard.class, "dashboard/licenses/chart-js.txt"); this.addLicenseText("ChartJs", String.join("\n", chartJsLicense), d -> d.enableChartJs); } catch (IOException e) { throw new RuntimeException("Failed to read license file.", e); } } public void setContentSheetEntryNames(String singular, String plural) { this.contentSheetEntryNames = new String[]{singular, plural}; } public void addKeyword(String keyword) { this.keywords.add(keyword); } public void addSheet(Sheet sheet) { synchronized (this.sheets) { this.sheets.add(sheet); } } public void removeSheet(Sheet sheet) { synchronized (this.sheets) { this.sheets.remove(sheet); } } public List getSheets() { synchronized (this.sheets) { return sheets; } } public void addModal(Modal modal) { this.modals.add(modal); } public void removeModal(Modal modal) { this.modals.remove(modal); } public void addSidebarElement(SidebarElement sidebarElement) { this.sidebarElements.add(sidebarElement); } public void removeSidebarElement(SidebarElement sidebarElement) { this.sidebarElements.remove(sidebarElement); } public void addNavigationColumnHeader(String header, NavigationCellStyle style, NavigationColumnHeaderConfig.Alignment alignment) { this.addNavigationColumnHeader(header, style, alignment, false); } public void addNavigationColumnHeader(String header, NavigationCellStyle style, NavigationColumnHeaderConfig.Alignment alignment, boolean hideIfNoData) { this.navigationColumnHeaders.add(new NavigationColumnHeaderConfig(header, style, alignment, hideIfNoData)); } public void setChartJsEnabled(boolean enabled) { this.enableChartJs = enabled; } public boolean isChartJsEnabled() { return enableChartJs; } public void addAdditionalContent(Tag> content) { additionalContent.with(content); } public SpanTag getEditableAdditionalContent() { return additionalContent; } public void addBottomLeftBadge(SpanTag badgeContent) { additionalBottomLeftBadges.add(badgeContent.withClasses("badge", "badge-primary")); } public void addBottomLeftBadge(SpanTag badgeContent, String style) { additionalBottomLeftBadges.add(badgeContent.withClasses("badge", "badge-" + style)); } public void addBottomLeftBadge(String badgeContent) { additionalBottomLeftBadges.add(span(badgeContent).withClasses("badge", "badge-primary")); } public void addLicenseText(String identifier, String license, Function includeFunction) { dashboardLicenseNotices.add(0, new DashboardLicenseNotice(identifier, license, includeFunction)); } public void addNavigationFilterTemplates(String text, NavigationFilter... filter) { navigationFilterTemplates.computeIfAbsent(text, k -> new ArrayList<>()).addAll(Arrays.asList(filter)); } public void addOnFilterApplied(String script) { onFilterApplied.append(rawHtml(script)); } public void addOnThemeChange(String script) { onThemeChange.append(rawHtml(script)); } public void addAdditionalInformationModalContent(DomContent... content) { additionalInformationModalContent.addAll(Arrays.asList(content)); } private final static String[] FILTER_OPERATIONS = {"contains", "not contains", "equal", "not equal", "larger", "larger/equal", "smaller/equal", "smaller"}; private void addLibraryFilesToHead(HeadTag dashboardHead) { dashboardHead.with(style(ColorScheme.cssRoot())); try { // ChartJs, but only if it is enabled if (enableChartJs) { log.info("ChartJs is enabled for dashboard, adding JS contents"); List chartJS = readResourceAsStringList(Dashboard.class, "dashboard/chart.js"); if (!chartJS.isEmpty()) { dashboardHead.with(script(String.join("\n", chartJS))); } } // custom JS List dashboardJS = readResourceAsStringList(Dashboard.class, "dashboard/dashboard.js"); if (!dashboardJS.isEmpty()) { dashboardHead.with(script(String.join("\n", dashboardJS))); } // custom CSS List dashboardCss = readResourceAsStringList(Dashboard.class, "dashboard/dashboard.css"); if (!dashboardCss.isEmpty()) { dashboardHead.with(style(dashboardCss.stream().map(String::trim).collect(Collectors.joining()))); } } catch (IOException e) { throw new RuntimeException("Unable to read library files for dashboard generation [" + getTitle() + "]", e); } } private TrTag generateNavigationRow(Sheet sheet, List navigationColumnHeaders, Map maxEntryLengths) { TrTag tr = tr() .withClasses("slightBorder", "navigation-entry") .attr("onclick", "showSheet('" + sheet.getId() + "')") .withId("nav-row-" + sheet.getId()); for (int i = 0; i < navigationColumnHeaders.size(); i++) { final NavigationColumnHeaderConfig navigationColumnHeader = navigationColumnHeaders.get(i); final NavigationCellStyle navigationCellStyle = navigationColumnHeader.getStyler(); final Map entries = sheet.getNavigationRow().getEntries(); final TdTag td = td(); if (i == 0) { td.attr("oncontextmenu", "toggleDataSheetBookmark('" + sheet.getId() + "');return false;"); } if (entries.containsKey(navigationColumnHeader.getName())) { final NavigationRow.Entry cellEntry = entries.get(navigationColumnHeader.getName()); // if the value is a number: js does not sort numbers correctly if they are in a string format. To // ensure it still sorts them correctly, all the numbers in the column have to be of the same length final DomContent strCellValue; if (cellEntry.getContent().getClass() == j2html.tags.Text.class && cellEntry.getContent().render().matches("-?\\d+.?(?:\\d+)?")) { strCellValue = text(fillStringFromLeft(cellEntry.getContent().render(), maxEntryLengths.getOrDefault(navigationColumnHeader.getName(), 0))); } else { strCellValue = cellEntry.getContent(); } final ATag cellValueTag = a(strCellValue).withClasses("nav-link", "navigation-tile"); if (navigationCellStyle != null) { final String style = cellEntry.computeStyle(navigationCellStyle); if (style != null) { cellValueTag.withStyle(style); } } if (i == 0) { cellValueTag.withId("navigation-tile-" + sheet.getId()); } td.with(cellValueTag); } tr.with(td); } return tr; } public void sortSheets(Comparator comparator) { for (Sheet sheet : sheets) { if (sheet.getId() == null) { throw new IllegalStateException("Sheet has no ID, cannot sort sheets. This is most likely an issue in the dashboard implementation."); } } sheets.sort(comparator); } /** * Adds as many space characters on the left side of the string required for it to reach the given total length.
* Example: s ="test"
, totalLength =6
, will return" test"
* * @param s The string to add the padding to. * @param totalLength The total length the string should be in the end. * @return A string filled with space characters from the left with the given length or longer, if the initial * string was already longer. */ private String fillStringFromLeft(String s, int totalLength) { if (s.length() >= totalLength) { return s; } StringBuilder sBuilder = new StringBuilder(s); while (sBuilder.length() < totalLength) { sBuilder.insert(0, " "); } return sBuilder.toString(); } private boolean columnHasData(String columnName) { for (Sheet sheet : sheets) { if (sheet.getNavigationRow().getEntries().containsKey(columnName)) { final String content = sheet.getNavigationRow().getEntries().get(columnName).getContent().render(); if (!content.contains("N/A")) { return true; } } } return false; } private ListfilterApplicableNavigationColumnHeaders() { return navigationColumnHeaders.stream().filter(navigationColumnHeader -> { if (navigationColumnHeader.isHideIfNoData()) { return columnHasData(navigationColumnHeader.getName()); } return true; }).collect(Collectors.toList()); } private TdTag buildNavigation() { TheadTag navigationTableHead = thead().withStyle("position: sticky; top: -10px; background-color: var(--background-color); box-shadow: 0px -10px 0px 0px var(--background-color);"); TrTag navigationTableHeaderElements = tr().attr("valign", "bottom"); final List filteredNavigationColumnHeader = filterApplicableNavigationColumnHeaders(); for (int i = 0; i < filteredNavigationColumnHeader.size(); i++) { final NavigationColumnHeaderConfig headerItem = filteredNavigationColumnHeader.get(i); if (headerItem.isHideIfNoData()) { if (!columnHasData(headerItem.getName())) { continue; } } ThTag thElement = th(rawHtml(headerItem.getName())) .attr("onclick", "onColumnHeaderClicked(event.currentTarget);") .attr("oncontextmenu", "onColumnHeaderRightClicked(event.currentTarget);return false;") .withId("header-table-id-" + i) .withClasses("navigation-header-tile", "clickable", headerItem.getAlignment().cssClass()); navigationTableHeaderElements.with(thElement); } navigationTableHead.with(navigationTableHeaderElements); final Map maxEntryLengths = new HashMap<>(); for (Sheet sheet : sheets) { final Map entries = sheet.getNavigationRow().getEntries(); for (Map.Entry entry : entries.entrySet()) { int currentLength = entry.getValue().getContent().render().length(); if (maxEntryLengths.containsKey(entry.getKey())) { if (currentLength > maxEntryLengths.get(entry.getKey())) { maxEntryLengths.put(entry.getKey(), currentLength); } } else { maxEntryLengths.put(entry.getKey(), currentLength); } } } final TbodyTag navigationTableBody = tbody(); for (Sheet sheet : sheets) { navigationTableBody.with(generateNavigationRow(sheet, filteredNavigationColumnHeader, maxEntryLengths)); } return td().withId("navigation-table").withClasses("navigation-table-container", "hidden", "card-design", "scrollbox", "horizontal-resize").with( table().withId("navigation-table-table").withClass("navigation-table").with( navigationTableHead, navigationTableBody ) ); } private SpanTag getTopRightSidebar() { DivTag alwaysVisible = div().withId("top-right-badges").withClasses("top-right-badges", "card-design", "hidden"); DivTag overhang = div().withId("top-right-badges-overhang").withClasses("card-design", "hidden"); DivTag itemStorage = div().withId("top-right-badges-storage").withClass("hidden"); SpanTag completeSidebar = span(); completeSidebar.with(alwaysVisible, overhang, itemStorage); // the go-back-arrow shall be the first element itemStorage.with( div().withClasses("clickable", "unselectable", "hidden", "previous-sheet-arrow").attr("onclick", "historyBackwards();").with( SvgIcon.ARROW_LEFT_CIRCLE_FILL.getTag(topRightIconSize) ).attr("tooltip", "Go back one sheet Backspace
") ); // add the custom sidebar elements for (SidebarElement sidebarElement : sidebarElements) { if (sidebarElement.getIcon() != null) { itemStorage.with( div().withClasses("clickable", "unselectable").attr("onclick", sidebarElement.getJs()).with( sidebarElement.getIcon().getTag(topRightIconSize) ).attr("tooltip", sidebarElement.getTitle()) ); } } // add the custom sidebar elements for (Modal modal : modals) { if (modal.isShowInSidebar()) { SidebarElement sidebarElement = modal.getSidebarElement(); if (sidebarElement.getIcon() != null) { itemStorage.with( div().withClasses("clickable", "unselectable").attr("onclick", sidebarElement.getJs()).with( sidebarElement.getIcon().getTag(topRightIconSize) ).attr("tooltip", sidebarElement.getTitle() + (modal.getToggleKey() != null ? "" + modal.getToggleKey().replace("Key", "") + "
" : "")) ); } } } // add default elements itemStorage.with( div().withClasses("clickable", "unselectable").attr("onclick", "previousDisplayMode(true);").with( SvgIcon.LAYOUT_TEXT_SIDEBAR.getTag(topRightIconSize) ).attr("tooltip", "Toggle display modealt/opt + →/←
"), div().withClasses("clickable", "unselectable").attr("onclick", "openModal('settingsModal');").with( SvgIcon.GEAR_FILL.getTag(topRightIconSize) ).attr("tooltip", "Settingss
"), iff(additionalInformationModalContent.size() > 0, div().withClasses("clickable", "unselectable").attr("onclick", "openModal('dashboardAdditionalInfo');").with( SvgIcon.INFO_FILL.getTag(topRightIconSize) ).attr("tooltip", "Additional Informationa
")) , div().withClasses("clickable", "unselectable").attr("onclick", "openModal('dashboardLicenses');").with( SvgIcon.FLAG_FILL.getTag(topRightIconSize) ).attr("tooltip", "Dashboard Content and Code Licensesl
"), div().withClasses("clickable", "unselectable").withId("dark-mode-switch") .attr("onclick", "toggleManualDarkMode();") .attr("data-svg-light-mode", SvgIcon.SUN_FILL.getTag(topRightIconSize)) .attr("data-svg-dark-mode", SvgIcon.MOON_FILL.getTag(topRightIconSize - 2)) .with( SvgIcon.SUN_FILL.getTag(topRightIconSize) ).attr("tooltip", "Switch theme"), div().withId("top-right-items-show-more-storage").withClasses("clickable", "unselectable").attr("onclick", "topRightBadgesOverhangToggle();").with( SvgIcon.THREE_DOTS_VERTICAL.getTag(topRightIconSize) ) ); return completeSidebar; } private void addSidebarElementToTable(TableTag table, SidebarElement sidebarElement) { table.with( tr().withClasses("unselectable", "clickable").attr("onclick", sidebarElement.getJs()).with( td(iff(sidebarElement.getIcon() != null, sidebarElement.getIcon().getTag(topRightIconSize) ), td().withClass("fly-in-sidebar-text").with(b(sidebarElement.getTitle())) ) ) ); } private static ByteArrayOutputStream gzipCompress(String dataToBeEncoded) throws IOException { final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); try (final GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream)) { gzipOutputStream.write(dataToBeEncoded.getBytes(StandardCharsets.UTF_8)); } return byteArrayOutputStream; } private static String base64Encode(byte[] dataToBeEncoded) { return Base64.getEncoder().encodeToString(dataToBeEncoded); } private SpanTag buildSheetContents() { SpanTag dashboardSheetContent = span(); if (!sheets.isEmpty()) { log.info("Building [{}] content sheets", sheets.size()); final ScriptTag sheetsScriptTag = script(); for (Sheet sheet : sheets) { // https://nvd.nist.gov/vuln/detail/CVE-2022-25881 contains two carriage returns trailing the text, breaking the process on windows final String key = sheet.getId().replace("\n", "LNBRK").replace("\r", "CARRET").replace("\\", "\\\\").replace("'", "\\'").replace("<", "OPENTAG").replace(">", "CLOSETAG"); final String value = sheet.getContent().render(); try { sheetsScriptTag.with(rawHtml("sheetsMap.set('" + key + "', '" + base64Encode(gzipCompress(value).toByteArray()) + "');")); } catch (IOException e) { throw new RuntimeException("Failed to compress and encode sheet content for sheet [" + sheet.getId() + "]", e); } } dashboardSheetContent.with( br(), br(), table().withClass("selection-sheet-table").with( // both sides of the main content table tr( buildNavigation(), td(sheetsScriptTag).withClass("right-sheet").withId("sheet-container") ) ), br(), getTopRightSidebar() ); } else { log.info("Dashboard has no data sheets"); String noVulnerabilitiesBuilder = "" + "" + emptyDashboardText + "" + emptyDashboardIcon; dashboardSheetContent.with(rawHtml(noVulnerabilitiesBuilder)); } return dashboardSheetContent; } public ListgetSheetParagraphTitles() { List titles = new ArrayList<>(); for (Sheet sheet : sheets) { for (SheetParagraph paragraph : sheet.getParagraphs()) { if (paragraph.getIdentifier() != null && !titles.contains(paragraph.getIdentifier())) { titles.add(paragraph.getIdentifier()); } } } return titles; } private List getSecureSheetParagraphTitles() { List titles = new ArrayList<>(); for (Sheet sheet : sheets) { for (SheetParagraph paragraph : sheet.getParagraphs()) { if (paragraph.getSecureIdentifier() != null && !titles.contains(paragraph.getSecureIdentifier())) { titles.add(paragraph.getSecureIdentifier()); } } } return titles; } private SpanTag buildModals() { final Modal settingsModal = buildSettingsModal(); // add a modal that contains all license information final Modal licenseModal = new Modal(); licenseModal.setId("dashboardLicenses"); licenseModal.setTitle("Dashboard Content and Code Licenses"); licenseModal.setShowInSidebar(false); licenseModal.setToggleKey("KeyL"); licenseModal.setToggleKeyActive(true); licenseModal.setSize(Modal.Size.LARGE); licenseModal.with(br()); for (Map.Entry license : getApplicableLicenseNotices().entrySet()) { licenseModal .with(h2(license.getKey())) .with(rawHtml(String.valueOf(license.getValue()).replace("\n", "
"))) .with(br(), br()); } // add a modal that contains all license information final Modal dashboardAdditionalInfoModal = new Modal(); dashboardAdditionalInfoModal.setId("dashboardAdditionalInfo"); dashboardAdditionalInfoModal.setTitle("Additional Information"); dashboardAdditionalInfoModal.setShowInSidebar(false); dashboardAdditionalInfoModal.setToggleKey("KeyA"); dashboardAdditionalInfoModal.setToggleKeyActive(true); dashboardAdditionalInfoModal.setSize(Modal.Size.LARGE); for (DomContent domContent : additionalInformationModalContent) { dashboardAdditionalInfoModal.with(domContent); } final SpanTag modalsTag = span(); modalsTag.with(script("const onOpenModalScripts={};")); modalsTag.with(settingsModal.generateContent()); modalsTag.with(licenseModal.generateContent()); modalsTag.with(dashboardAdditionalInfoModal.generateContent()); // build the rest of the modals final ListotherModalContents = new ArrayList<>(); for (Modal modal : modals) { if (modal == null) { continue; } log.info("Adding custom modal [{}]", modal.getTitle()); otherModalContents.add(modal.generateContent()); } otherModalContents.forEach(modalsTag::with); return modalsTag; } private Modal buildSettingsModal() { // build the buttons that control sorting order final DivTag displaySettingsSortingButtons = div().withStyle("display:inline;"); displaySettingsSortingButtons.with( b("Navigation sorting"), br() ); final List filteredNavigationColumnHeader = filterApplicableNavigationColumnHeaders(); for (int i = 0; i < filteredNavigationColumnHeader.size(); i++) { final NavigationColumnHeaderConfig headerItem = filteredNavigationColumnHeader.get(i); if (headerItem.isHideIfNoData()) { if (!columnHasData(headerItem.getName())) { continue; } } final String actualItemWithoutBr = headerItem.getName().replaceAll("-?
", ""); displaySettingsSortingButtons.with( button(actualItemWithoutBr) .withId("header-table-sort-" + i) .withClasses("btn", "btn-secondary", "btn-sm", "header-table-sort") .attr("onclick", "onColumnHeaderClicked(document.getElementById('header-table-id-" + i + "'))") ); } // switches to toggle data sheet content visibility final ListvisibilityToggleDataLabels = getSheetParagraphTitles(); final List visibilityToggleDataIdentifiers = getSecureSheetParagraphTitles(); final DivTag displaySettingsContentToggles = div().withStyle("display:inline;"); displaySettingsContentToggles.with( b("Data-Sheet content visibility") ); final ScriptTag dataVisibilityToggleIdentifiers = script().with(rawHtml("const visibilityToggleDataIdentifiers = [];")); for (int i = 0; i < visibilityToggleDataIdentifiers.size(); i++) { String dataToggleIdentifier = "data-sheet-content-toggle-" + visibilityToggleDataIdentifiers.get(i); dataVisibilityToggleIdentifiers.with(rawHtml("visibilityToggleDataIdentifiers.push('" + dataToggleIdentifier + "');")); displaySettingsContentToggles.with( div().withClass("form-check form-switch").with( input().withClass("form-check-input") .withType("checkbox") .withId(dataToggleIdentifier) .attr("onclick", "toggleDataSheetContentVisibility('" + visibilityToggleDataIdentifiers.get(i) + "', true);") .attr("checked"), label(visibilityToggleDataLabels.get(i)).withClass("form-check-label") .attr("for", dataToggleIdentifier) ) ); } // search bar to filter sheets final SpanTag displaySettingsFiltersList = span().with( b("Navigation Filters"), br(), span("+") .withClasses("badge", "badge-primary", "clickable") .withStyle("margin-bottom:5px") .attr("onclick", "addFilter('','','')"), iff(!navigationFilterTemplates.isEmpty(), join( each(navigationFilterTemplates, e -> span("+ " + e.getKey()) .withClasses("badge", "badge-secondary", "clickable") .attr("onclick", "createFilterFromGETValue('" + e.getValue().stream().map(NavigationFilter::toString).collect(Collectors.joining(",")) + "');") ) ) ), span("Clear All") .withId("display-filter-clear-filters") .withClasses("badge", "badge-danger", "clickable", "hidden") .withStyle("margin-bottom:5px") .attr("onclick", "clearAllFilters();"), br(), div().withId("display-filter-listing").withClasses("card", "card-margin", "card-noflex", "hidden"), br(), script("const FILTER_OPERATIONS = [\"" + String.join("\", \"", FILTER_OPERATIONS) + "\"];") ); final SpanTag copyAllSheetNamesToClipboard = span().with( b("Copy content"), br(), span("Copy shown " + contentSheetEntryNames[1]) .withClasses("badge", "badge-primary", "clickable") .attr("onclick", "copyContentSheetNames();this.innerHTML='Copied!';setTimeout(() => { this.innerHTML='Copy shown " + contentSheetEntryNames[1] + "'; }, 1400);") ); // list the shortcuts and other hints final SpanTag displaySettingsHints = span().with( b("Notes"), br(), i(join( "Press ", code("esc"), " to close any modal.", br(), "Use ", code("alt"), "/", code("opt"), " + ", code("↑"), "/", code("↓"), " to navigate through the data sheets.", br(), "Use ", code("alt"), "/", code("opt"), " + ", code("→"), "/", code("←"), " to switch between display modes (content sheets/navigation).", br(), "Use ", code("alt"), "/", code("opt"), " + ", code("s"), " to create a new sheet content search filter.", br(), "Hold the ", code("alt"), " key to keep a tooltip static when moving the cursor.", br(), "Right-click the data sheet ID in the navigation to bookmark the data sheet.", br(), "The current display settings will be restored on reloading the page.", br(), "The navigation table headers can be left-clicked to sort the navigation.", br(), "The navigation table headers can be right-clicked to create new filters." )) ); final Modal settingsModal = new Modal(); settingsModal.setId("settingsModal"); settingsModal.setTitle("Display Settings"); settingsModal.setToggleKey("KeyS"); settingsModal.setToggleKeyActive(false); settingsModal.setSvgIcon(SvgIcon.GEAR_FILL); settingsModal.with( dataVisibilityToggleIdentifiers, displaySettingsSortingButtons, br(), br(), displaySettingsFiltersList, displaySettingsContentToggles, br(), copyAllSheetNamesToClipboard, br(), br(), displaySettingsHints ); return settingsModal; } private Map getApplicableLicenseNotices() { return dashboardLicenseNotices.stream() .filter(e -> e.applies(this)) .collect(Collectors.toMap( DashboardLicenseNotice::getTitle, DashboardLicenseNotice::getContent, (u, v) -> u + " - duplicate", LinkedHashMap::new )); } private void addLicensesToScriptTag(ScriptTag scriptTag) { Map licenses = getApplicableLicenseNotices(); scriptTag.with(rawHtml("let licenseMap = \n/*START-LICENSE-MAP*/\n" + new JSONObject(licenses) + "\n/*END-LICENSE-MAP*/\n;")); log.info("License texts added: {}", licenses.keySet()); } public void generateIntoFile(File outputFile) throws IOException { if (!outputFile.getParentFile().exists()) { outputFile.mkdirs(); } final FileAppendable writer = new FileAppendable(outputFile, StandardCharsets.UTF_8, true); writer.append(""); final HtmlTag generated = generate(); log.info("Writing generated dashboard to {}", outputFile.getAbsolutePath()); generated.renderModel(writer, null); // flush the writer writer.append("\n"); writer.flush(); writer.close(); log.info("Done generating dashboard with size [{} MB]", getStringMBSize(outputFile.length())); } public HtmlTag generate() throws IOException { log.info("Generating dashboard [{}] version [{}]", getTitle(), getVersion()); // build the HTML head HeadTag dashboardHead = head(); dashboardHead.with( meta().withCharset("utf-8"), iff(title != null, title().with(text(title))), iff(description != null, meta().withName("description").withContent(description)), iff(!keywords.isEmpty(), meta().withName("keywords").withContent(String.join(", ", keywords))), iff(author != null, meta().withName("author").withContent(author)), meta().withName("viewport").withContent("width=device-width, initial-scale=1.0"), iff(favicon != null, favicon) ); addLibraryFilesToHead(dashboardHead); SpanTag dashboardSheetContent = buildSheetContents(); ScriptTag finalScripts = script().with(rawHtml("\n" + "hideRequiredSheetParagraphs(true);\n" + "checkForTableRowsSortedInit();\n" + "var viewedSheet = findGetParameter('sheet');\n" + // restore hidden sheet data, restore last viewed cve "isFirstSheet = true;\n"), rawHtml(!sheets.isEmpty() ? "let defaultSheet = '" + sheets.get(0).getId() + "';\n" : "let defaultSheet = '';\n"), rawHtml("" + "async function showFirstSheet() {\n" + " if (viewedSheet != null && viewedSheet.length > 0) {\n" + " showSheet(viewedSheet.replaceAll('_', '-'));\n" + " }"), // otherwise, show the navigation full-screen rawHtml(!sheets.isEmpty() ? "" + " else {" + " setDisplayMode(0);\n" + " }\n" : "\n"), rawHtml("}\n" + "setTimeout(function() { showFirstSheet(); }, 100);\n" + "isFirstSheet = false;\n" + "updateUrl = true;\n" + "let modalsToClose = document.getElementsByClassName('modal');\n" + // when the user clicks anywhere outside the modal, close it "window.onclick = function(event) {\n" + " if (modalsToClose == null) return;\n" + " for (let i = 0; i < modalsToClose.length; i++) {\n" + " if (event.target == modalsToClose[i]) {\n" + " closeAllModals();\n" + " }\n" + " }\n" + "}\n" + "const removeChartJsDisplayBlock = document.getElementsByClassName('chart-js-remove-display-block');\n" + // ChartJs automatically adds a few tags to the canvas element, which is not what we need. This removes them. "for (var index = 0; index < removeChartJsDisplayBlock.length; index++) {\n" + " removeChartJsDisplayBlock[index].style.removeProperty('display');\n" + " removeChartJsDisplayBlock[index].style.removeProperty('box-sizing');\n" + "}\n" + "function removeOnLoadFunction() {\n" + // remove all elements that have the 'remove-on-load' class (loading animation) " var removeOnLoad = document.getElementsByClassName('remove-on-load');\n" + " for (var index = 0; index < removeOnLoad.length; index++) {\n" + " removeOnLoad[index].parentNode.removeChild(removeOnLoad[index]);\n" + " }\n" + "}\n" + "setTimeout(function() { removeOnLoadFunction(); }, 100);\n" + "function onFilterApplied() {" + onFilterApplied + "}\n" + "function onThemeChanged(darkMode) {" + onThemeChange + "}" )); addLicensesToScriptTag(finalScripts); ScriptTag onInitScripts = script("" + "document.getElementById('loading-animation').style.display='block';\n" + "document.getElementById('js-is-disabled').style.display='none';\n"); // build the HTML body final BodyTag dashboardBody = body(); dashboardBody.with( div().withClasses("spinner-grow", "text-primary", "remove-on-load") // loading animation .withId("loading-animation") .attr("role", "status") .withStyle("display:none;position:absolute;top:49vh;left:49vw;width:50px;height:50px;color:var(--strong-blue);"), span("Please enable JS to use this document") .withId("js-is-disabled") .withStyle("display:block;position:absolute;top:40vh;left:40vw;font-size:20px;color:var(--strong-red);border: 2px solid var(--strong-red);border-radius:5px;padding:8px;"), onInitScripts, div().withClass("main-container").withStyle("margin-top: 20px; margin-left: 30px").with( span( span(title != null ? title : "Dashboard").withId("dashboard-title").withClass("primary-header"), span(br()).withClass("hidden-on-desktop"), span().withClass("hidden-on-mobile").withStyle("margin-left: 5px;"), iffElse(subtitle != null, span(subtitle).withId("dashboard-subtitle").withStyle("margin-left:6px;"), span().withId("dashboard-subtitle")) ).withId("dashboard-full-title"), dashboardSheetContent, div().withClass("bottom-left-badges").with( a().withClasses("badge", "badge-primary").withStyle("color:var(--text-color-white);").withHref("http://www.metaeffekt.com/").withTarget("metaeffekt").with( span("Powered by {metæffekt}") ), iff(version != null, div(version).withClasses("badge", "badge-secondary")), div().withClasses("badge", "badge-secondary").withText( "Generated " + new SimpleDateFormat("yyyy-MM-dd HH:mm").format(new Date()) ), each(additionalBottomLeftBadges, badge -> badge) ) ), buildModals(), finalScripts, iff(additionalContent.getNumChildren() > 0, additionalContent) ); return html().withLang("en-US") .with(dashboardHead, dashboardBody); } private String getStringMBSize(String s) { return String.valueOf(Math.round((s.getBytes(StandardCharsets.UTF_8).length / 1e+6) * 100.0) / 100.0); } private String getStringMBSize(long s) { return String.valueOf(Math.round((s / 1e+6) * 100.0) / 100.0); } /** * Creates an {@link InputStream} from the given filename.
* Used to access a file from the resources directory. * * @param filename The path to the file, starting at the resource directory. * @return The {@link InputStream} with the file. */ private static InputStream getFileFromResourceAsStream(Class> loadingClass, String filename) { return loadingClass.getClassLoader().getResourceAsStream(filename); } /** * Creates an {@link InputStream} from the given filename to read it into a String {@link List}.
* Used to access a file from the resources directory. * * @param loadingClass The class to use for resource loading. * @param filename The path to the file, starting at the resource directory. * @return The {@link List} containing the file lines. * @throws IOException If the file could not be read. */ public static ListreadResourceAsStringList(Class> loadingClass, String filename) throws IOException { // FIXME: unclosed input stream final InputStream inputStream = getFileFromResourceAsStream(loadingClass, filename); if (inputStream == null) { throw new FileNotFoundException("File not found: " + filename); } final BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); final List lines = new ArrayList<>(); String line = reader.readLine(); while (line != null) { lines.add(line); line = reader.readLine(); } return lines; } public static String attemptEscapeScripts(String rawHtml) { return rawHtml .replace("onload", "_onload") .replace("onerror", "_onerror") .replace("null links = new HashMap<>(); while (linkMatcher.find()) { String entireMatch = linkMatcher.group(1); String linkText = linkMatcher.group(2); String linkHref = linkMatcher.group(3); ATag aTag = a(linkText).withHref(linkHref); if (linkTarget != null) { aTag.withTarget(linkTarget); } links.put(entireMatch, aTag); } // find and store code fields final Matcher codeMatcher = MD_CODE_PATTERN.matcher(lines[i]); final Map code = new HashMap<>(); while (codeMatcher.find()) { String entireMatch = codeMatcher.group(1); String codeText = codeMatcher.group(2); code.put(entireMatch, code(codeText)); } // find and store code fields final Matcher boldMatcher = MD_BOLD_PATTERN.matcher(lines[i]); final Map bold = new HashMap<>(); while (boldMatcher.find()) { String entireMatch = boldMatcher.group(1); String boldText = boldMatcher.group(2); bold.put(entireMatch, b(boldText)); } // iterate over all characters to replace special formatting final char[] chars = lines[i].toCharArray(); final SpanTag currentLineTag = span(); String checkBuffer = ""; int codeTickCount = 0; boolean isInsideBold = false; for (int j = 0; j < chars.length; j++) { if (chars[j] == '`') { codeTickCount++; } if (!links.isEmpty() && chars[j] == '[') { currentLineTag.with(rawHtml(checkBuffer)); checkBuffer = ""; } else if (!code.isEmpty() && codeTickCount % 2 == 1 && chars[j] == '`') { currentLineTag.with(rawHtml(checkBuffer)); checkBuffer = ""; } else if (chars[j] == '*' && chars.length > j + 1 && chars[j + 1] != '*' && !isInsideBold) { isInsideBold = true; currentLineTag.with(rawHtml(checkBuffer.replaceAll("\\*$", ""))); checkBuffer = "*"; } else if (chars[j] == '*' && chars.length > j + 1 && chars[j + 1] != '*' && isInsideBold) { isInsideBold = false; } checkBuffer += chars[j]; if (links.containsKey(checkBuffer)) { currentLineTag.with(links.getOrDefault(checkBuffer, a(checkBuffer).withStyle("color:red;"))); checkBuffer = ""; } if (code.containsKey(checkBuffer)) { currentLineTag.with(code.getOrDefault(checkBuffer, code(checkBuffer).withStyle("color:red;"))); checkBuffer = ""; } if (bold.containsKey(checkBuffer)) { currentLineTag.with(bold.getOrDefault(checkBuffer, b(checkBuffer).withStyle("color:red;"))); checkBuffer = ""; } } if (!checkBuffer.isEmpty()) { currentLineTag.with(rawHtml(checkBuffer)); } if (currentLineTag.getNumChildren() > 0) { if (appendTo instanceof SpanTag) { appendTo.with(currentLineTag); } else { appendTo.with(li(currentLineTag)); } } lastLineWasHeader = false; lastLineWasEmpty = false; } if (currentUlList != null && currentUlList.getNumChildren() > 0) { markdownContent.with(currentUlList); } } catch (Exception ignored) { } return markdownContent; } private final static Pattern MD_HEADER_PATTERN = Pattern.compile("^ ?(#+) ?(.+) ?$"); private final static Pattern MD_LINK_PATTERN = Pattern.compile("(\\[([^\\[\\]]+)]\\(([^()]+)\\))"); private final static Pattern MD_UL_PATTERN = Pattern.compile("^ ?[*\\-+](?![*\\-+]) ?(.+)$"); private final static Pattern MD_CODE_PATTERN = Pattern.compile("(`([^`]+)`)"); private final static Pattern MD_BOLD_PATTERN = Pattern.compile("(\\*+([^*]+)\\*+)"); public static class NavigationFilter { private String column; private FilterOperation operation; private String value; private NavigationFilter() { } @Override public String toString() { return column + ";" + operation.name + ";" + value; } } public static class NavigationFilterBuilder { private final NavigationFilter filter = new NavigationFilter(); private NavigationFilterBuilder() { } public NavigationFilterBuilder column(String column) { filter.column = column; return this; } public NavigationFilterBuilder operation(FilterOperation operation) { filter.operation = operation; return this; } public NavigationFilterBuilder value(Object value) { filter.value = value.toString(); return this; } public NavigationFilter build() { return filter; } } public enum FilterOperation { CONTAINS("contains"), NOT_CONTAINS("not contains"), EQUALS("equal"), NOT_EQUALS("not equal"), LARGER("larger"), SMALLER("smaller"), LARGER_EQUALS("larger/equal"), SMALLER_EQUALS("smaller/equal"); public final String name; FilterOperation(String name) { this.name = name; } } private static class DashboardLicenseNotice { private final String title; private final String content; private final Function includeCondition; private DashboardLicenseNotice(String title, String content, Function includeCondition) { this.title = title; this.content = content; this.includeCondition = includeCondition; } public String getTitle() { return title; } public String getContent() { return content; } public Function getIncludeCondition() { return includeCondition; } public boolean applies(Dashboard dashboard) { return includeCondition.apply(dashboard); } } public static NavigationFilterBuilder navigationFilterBuilder() { return new NavigationFilterBuilder(); } }
© 2015 - 2025 Weber Informatics LLC | Privacy Policy