Autopsy  4.19.1
Graphical digital forensics platform for The Sleuth Kit and other tools.
AnnotationUtils.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2021 Basis Technology Corp.
5  * Contact: carrier <at> sleuthkit <dot> org
6  *
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  * http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  */
19 package org.sleuthkit.autopsy.contentviewers.annotations;
20 
21 import java.util.ArrayList;
22 import java.util.Arrays;
23 import java.util.List;
24 import java.util.function.Function;
25 import java.util.logging.Level;
26 import java.util.stream.Collectors;
27 import org.apache.commons.lang.StringUtils;
28 import org.apache.commons.lang3.tuple.Pair;
29 import org.jsoup.Jsoup;
30 import org.jsoup.nodes.Document;
31 import org.jsoup.nodes.Element;
32 import org.openide.nodes.Node;
33 import org.openide.util.NbBundle;
44 import org.sleuthkit.datamodel.AbstractFile;
45 import org.sleuthkit.datamodel.AnalysisResult;
46 import org.sleuthkit.datamodel.BlackboardArtifact;
47 import org.sleuthkit.datamodel.BlackboardArtifactTag;
48 import org.sleuthkit.datamodel.BlackboardAttribute;
49 import org.sleuthkit.datamodel.Content;
50 import org.sleuthkit.datamodel.ContentTag;
51 import org.sleuthkit.datamodel.DataArtifact;
52 import org.sleuthkit.datamodel.SleuthkitCase;
53 import org.sleuthkit.datamodel.Tag;
54 import org.sleuthkit.datamodel.TskCoreException;
55 
59 public class AnnotationUtils {
60 
61  @NbBundle.Messages({
62  "AnnotationUtils.title=Annotations",
63  "AnnotationUtils.toolTip=Displays tags and comments associated with the selected content.",
64  "AnnotationUtils.centralRepositoryEntry.title=Central Repository Comments",
65  "AnnotationUtils.centralRepositoryEntryDataLabel.case=Case:",
66  "AnnotationUtils.centralRepositoryEntryDataLabel.type=Type:",
67  "AnnotationUtils.centralRepositoryEntryDataLabel.comment=Comment:",
68  "AnnotationUtils.centralRepositoryEntryDataLabel.path=Path:",
69  "AnnotationUtils.tagEntry.title=Tags",
70  "AnnotationUtils.tagEntryDataLabel.tag=Tag:",
71  "AnnotationUtils.tagEntryDataLabel.tagUser=Examiner:",
72  "AnnotationUtils.tagEntryDataLabel.comment=Comment:",
73  "AnnotationUtils.fileHitEntry.artifactCommentTitle=Artifact Comment",
74  "AnnotationUtils.fileHitEntry.hashSetHitTitle=Hash Set Hit Comments",
75  "AnnotationUtils.fileHitEntry.interestingFileHitTitle=Interesting File Hit Comments",
76  "AnnotationUtils.fileHitEntry.setName=Set Name:",
77  "AnnotationUtils.fileHitEntry.comment=Comment:",
78  "AnnotationUtils.sourceFile.title=Source File",
79  "AnnotationUtils.onEmpty=No annotations were found for this particular item."
80  })
81 
82  private static final Logger logger = Logger.getLogger(AnnotationUtils.class.getName());
83 
84  private static final String EMPTY_HTML = "<html><head></head><body></body></html>";
85 
86  // describing table values for a tag
87  private static final List<ItemEntry<Tag>> TAG_ENTRIES = Arrays.asList(
88  new ItemEntry<>(Bundle.AnnotationUtils_tagEntryDataLabel_tag(),
89  (tag) -> (tag.getName() != null) ? tag.getName().getDisplayName() : null),
90  new ItemEntry<>(Bundle.AnnotationUtils_tagEntryDataLabel_tagUser(), (tag) -> tag.getUserName()),
91  new ItemEntry<>(Bundle.AnnotationUtils_tagEntryDataLabel_comment(), (tag) -> tag.getComment())
92  );
93 
94  private static final SectionConfig<Tag> TAG_CONFIG
95  = new SectionConfig<>(Bundle.AnnotationUtils_tagEntry_title(), TAG_ENTRIES);
96 
97  // file set attributes and table configurations
98  private static final List<ItemEntry<BlackboardArtifact>> FILESET_HIT_ENTRIES = Arrays.asList(
99  new ItemEntry<>(Bundle.AnnotationUtils_fileHitEntry_setName(),
100  (bba) -> tryGetAttribute(bba, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME)),
101  new ItemEntry<>(Bundle.AnnotationUtils_fileHitEntry_comment(),
102  (bba) -> tryGetAttribute(bba, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_COMMENT))
103  );
104 
105  private static final SectionConfig<BlackboardArtifact> INTERESTING_FILE_CONFIG
106  = new SectionConfig<>(Bundle.AnnotationUtils_fileHitEntry_interestingFileHitTitle(), FILESET_HIT_ENTRIES);
107 
108  private static final SectionConfig<BlackboardArtifact> HASHSET_CONFIG
109  = new SectionConfig<>(Bundle.AnnotationUtils_fileHitEntry_hashSetHitTitle(), FILESET_HIT_ENTRIES);
110 
111  private static final SectionConfig<BlackboardArtifact> ARTIFACT_COMMENT_CONFIG
112  = new SectionConfig<>(Bundle.AnnotationUtils_fileHitEntry_artifactCommentTitle(), FILESET_HIT_ENTRIES);
113 
114  // central repository attributes and table configuration
115  private static final List<ItemEntry<CorrelationAttributeInstance>> CR_COMMENTS_ENTRIES = Arrays.asList(
116  new ItemEntry<>(Bundle.AnnotationUtils_centralRepositoryEntryDataLabel_case(),
117  cai -> (cai.getCorrelationCase() != null) ? cai.getCorrelationCase().getDisplayName() : null),
118  new ItemEntry<>(Bundle.AnnotationUtils_centralRepositoryEntryDataLabel_comment(), cai -> cai.getComment()),
119  new ItemEntry<>(Bundle.AnnotationUtils_centralRepositoryEntryDataLabel_path(), cai -> cai.getFilePath())
120  );
121 
122  private static final SectionConfig<CorrelationAttributeInstance> CR_COMMENTS_CONFIG
123  = new SectionConfig<>(Bundle.AnnotationUtils_centralRepositoryEntry_title(), CR_COMMENTS_ENTRIES);
124 
125  /*
126  * Private constructor for this utility class.
127  */
128  private AnnotationUtils() {
129 
130  }
131 
140  static DisplayTskItems getDisplayContent(Node node) {
141  BlackboardArtifactItem<?> artItem = node.getLookup().lookup(BlackboardArtifactItem.class);
142  BlackboardArtifact artifact = artItem == null ? null : artItem.getTskContent();
143 
144  Content content = artItem != null
145  ? artItem.getSourceContent()
146  : node.getLookup().lookup(AbstractFile.class);
147 
148  return new DisplayTskItems(artifact, content);
149  }
150 
158  public static boolean isSupported(Node node) {
159  return getDisplayContent(node).getContent() != null;
160  }
161 
171  public static Document buildDocument(Node node) {
172  Document html = Jsoup.parse(EMPTY_HTML);
173  Element body = html.getElementsByTag("body").first();
174 
175  DisplayTskItems displayItems = getDisplayContent(node);
176  BlackboardArtifact artifact = displayItems.getArtifact();
177  Content srcContent = displayItems.getContent();
178 
179  boolean somethingWasRendered = false;
180  if (artifact != null) {
181  somethingWasRendered = renderArtifact(body, artifact, srcContent);
182  } else {
183  somethingWasRendered = renderContent(body, srcContent, false);
184  }
185 
186  if (!somethingWasRendered) {
187  return null;
188  }
189 
190  return html;
191  }
192 
203  private static boolean renderArtifact(Element parent, BlackboardArtifact bba, Content sourceContent) {
204  boolean contentRendered = appendEntries(parent, TAG_CONFIG, getTags(bba), false, true);
205 
207  List<CorrelationAttributeInstance> centralRepoComments = getCentralRepositoryData(bba);
208  boolean crRendered = appendEntries(parent, CR_COMMENTS_CONFIG, centralRepoComments, false, !contentRendered);
209  contentRendered = contentRendered || crRendered;
210  }
211 
212  // if artifact is a hashset hit or interesting file and has a non-blank comment
213  if ((BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT.getTypeID() == bba.getArtifactTypeID()
214  || BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT.getTypeID() == bba.getArtifactTypeID())
215  && (hasTskComment(bba))) {
216 
217  boolean filesetRendered = appendEntries(parent, ARTIFACT_COMMENT_CONFIG, Arrays.asList(bba), false, !contentRendered);
218  contentRendered = contentRendered || filesetRendered;
219  }
220 
221  Element sourceFileSection = appendSection(parent, Bundle.AnnotationUtils_sourceFile_title());
222  sourceFileSection.attr("class", ContentViewerHtmlStyles.getSpacedSectionClassName());
223 
224  Element sourceFileContainer = sourceFileSection.appendElement("div");
225  sourceFileContainer.attr("class", ContentViewerHtmlStyles.getIndentedClassName());
226 
227  boolean sourceFileRendered = renderContent(sourceFileContainer, sourceContent, true);
228 
229  if (!sourceFileRendered) {
230  sourceFileSection.remove();
231  }
232 
233  return contentRendered || sourceFileRendered;
234  }
235 
246  private static boolean renderContent(Element parent, Content sourceContent, boolean isSubheader) {
247  boolean contentRendered = appendEntries(parent, TAG_CONFIG, getTags(sourceContent), isSubheader, true);
248 
249  if (sourceContent instanceof AbstractFile) {
250  AbstractFile sourceFile = (AbstractFile) sourceContent;
251 
253  List<CorrelationAttributeInstance> centralRepoComments = getCentralRepositoryData(sourceFile);
254  boolean crRendered = appendEntries(parent, CR_COMMENTS_CONFIG, centralRepoComments, isSubheader,
255  !contentRendered);
256  contentRendered = contentRendered || crRendered;
257  }
258 
259  boolean hashsetRendered = appendEntries(parent, HASHSET_CONFIG,
260  getFileSetHits(sourceFile, BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT),
261  isSubheader,
262  !contentRendered);
263 
264  boolean interestingFileRendered = appendEntries(parent, INTERESTING_FILE_CONFIG,
265  getFileSetHits(sourceFile, BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT),
266  isSubheader,
267  !contentRendered);
268 
269  contentRendered = contentRendered || hashsetRendered || interestingFileRendered;
270  }
271  return contentRendered;
272  }
273 
281  private static List<ContentTag> getTags(Content sourceContent) {
282  try {
283  SleuthkitCase tskCase = Case.getCurrentCaseThrows().getSleuthkitCase();
284  return tskCase.getContentTagsByContent(sourceContent);
285  } catch (NoCurrentCaseException ex) {
286  logger.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS
287  } catch (TskCoreException ex) {
288  logger.log(Level.SEVERE, "Exception while getting tags from the case database.", ex); //NON-NLS
289  }
290  return new ArrayList<>();
291  }
292 
300  private static List<BlackboardArtifactTag> getTags(BlackboardArtifact bba) {
301  try {
302  SleuthkitCase tskCase = Case.getCurrentCaseThrows().getSleuthkitCase();
303  return tskCase.getBlackboardArtifactTagsByArtifact(bba);
304  } catch (NoCurrentCaseException ex) {
305  logger.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS
306  } catch (TskCoreException ex) {
307  logger.log(Level.SEVERE, "Exception while getting tags from the case database.", ex); //NON-NLS
308  }
309  return new ArrayList<>();
310  }
311 
321  private static List<BlackboardArtifact> getFileSetHits(AbstractFile sourceFile, BlackboardArtifact.ARTIFACT_TYPE type) {
322  try {
323  SleuthkitCase tskCase = Case.getCurrentCaseThrows().getSleuthkitCase();
324  return tskCase.getBlackboardArtifacts(type, sourceFile.getId()).stream()
325  .filter((bba) -> hasTskComment(bba))
326  .collect(Collectors.toList());
327  } catch (NoCurrentCaseException ex) {
328  logger.log(Level.SEVERE, "Exception while getting open case.", ex); // NON-NLS
329  } catch (TskCoreException ex) {
330  logger.log(Level.SEVERE, "Exception while getting file set hits from the case database.", ex); //NON-NLS
331  }
332  return new ArrayList<>();
333  }
334 
342  private static boolean hasTskComment(BlackboardArtifact artifact) {
343  return StringUtils.isNotBlank(tryGetAttribute(artifact, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_COMMENT));
344  }
345 
355  private static String tryGetAttribute(BlackboardArtifact artifact, BlackboardAttribute.ATTRIBUTE_TYPE attributeType) {
356  if (artifact == null) {
357  return null;
358  }
359 
360  BlackboardAttribute attr = null;
361  try {
362  attr = artifact.getAttribute(new BlackboardAttribute.Type(attributeType));
363  } catch (TskCoreException ex) {
364  logger.log(Level.WARNING, String.format("Unable to fetch attribute of type %s for artifact %s", attributeType, artifact), ex);
365  }
366 
367  if (attr == null) {
368  return null;
369  }
370 
371  return attr.getValueString();
372  }
373 
383  private static List<CorrelationAttributeInstance> getCentralRepositoryData(BlackboardArtifact artifact) {
384  if (artifact == null) {
385  return new ArrayList<>();
386  }
387  List<CorrelationAttributeInstance> instances = new ArrayList<>();
388  if (artifact instanceof DataArtifact) {
389  instances.addAll(CorrelationAttributeUtil.makeCorrAttrsForSearch((DataArtifact) artifact));
390  } else if (artifact instanceof AnalysisResult) {
391  instances.addAll(CorrelationAttributeUtil.makeCorrAttrsForSearch((AnalysisResult) artifact));
392  }
393 
394  List<Pair<CorrelationAttributeInstance.Type, String>> lookupKeys = instances.stream()
395  .map(cai -> Pair.of(cai.getCorrelationType(), cai.getCorrelationValue()))
396  .collect(Collectors.toList());
397 
398  return getCorrelationAttributeComments(lookupKeys);
399  }
400 
410  private static List<CorrelationAttributeInstance> getCentralRepositoryData(AbstractFile sourceFile) {
411  if (sourceFile == null || StringUtils.isEmpty(sourceFile.getMd5Hash())) {
412  return new ArrayList<>();
413  }
414 
415  List<CorrelationAttributeInstance.Type> artifactTypes = null;
416  try {
418  } catch (CentralRepoException ex) {
419  logger.log(Level.SEVERE, "Error connecting to the Central Repository database.", ex); // NON-NLS
420  }
421 
422  if (artifactTypes == null || artifactTypes.isEmpty()) {
423  return new ArrayList<>();
424  }
425 
426  String md5 = sourceFile.getMd5Hash();
427 
428  // get key lookups for a file attribute types and the md5 hash
429  List<Pair<CorrelationAttributeInstance.Type, String>> lookupKeys = artifactTypes.stream()
430  .filter((attributeType) -> attributeType.getId() == CorrelationAttributeInstance.FILES_TYPE_ID)
431  .map((attributeType) -> Pair.of(attributeType, md5))
432  .collect(Collectors.toList());
433 
434  return getCorrelationAttributeComments(lookupKeys);
435  }
436 
445  private static List<CorrelationAttributeInstance> getCorrelationAttributeComments(List<Pair<CorrelationAttributeInstance.Type, String>> lookupKeys) {
446  List<CorrelationAttributeInstance> instancesToRet = new ArrayList<>();
447 
448  try {
449  // use lookup instances to find the actual correlation attributes for the items selected
450  for (Pair<CorrelationAttributeInstance.Type, String> typeVal : lookupKeys) {
451  instancesToRet.addAll(CentralRepository.getInstance()
452  .getArtifactInstancesByTypeValue(typeVal.getKey(), typeVal.getValue())
453  .stream()
454  // for each one found, if it has a comment, return
455  .filter((cai) -> StringUtils.isNotBlank(cai.getComment()))
456  .collect(Collectors.toList()));
457  }
458 
459  } catch (CentralRepoException ex) {
460  logger.log(Level.SEVERE, "Error connecting to the Central Repository database.", ex); // NON-NLS
462  logger.log(Level.SEVERE, "Error normalizing instance from Central Repository database.", ex); // NON-NLS
463  }
464 
465  return instancesToRet;
466  }
467 
485  private static <T> boolean appendEntries(Element parent, AnnotationUtils.SectionConfig<T> config, List<? extends T> items,
486  boolean isSubsection, boolean isFirstSection) {
487  if (items == null || items.isEmpty()) {
488  return false;
489  }
490 
491  Element sectionDiv = (isSubsection) ? appendSubsection(parent, config.getTitle()) : appendSection(parent, config.getTitle());
492  if (!isFirstSection) {
493  sectionDiv.attr("class", ContentViewerHtmlStyles.getSpacedSectionClassName());
494  }
495 
496  Element sectionContainer = sectionDiv.appendElement("div");
497 
498  if (!isSubsection) {
499  sectionContainer.attr("class", ContentViewerHtmlStyles.getIndentedClassName());
500  }
501 
502  appendVerticalEntryTables(sectionContainer, items, config.getAttributes());
503  return true;
504  }
505 
516  private static <T> Element appendVerticalEntryTables(Element parent, List<? extends T> items, List<ItemEntry<T>> rowHeaders) {
517  boolean isFirst = true;
518  for (T item : items) {
519  if (item == null) {
520  continue;
521  }
522 
523  List<List<String>> tableData = rowHeaders.stream()
524  .map(row -> Arrays.asList(row.getItemName(), row.retrieveValue(item)))
525  .collect(Collectors.toList());
526 
527  Element childTable = appendTable(parent, 2, tableData, null);
528 
529  if (isFirst) {
530  isFirst = false;
531  } else {
532  childTable.attr("class", ContentViewerHtmlStyles.getSpacedSectionClassName());
533  }
534  }
535 
536  return parent;
537  }
538 
551  private static Element appendTable(Element parent, int columnNumber, List<List<String>> content, List<String> columnHeaders) {
552  Element table = parent.appendElement("table")
553  .attr("valign", "top")
554  .attr("align", "left");
555 
556  if (columnHeaders != null && !columnHeaders.isEmpty()) {
557  Element header = table.appendElement("thead");
558  appendRow(header, columnHeaders, columnNumber, true);
559  }
560  Element tableBody = table.appendElement("tbody");
561 
562  content.forEach((rowData) -> appendRow(tableBody, rowData, columnNumber, false));
563  return table;
564  }
565 
577  private static Element appendRow(Element rowParent, List<String> data, int columnNumber, boolean isHeader) {
578  String cellType = isHeader ? "th" : "td";
579  Element row = rowParent.appendElement("tr");
580  for (int i = 0; i < columnNumber; i++) {
581  Element cell = row.appendElement(cellType);
582 
583  if (i == 0) {
584  cell.attr("class", ContentViewerHtmlStyles.getKeyColumnClassName());
585  }
586 
587  if (data != null && i < data.size()) {
588  cell.appendElement("span")
589  .attr("class", ContentViewerHtmlStyles.getTextClassName())
590  .text(StringUtils.isEmpty(data.get(i)) ? "" : data.get(i));
591  }
592  }
593  return row;
594  }
595 
604  private static Element appendSection(Element parent, String headerText) {
605  Element sectionDiv = parent.appendElement("div");
606  Element header = sectionDiv.appendElement("h1");
607  header.text(headerText);
608  header.attr("class", ContentViewerHtmlStyles.getHeaderClassName());
609  return sectionDiv;
610  }
611 
620  private static Element appendSubsection(Element parent, String headerText) {
621  Element subsectionDiv = parent.appendElement("div");
622  Element header = subsectionDiv.appendElement("h2");
623  header.text(headerText);
624  header.attr("class", ContentViewerHtmlStyles.getHeaderClassName());
625  return subsectionDiv;
626  }
627 
635  static class ItemEntry<T> {
636 
637  private final String itemName;
638  private final Function<T, String> valueRetriever;
639 
640  ItemEntry(String itemName, Function<T, String> valueRetriever) {
641  this.itemName = itemName;
642  this.valueRetriever = valueRetriever;
643  }
644 
645  String getItemName() {
646  return itemName;
647  }
648 
649  Function<T, String> getValueRetriever() {
650  return valueRetriever;
651  }
652 
653  String retrieveValue(T object) {
654  return valueRetriever.apply(object);
655  }
656  }
657 
663  static class SectionConfig<T> {
664 
665  private final String title;
666  private final List<ItemEntry<T>> attributes;
667 
668  SectionConfig(String title, List<ItemEntry<T>> attributes) {
669  this.title = title;
670  this.attributes = attributes;
671  }
672 
676  String getTitle() {
677  return title;
678  }
679 
684  List<ItemEntry<T>> getAttributes() {
685  return attributes;
686  }
687  }
688 
693  static class DisplayTskItems {
694 
695  private final BlackboardArtifact artifact;
696  private final Content content;
697 
705  DisplayTskItems(BlackboardArtifact artifact, Content content) {
706  this.artifact = artifact;
707  this.content = content;
708  }
709 
713  BlackboardArtifact getArtifact() {
714  return artifact;
715  }
716 
720  Content getContent() {
721  return content;
722  }
723  }
724 }
static String tryGetAttribute(BlackboardArtifact artifact, BlackboardAttribute.ATTRIBUTE_TYPE attributeType)
static List< CorrelationAttributeInstance > getCentralRepositoryData(AbstractFile sourceFile)
static Element appendRow(Element rowParent, List< String > data, int columnNumber, boolean isHeader)
static Element appendSubsection(Element parent, String headerText)
static< T > boolean appendEntries(Element parent, AnnotationUtils.SectionConfig< T > config, List<?extends T > items, boolean isSubsection, boolean isFirstSection)
static List< ContentTag > getTags(Content sourceContent)
static final List< ItemEntry< BlackboardArtifact > > FILESET_HIT_ENTRIES
static final SectionConfig< BlackboardArtifact > HASHSET_CONFIG
List< CorrelationAttributeInstance > getArtifactInstancesByTypeValue(CorrelationAttributeInstance.Type aType, String value)
static final SectionConfig< BlackboardArtifact > INTERESTING_FILE_CONFIG
static Element appendSection(Element parent, String headerText)
static List< BlackboardArtifact > getFileSetHits(AbstractFile sourceFile, BlackboardArtifact.ARTIFACT_TYPE type)
static< T > Element appendVerticalEntryTables(Element parent, List<?extends T > items, List< ItemEntry< T >> rowHeaders)
static final List< ItemEntry< CorrelationAttributeInstance > > CR_COMMENTS_ENTRIES
List< CorrelationAttributeInstance.Type > getDefinedCorrelationTypes()
static List< BlackboardArtifactTag > getTags(BlackboardArtifact bba)
static List< CorrelationAttributeInstance > makeCorrAttrsForSearch(AnalysisResult analysisResult)
static final SectionConfig< BlackboardArtifact > ARTIFACT_COMMENT_CONFIG
static boolean hasTskComment(BlackboardArtifact artifact)
static Element appendTable(Element parent, int columnNumber, List< List< String >> content, List< String > columnHeaders)
static final SectionConfig< CorrelationAttributeInstance > CR_COMMENTS_CONFIG
static boolean renderContent(Element parent, Content sourceContent, boolean isSubheader)
static List< CorrelationAttributeInstance > getCentralRepositoryData(BlackboardArtifact artifact)
static List< CorrelationAttributeInstance > getCorrelationAttributeComments(List< Pair< CorrelationAttributeInstance.Type, String >> lookupKeys)
synchronized static Logger getLogger(String name)
Definition: Logger.java:124
static boolean renderArtifact(Element parent, BlackboardArtifact bba, Content sourceContent)

Copyright © 2012-2021 Basis Technology. Generated on: Thu Sep 30 2021
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.