diff --git a/OPAL/ProjectDependencies.mmd b/OPAL/ProjectDependencies.mmd
index b2055fe410..d4f33b8fc7 100644
--- a/OPAL/ProjectDependencies.mmd
+++ b/OPAL/ProjectDependencies.mmd
@@ -27,6 +27,8 @@ flowchart BT
bp[BugPicker\n bp]
hermes[Hermes\n hermes]
+ ce[ConfigurationExplorer \n ce]
+
style common fill:#9cbecc,color:black
style framework fill:#c0ffc0
style bp fill:#ffd7cf
@@ -68,4 +70,10 @@ flowchart BT
demos --> framework
bp --> framework
- hermes --> framework
\ No newline at end of file
+ hermes --> framework
+
+ ce --> br
+ ce --> apk
+ ce --> demos
+ ce --> hermes
+ ce --> bp
\ No newline at end of file
diff --git a/OPAL/ProjectDependencies.pdf b/OPAL/ProjectDependencies.pdf
index 1cd89db7f5..7029db12a9 100644
Binary files a/OPAL/ProjectDependencies.pdf and b/OPAL/ProjectDependencies.pdf differ
diff --git a/OPAL/ProjectDependencies.svg b/OPAL/ProjectDependencies.svg
index dd235d90a7..4035046177 100644
--- a/OPAL/ProjectDependencies.svg
+++ b/OPAL/ProjectDependencies.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/TOOLS/ce/build.sbt b/TOOLS/ce/build.sbt
new file mode 100644
index 0000000000..b511e98651
--- /dev/null
+++ b/TOOLS/ce/build.sbt
@@ -0,0 +1 @@
+// build settings reside in the opal root build.sbt file
diff --git a/TOOLS/ce/readme.md b/TOOLS/ce/readme.md
new file mode 100644
index 0000000000..142f779ddd
--- /dev/null
+++ b/TOOLS/ce/readme.md
@@ -0,0 +1,131 @@
+# Configuration explorer documentation
+## How to document your configs
+The configuration explorer uses a custom parser to parse flags into a browsable documentation
+This makes every element of your config documentable.
+This guide is about to teach you how to utilize these flags to create a comprehensible documentation
+
+### How do I create a browsable configuration?
+1. Start up your sbt shell
+2. run the doc command
+3. Open the generated commentedconfigs.html in your source directory
+
+### Where can I add my documentation?
+You can add your documentation in the lines before an element, or directly behind the element within the same line.
+
+```
+ // You can either add your comment to the element here
+ Value = "one" //Or you can also add your comment here
+
+ // However, you cannot add your comment here
+```
+
+Also keep in mind that HOCON allows for multiple values within one line.
+In this case, the comment will be associated with its closest neighbor that fullfills the criteria:
+
+```
+ Object = {Key = "Value", AnotherKey = "SecondValue"} // This comment will be associated with Object, since its closing bracket is closest
+```
+
+Sub-values need to be placed in its own lines in order to be documented.
+
+### Usage of flags
+
+The configuration explorer allows for different flags to be utilized for different elements of the documentation.
+Each flag will be interpreted differently and be represented in a different way after exporting.
+
+The flags can be grouped in two groups, by the point in time where they are visible when the documentation is exported.
+
+#### @label
+@label documents the label of the object.
+It will be shown in the documentation as the name of the object and will be visible even when the object is collapsed.
+If the configuration element is part of an object, the label will be automatically set as its identifier within the object.
+This is overridable by manually setting the @label flag within its documentation.
+
+```
+ {
+ key = "value" // The label property will be set to "key" automatically.
+
+ //@label Custom label
+ another_key = "value" // The label property is now overridden to "Custom label"
+ }
+```
+
+#### @brief
+@brief shows a brief description of the element for easier comprehension without the need for expanding the element.
+For optimal formatting, try to keep the length of the text behind this flag below 50 characters.
+
+#### @description
+@description will set the description of the configuration element when the element is expanded.
+Usage of the flag is optional, as unflagged content will be added to the description area too.
+
+```
+ {
+ // @description You can use this flag to add this text to the elements description.
+ // However, without any flags, the text will be added to the description too.
+ key = "value"
+ }
+```
+
+#### @type
+@type can be used to indicate the type of a value that will be used.
+
+##### Subclass type
+The subclass type is one of two special types currently implemented into Configuration Explorer
+When tagged with a subclass type, configuration explorer will search for all subclasses of a given root class.
+
+##### Enum type
+The enum type is the second of the special types in Configuration Explorer. Use it if you have a finite amount of allowed values that you all want to list in the constraints.
+
+##### Other types
+You can pick a type that you want to indicate, which logical restraints are, but they will be treated as-is and not be refined further.
+
+#### @constraint
+Use @constraint to define which values are allowed and which are not.
+If there are multiple constraints, use a new line for each constraint using the flag "@constraint" at the beginnning of each line.
+
+There are currently two types implemented where you use a special style to list constraints:
+##### Subclass type
+If your type is "subclass", then list exactly one constraint. The constraint must be the class where all allowed classes inherit from.
+Configuration Explorer will fetch all valid Subclasses of the listed class and list these in the documentation. You may specify a different value in the value field when generating the documentation.
+
+Example:
+```
+ {
+ // @description Configuration Explorer will list all subclasses of ConfigNode as allowed values. (Which are ConfigObject, ConfigList, ConfigEntry)
+ // @type subclass
+ // @constraint ConfigNode
+ value = ConfigObject
+ }
+```
+
+##### Enum type
+Create one line with the constraint flag for every allowed value that you want to list.
+
+Example:
+
+```
+ {
+ // @description Add one row for each allowed value
+ // @type enum
+ // @constraint two
+ // @constraint three
+ // @constraint five
+ // @constraint seven
+ primeNumberBelowTen = 3
+ }
+```
+
+#### Other types
+The constraints for other types will be passed as-is and can be in a free text.
+
+Example:
+
+```
+ {
+ // @type int
+ // @constraint Values must be within 0 and 100
+ // @constraint Values must be even
+ key = 2
+ }
+```
+
diff --git a/TOOLS/ce/src/main/resources/ce.conf b/TOOLS/ce/src/main/resources/ce.conf
new file mode 100644
index 0000000000..479b1af031
--- /dev/null
+++ b/TOOLS/ce/src/main/resources/ce.conf
@@ -0,0 +1,61 @@
+// @brief Configuration for the config explorer
+// @description Settings under this hierarchy control the configuration explorer options
+
+{
+ org.opalj.ce {
+ // @brief Used filenames for configurations
+ // @description This setting contains a list of filenames that are in use for configuration files in this project
+ // reference.conf, application.conf are default filenames defined by the maker of typesafe configuration
+ // @type String
+ configurationFilenames = ["ce.conf","reference.conf","application.conf", "hermes.conf","CommandLineProject.conf","LibraryProject.conf","NoTransformations.conf"]
+
+ // @brief Toggle for replacing classes in the configuration documentation
+ // @description You can use this option to activate / deactivate the replacement of subclass type configuration entries
+ // When activated, ce will list all implemented subclasses able to replace a class type value
+ // @type Boolean
+ replaceSubclasses = true
+
+ // @label HTML Settings
+ // @brief These settings store the information for the HTML export
+ html = {
+ // @brief Path to the HTMLTemplate
+ // @description This setting contains a relative path from the projects root to the file that stores the HTML Stylesheet and the JavaScript components
+ // @type String
+ // @constraint must be a path to an existing html file in Linux syntax (use / instead of \)
+ template = "/TOOLS/ce/src/main/resources/template.html"
+
+ // @brief Defines the headline of a config Node
+ // @description This sets the syntax for the headline of a config node.
+ // $label will be replaced with the label of the ConfigNode
+ // $brief will be replaced with the brief description of the ConfigNode
+ // This does not render correctly on the documentation due to it being an HTML expression
+ // @type String
+ // @constraint Must be a valid closed HTML expression
+ headline = "
$label - $brief Show
"
+
+ // @brief defines syntax of the content bracket
+ // @description This setting sets the syntax of the content brackets
+ // $content is a placeholder that will be replaced with the further content of the config node
+ // This does not render correctly on the documentation due to it being an HTML expression
+ // @type String
+ // @constraint Must be a valid closed HTML expression
+ // @constraint Must contain the placeholder $content
+ content = "
$content
"
+
+ // @brief Defines the export path of the commented commentedconfigs
+ // @type String
+ // @constraint must be a file name
+ export = "/commentedconfigs.html"
+
+ // @brief Defines if the keys in the export should be sorted alphabetically
+ // @type boolean
+ sort_alphabetically = false
+
+ // @brief Defines how long the fallback preview of the brief description can be
+ // @description This value defines, how long the preview in the brief window can be, if there is not explicit brief description available.
+ // The number is the amount of characters displayed.
+ // @type Integer
+ maximum_headline_preview_length = 70
+ }
+ }
+}
\ No newline at end of file
diff --git a/TOOLS/ce/src/main/resources/template.html b/TOOLS/ce/src/main/resources/template.html
new file mode 100644
index 0000000000..61dd5df08f
--- /dev/null
+++ b/TOOLS/ce/src/main/resources/template.html
@@ -0,0 +1,71 @@
+
+
+
+
+
+ OPAL Config Documentation
+
+
+
+
+$body
+
+
+
+
+
diff --git a/TOOLS/ce/src/main/scala/org.opalj.ce/CommentParser.scala b/TOOLS/ce/src/main/scala/org.opalj.ce/CommentParser.scala
new file mode 100644
index 0000000000..c0e4faf714
--- /dev/null
+++ b/TOOLS/ce/src/main/scala/org.opalj.ce/CommentParser.scala
@@ -0,0 +1,361 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package ce
+
+import java.nio.file.Path
+import scala.collection.mutable
+import scala.collection.mutable.ListBuffer
+import scala.io.Source
+import scala.util.Using
+import scala.util.control.Breaks.break
+import scala.util.control.Breaks.breakable
+
+import com.typesafe.config.ConfigFactory
+
+import org.opalj.log.GlobalLogContext
+import org.opalj.log.LogContext
+import org.opalj.log.OPALLogger
+
+/**
+ * The class CommentParserWrapper is the class that should be used for parsing commented config within the Configuration Explorer.
+ */
+class CommentParser {
+ /**
+ * Made to parse multiple Configuration Files in bulk.
+ * Used in combination with the file Locator to locate and parse all config files of a project.
+ * @param filepaths accepts a list of full paths to the HOCON config files that shall be parsed.
+ * @return is a Seq of the parsed configuration files, paired with the path they originate from.
+ */
+ def iterateConfigs(filepaths: Iterable[Path], rootDirectory: Path): Seq[ConfigObject] = {
+ val commentedConfigs = filepaths.map(filepath => ParseComments(filepath, rootDirectory)).toList
+
+ // Merge all config files named "reference.conf"
+ val (mergingConfigs, otherConfigs) = commentedConfigs.partition(_.comment.label.endsWith("reference.conf"))
+
+ val mergedReferenceConfOpt = if (mergingConfigs.nonEmpty) {
+ val mergedConfig =
+ mergingConfigs.foldLeft(ConfigObject(
+ mutable.Map[String, ConfigNode](),
+ new DocumentationComment(
+ "reference.conf",
+ "Aggregated standard configuration of merged reference.conf files",
+ Seq(),
+ "",
+ Seq()
+ )
+ )) {
+ (accumulatedConfig, mergingConfig) => accumulatedConfig.merge(mergingConfig); accumulatedConfig
+ }
+ Some(mergedConfig)
+ } else {
+ None
+ }
+
+ val finalConfigs = otherConfigs ++ mergedReferenceConfOpt
+ finalConfigs.foreach(config => config.collapse())
+
+ finalConfigs
+ }
+
+ /**
+ * Handles the frame around parsing the configuration file.
+ * Also checks if the config files are in an allowed HOCON formats to prevent endless loops.
+ * @param filepath accepts the full path to a valid HOCON config file.
+ * @return returns the parsed config as a ConfigNode.
+ */
+ def ParseComments(filepath: Path, rootDirectory: Path): ConfigObject = {
+ // This prevents the Parser from parsing a file without valid syntax
+ ConfigFactory.load(filepath.toString)
+
+ val cp = new HOCONParser
+ cp.parseFile(filepath, rootDirectory)
+ }
+
+ /**
+ * Inner class of the Comment parser Wrapper
+ * This class handles the parsing process itself
+ */
+ private class HOCONParser {
+ private var iterator: Iterator[String] = Iterator.empty
+ private var line = ""
+ implicit val logContext: LogContext = GlobalLogContext
+
+ /**
+ * parseComments initiates the parsing process.
+ * A ConfigNode can consist out of 3 possible types that are parsed differently: Objects, Lists and Entries.
+ * The source node of a config file always is an object.
+ * During parsing, the Parser will iterate the file and sort it into the ConfigNode structure.
+ * Since the Nodes can be nested and there can be multiple Nodes in one line, the Parser needs to examine most control structures like a stream and not linewise.
+ * @param filePath accepts the path to a valid HOCON file.
+ * @return returns the fully parsed file as a configObject.
+ */
+ def parseFile(filePath: Path, rootDirectory: Path): ConfigObject = {
+ // Initialize iterator
+ OPALLogger.info("Configuration Explorer", s"Parsing: ${filePath.toString}")
+
+ Using.resource(Source.fromFile(filePath.toString)) { source =>
+ iterator = source.getLines()
+
+ // Parse initial Comments
+ val initialComment = ListBuffer[String]()
+ initialComment += ("@label " + filePath.toAbsolutePath.toString.stripPrefix(
+ rootDirectory.toAbsolutePath.toString
+ ))
+ initialComment ++= parseComments()
+ parseObject(initialComment)
+ }
+ }
+
+ /**
+ * Method responsible to parse Object-Type Nodes.
+ * @param currentComment assigns previously parsed comment to this Node. This is necessary as most comments appear before the opening bracket of an object (Which identifies it as an object).
+ * @return returns the fully parsed object.
+ */
+ private def parseObject(currentComment: ListBuffer[String]): ConfigObject = {
+ line = line.trim.stripPrefix("{")
+ // Creating necessary components
+ val entries = mutable.Map[String, ConfigNode]()
+ var nextComment = parseComments()
+ var currentKey = ""
+ var currentvalue: ConfigNode = null
+
+ // Using a breakable while loop to interrupt as soon as the object ends
+ breakable {
+ while (iterator.hasNext || line.nonEmpty) {
+ parseComments(nextComment)
+ if (line.startsWith("}")) {
+ // Found the closing bracket of the object. Remove the closing bracket and stop parsing the object
+ line = line.stripPrefix("}")
+ break()
+
+ } else if (line != "") {
+ // If none of the options above apply and the line is NOT empty (in which case load the next line and ignore this)
+ // What follows now is part of the content of the object
+ // Objects are Key Value pairs, so parsing these is a two stage job: Separating Key and value and then parsing the value
+
+ // 1. Separating Key and value
+ // In JSON, Keys and values are separated with ':'. HOCON allows substituting ':' with '=' and also allows ommitting these symbols when using a '{' or '[' to open an object/list afterwards
+ // Finding first instance of these symbols
+ // TerminatingIndex is the index of the symbol that terminates the key.
+ val terminatingChars = Set(':', '=', '{', '[')
+ val terminatingIndex = line.indexWhere(terminatingChars.contains)
+
+ // Splitting the key from the string (while splitting of the ':' or '=' as they are not needed anymore
+ currentKey = line.substring(0, terminatingIndex - 1).trim.stripPrefix("\"").stripSuffix("\"")
+ line = line.substring(terminatingIndex).trim.stripPrefix(":").stripPrefix("=").trim
+
+ // Evaluating the type of value
+ if (line.startsWith("{")) {
+ // Case: Value is an object
+ currentvalue = parseObject(nextComment)
+ } else if (line.startsWith("[")) {
+ // Case: Value is a list
+ currentvalue = parseList(nextComment)
+ } else {
+ // Case: Value is an entry
+ currentvalue = parseEntry(nextComment)
+ }
+
+ // Reset next comment
+ nextComment = ListBuffer[String]()
+
+ // Json Keys are split using a ",". This is not necessary, but tolerated in HOCON syntax
+ line = line.stripPrefix(",").trim
+
+ // Adding the new Key, Value pair to the Map
+ entries += ((currentKey, currentvalue))
+ }
+
+ // Proceed with the next line if the current one was fully parsed
+ if (line.trim == "" && iterator.hasNext) {
+ line = iterator.next().trim
+ }
+ }
+ }
+
+ // If there is a comment directly behind the closing bracket of the object, add it to comments too.
+ if (line.startsWith("#") || line.startsWith("//")) {
+ currentComment += line.stripPrefix("#").stripPrefix("//").trim
+ line = ""
+ }
+
+ // Return the finished ConfigObject
+ ConfigObject(entries, DocumentationComment.fromString(currentComment))
+ }
+
+ /**
+ * Method responsible to parse Entry-Type Nodes.
+ * @param currentComment assings previously parsed comment to this Node. This is necessary as most comments appear before the opening bracket of an object (Which identifies it as an object).
+ * @return returns the fully parsed entry.
+ */
+ private def parseEntry(currentComment: ListBuffer[String]): ConfigEntry = {
+ // Creation of necessary values
+ var value = ""
+
+ parseComments(currentComment)
+
+ if (line.startsWith("\"\"\"")) {
+ // Case: line starts with a triple quoted string (These allow for multi-line values, so the line end does not necessarily terminate the value
+ line = line.stripPrefix("\"\"\"").trim
+ val valueBuilder = new StringBuilder
+ var index = line.indexOf("\"\"\"")
+ if (index >= 0) {
+ // The value is a single line value
+ valueBuilder ++= line.substring(0, index)
+ line = line.substring(index).stripPrefix("\"\"\"").trim
+ } else {
+ // The value is a multi line value
+ valueBuilder ++= s"$line \n"
+ breakable {
+ while (iterator.hasNext) {
+ line = iterator.next().trim
+ index = line.indexOf("\"\"\"")
+ if (index >= 0) {
+ valueBuilder ++= s"${line.substring(0, index)} \n"
+ line = line.stripPrefix(line.trim.substring(
+ 0,
+ index
+ )).stripPrefix("\"\"\"").trim
+ break()
+ } else {
+ valueBuilder ++= s"$line \n"
+ }
+ }
+ }
+ }
+ value = valueBuilder.toString
+ } else if (line.startsWith("\"")) {
+ // Case: line starts with a double quoted string
+ line = line.stripPrefix("\"").trim
+ // A '\' can escape a quote. Thus we need to exclude that from the terminating Index
+ var index = line.indexOf('\"')
+ breakable(while (index != -1) {
+ if (index == 0 || line(index - 1) != '\\') {
+ break()
+ } else {
+ index = line.indexOf('\"', index + 1)
+ }
+ })
+
+ value = line.substring(0, index).trim
+ line = line.stripPrefix(value).trim.stripPrefix("\"").trim
+ } else if (line.startsWith("\'")) {
+ // Case: line starts with a single quoted string
+ line = line.stripPrefix("\'").trim
+ // A '\' can escape a quote. Thus we need to exclude that from the terminating Index
+ var index = line.indexOf('\'')
+ breakable(while (index != -1) {
+ if (index == 0 || line(index - 1) != '\\') {
+ break()
+ } else {
+ index = line.indexOf('\'', index + 1)
+ }
+ })
+
+ value = line.substring(0, index).trim
+ line = line.stripPrefix(value).trim.stripPrefix("\'").trim
+ } else {
+ // Case: Line starts with an unquoted string
+ // There are two ways of terminating an unquoted string
+ // Option 1: The value is inside of a pattern that has other control structures
+ val terminatingChars = Set(',', ']', '}', ' ')
+ val terminatingIndex = line.indexWhere(terminatingChars.contains)
+
+ if (terminatingIndex > 0) {
+ value = line.substring(0, terminatingIndex).trim
+ line = line.stripPrefix(value).trim
+ } else {
+ // Option 2: The end of the line
+ value = line
+ line = ""
+ }
+ }
+
+ currentComment += getSingleLineComment
+ ConfigEntry(value, DocumentationComment.fromString(currentComment))
+ }
+
+ /**
+ * Method responsible to parse List-Type Nodes.
+ * @param currentComment assings previously parsed comment to this Node. This is necessary as most comments appear before the opening bracket of an object (Which identifies it as an object).
+ * @return returns the fully parsed entry.
+ */
+ private def parseList(currentComment: ListBuffer[String]): ConfigList = {
+ line = line.stripPrefix("[").trim
+ // Creating necessary variables
+ val value = new ListBuffer[ConfigNode]
+ var nextComment = ListBuffer[String]()
+
+ breakable {
+ while (iterator.hasNext || line.nonEmpty) {
+ nextComment = parseComments()
+ if (line.startsWith("{")) {
+ // Case: The following symbol opens an object
+ value += parseObject(nextComment)
+ line = line.stripPrefix(",").trim
+ } else if (line.startsWith("[")) {
+ // Case: The following symbol opens a list
+ value += parseList(nextComment)
+ line = line.stripPrefix(",").trim
+ } else if (
+ line.startsWith("]") || (line.startsWith(",") && line.stripPrefix(",").trim.startsWith("]"))
+ ) {
+ // Case: The following symbol closes the list
+ line = line.stripPrefix(",").trim.stripPrefix("]").trim
+ break()
+ } else if (line != "") {
+ // Case: The following symbol is an entry
+ value += parseEntry(nextComment)
+ line = line.stripPrefix(",").trim
+ }
+
+ if (line == "" && iterator.hasNext) {
+ // Load next line when done
+ line = iterator.next().trim
+ }
+ }
+ }
+ currentComment += getSingleLineComment
+
+ // Finish
+ ConfigList(value, DocumentationComment.fromString(currentComment))
+ }
+
+ /**
+ * Gets all comments until the next non-comment entry and writes them into a new ListBuffer.
+ * @return Returns a new ListBuffer with the content of the comment.
+ */
+ private def parseComments(): ListBuffer[String] = {
+ val comment = new ListBuffer[String]
+ parseComments(comment)
+ }
+
+ /**
+ * Gets all comments all comments until the next non-comment entry and writes them into an existing ListBuffer.
+ * @param comment Accepts the ListBuffer that the following comment should be added to.
+ * @return Returns the existing ListBuffer, but with the content of the comment added to it.
+ */
+ private def parseComments(comment: ListBuffer[String]): ListBuffer[String] = {
+ while (line.startsWith("#") || line.startsWith("//") || line == "") {
+ if (line != "") comment += getSingleLineComment
+ line = iterator.next().trim
+ }
+ comment
+ }
+
+ /**
+ * Adds a single line of Comment to the raw Comment string if the line has the comment flags
+ * @return returns an empty string if the next line is not a Comment. Returns the Comment without the comment flags if the line is a comment.
+ */
+ private def getSingleLineComment: String = {
+ if (line.startsWith("#") || line.startsWith("//")) {
+ // Add the comment in the same line of the list as well
+ val currentComment = line.stripPrefix("#").stripPrefix("//").trim
+ line = ""
+ currentComment
+ } else {
+ ""
+ }
+ }
+ }
+}
diff --git a/TOOLS/ce/src/main/scala/org.opalj.ce/ConfigEntry.scala b/TOOLS/ce/src/main/scala/org.opalj.ce/ConfigEntry.scala
new file mode 100644
index 0000000000..0820cb2218
--- /dev/null
+++ b/TOOLS/ce/src/main/scala/org.opalj.ce/ConfigEntry.scala
@@ -0,0 +1,87 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package ce
+
+import org.apache.commons.text.StringEscapeUtils
+
+/**
+ * Stores a value inside the structure of the configNode.
+ *
+ * @param value is the value stored in the entry.
+ * @param comment are all the comments associated with the value.
+ */
+case class ConfigEntry(value: String, var comment: DocumentationComment) extends ConfigNode {
+ /**
+ * Formats the entry into HTML code.
+ * @param label required if the Entry is part of an object (Writes the key of the K,V Map there instead). Overrides the label property of the Comment object.
+ * @param HTMLHeadline accepts the HTML syntax of the Headline of the value. Can contain $ label and $ brief flags for filling with content.
+ * @param HTMLContent accepts the HTML syntax of the content frame for the value. Must contains a $ content flag for correct rendering.
+ * @param HTMLStringBuilder accepts a StringBuilder. The method adds the HTML String to this StringBuilder.
+ * @param sorted actually does nothing in this method, but is required for the whole structure.
+ * @param maximumHeadlinePreviewLength accepts an integer that determines the maximum amount of characters that the fallback brief preview can contain.
+ */
+ override def toHTML(
+ label: String,
+ HTMLHeadline: String,
+ HTMLContent: String,
+ HTMLStringBuilder: StringBuilder,
+ sorted: Boolean,
+ maximumHeadlinePreviewLength: Int
+ ): Unit = {
+ // Set headline text
+ val head =
+ if (comment.label.nonEmpty) comment.label
+ else if (label.nonEmpty) label
+ else value
+
+ // If there is no brief preview, put the value into it
+ val brief = if (comment.brief.isEmpty) {
+ s"Value: ${StringEscapeUtils.escapeHtml4(value)} \n"
+ } else {
+ StringEscapeUtils.escapeHtml4(comment.brief)
+ }
+
+ // Adds Header line with collapse + expand options
+ HTMLStringBuilder ++= HTMLHeadline.replace("$label", StringEscapeUtils.escapeHtml4(head)).replace(
+ "$brief",
+ brief
+ )
+ HTMLStringBuilder ++= "\n"
+
+ // Write value into HTML code
+ val splitContent = HTMLContent.split("\\$content")
+ HTMLStringBuilder ++= splitContent(0)
+ comment.toHTML(HTMLStringBuilder)
+ HTMLStringBuilder ++= "Value: "
+ HTMLStringBuilder ++= StringEscapeUtils.escapeHtml4(value)
+ HTMLStringBuilder ++= " \n"
+ HTMLStringBuilder ++= splitContent(1)
+ }
+
+ /**
+ * Checks if the value object is empty.
+ * @return true if both the value and the comment are empty.
+ */
+ override def isEmpty: Boolean = {
+ if (value.isEmpty && comment.isEmpty) return true
+ false
+ }
+
+ /**
+ * Collapse is not needed in config Entry, due to it not having any sub-objects.
+ */
+ override def collapse(): Unit = {}
+
+ /**
+ * Expand is not needed in config Entry, due to it not having any sub-objects.
+ */
+ override def expand(): Unit = {}
+
+ /**
+ * Method for replacing a potential subclass type in the comment of the entry.
+ * @param se Accepts an initialized SubclassExtractor containing the ClassHierarchy required for a successful replacement.
+ */
+ override def replaceClasses(se: SubclassExtractor): Unit = {
+ comment = comment.replaceClasses(se)
+ }
+}
diff --git a/TOOLS/ce/src/main/scala/org.opalj.ce/ConfigList.scala b/TOOLS/ce/src/main/scala/org.opalj.ce/ConfigList.scala
new file mode 100644
index 0000000000..2107cae5a2
--- /dev/null
+++ b/TOOLS/ce/src/main/scala/org.opalj.ce/ConfigList.scala
@@ -0,0 +1,91 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package ce
+
+import scala.collection.mutable.ListBuffer
+
+import org.apache.commons.text.StringEscapeUtils
+
+/**
+ * Stores a List structure inside the ConfigNode structure.
+ * @param entries contains a List of ConfigNodes.
+ * @param comment are all the comments associated with the List.
+ */
+case class ConfigList(entries: ListBuffer[ConfigNode], var comment: DocumentationComment) extends ConfigNode {
+ /**
+ * Formats the entry into HTML code.
+ *
+ * @param label required if the Object is part of another object (Writes the key of the K,V Map there instead). Overrides the label property of the Comment object. Supply an empty string if not needed.
+ * @param HTMLHeadline accepts the HTML syntax of the Headline of the value. Can contain $ label and $ brief flags for filling with content.
+ * @param HTMLContent accepts the HTML syntax of the content frame for the value. Must contains a $ content flag for correct rendering.
+ * @param HTMLStringBuilder accepts a StringBuilder. The method adds the HTML String to this StringBuilder.
+ * @param sorted accepts a boolean to indicate if the export should sort the keys of the configObjects alphabetically.
+ * @param maximumHeadlinePreviewLength accepts an integer that determines the maximum amount of characters that the fallback brief preview can contain.
+ */
+ override def toHTML(
+ label: String,
+ HTMLHeadline: String,
+ HTMLContent: String,
+ HTMLStringBuilder: StringBuilder,
+ sorted: Boolean,
+ maximumHeadlinePreviewLength: Int
+ ): Unit = {
+ val head = if (comment.label.nonEmpty) {
+ comment.label
+ } else {
+ label
+ }
+
+ val brief = comment.getBrief(maximumHeadlinePreviewLength)
+
+ // Adds Header line with collapse + expand options
+ HTMLStringBuilder ++= HTMLHeadline.replace("$label", StringEscapeUtils.escapeHtml4(head)).replace(
+ "$brief",
+ StringEscapeUtils.escapeHtml4(brief)
+ )
+ HTMLStringBuilder ++= "\n"
+
+ // Write value into HTML code
+ val splitContent = HTMLContent.split("\\$content")
+ HTMLStringBuilder ++= splitContent(0)
+ comment.toHTML(HTMLStringBuilder)
+ for (entry <- entries) {
+ entry.toHTML("", HTMLHeadline, HTMLContent, HTMLStringBuilder, sorted, maximumHeadlinePreviewLength)
+ HTMLStringBuilder ++= "\n"
+ }
+ HTMLStringBuilder ++= " \n"
+ HTMLStringBuilder ++= splitContent(1)
+ }
+
+ /**
+ * Checks if the list is empty.
+ * @return true if both the List and the comment are empty.
+ */
+ override def isEmpty: Boolean = {
+ comment.isEmpty && entries.forall(_.isEmpty)
+ }
+
+ /**
+ * This method collapses the object structure by joining inheriting objects containing only one value.
+ * Inverse function of expand.
+ */
+ override def collapse(): Unit = {
+ entries.foreach(entry => entry.collapse())
+ }
+
+ /**
+ * This method expands the current object to represent all ob-objects within the structure.
+ * Inverse function of collapse.
+ */
+ override def expand(): Unit = {
+ entries.foreach(entry => entry.expand())
+ }
+
+ /**
+ * Iterator for replacing subclass types of all members of the List.
+ * @param se Accepts an initialized SubclassExtractor containing the ClassHierarchy required for a successful replacement.
+ */
+ override def replaceClasses(se: SubclassExtractor): Unit = {
+ entries.foreach(entry => entry.replaceClasses(se))
+ }
+}
diff --git a/TOOLS/ce/src/main/scala/org.opalj.ce/ConfigNode.scala b/TOOLS/ce/src/main/scala/org.opalj.ce/ConfigNode.scala
new file mode 100644
index 0000000000..cc0d0c8a4d
--- /dev/null
+++ b/TOOLS/ce/src/main/scala/org.opalj.ce/ConfigNode.scala
@@ -0,0 +1,52 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package ce
+
+/**
+ * Trait for representing the config structure
+ */
+trait ConfigNode {
+ var comment: DocumentationComment
+
+ /**
+ * Method for handling the export of the configuration structure into an HTML file.
+ * @param label required if the Object is part of another object (Writes the key of the K,V Map there instead). Overrides the label property of the Comment object. Supply an empty string if not needed.
+ * @param HTMLHeadline accepts the HTML syntax of the Headline of the value. Can contain $ label and $ brief flags for filling with content.
+ * @param HTMLContent accepts the HTML syntax of the content frame for the value. Must contains a $ content flag for correct rendering.
+ * @param HTMLStringBuilder accepts a StringBuilder. The method adds the HTML String to this StringBuilder.
+ * @param sorted accepts a boolean to indicate if the export should sort the keys of the configObjects alphabetically.
+ * @param maximumHeadlinePreviewLength accepts an integer that determines the maximum amount of characters that the fallback brief preview can contain.
+ */
+ def toHTML(
+ label: String,
+ HTMLHeadline: String,
+ HTMLContent: String,
+ HTMLStringBuilder: StringBuilder,
+ sorted: Boolean,
+ maximumHeadlinePreviewLength: Int
+ ): Unit
+
+ /**
+ * Checks if the configNode (and its potential child objects are empty.
+ * @return Returns true, if the ConfigNode, its comment and its childObjects are all empty. Returns false otherwise.
+ */
+ def isEmpty: Boolean
+
+ /**
+ * This method expands the current object to represent all objects within the structure.
+ * Inverse function of collapse.
+ */
+ def expand(): Unit
+
+ /**
+ * This method collapses the object structure by joining inheriting objects containing only one value.
+ * Inverse function of expand.
+ */
+ def collapse(): Unit
+
+ /**
+ * Method for replacing a potential subclass type in the comment of the Node.
+ * @param se Accepts an initialized SubclassExtractor containing the ClassHierarchy required for a successful replacement.
+ */
+ def replaceClasses(se: SubclassExtractor): Unit
+}
diff --git a/TOOLS/ce/src/main/scala/org.opalj.ce/ConfigObject.scala b/TOOLS/ce/src/main/scala/org.opalj.ce/ConfigObject.scala
new file mode 100644
index 0000000000..481de5a41d
--- /dev/null
+++ b/TOOLS/ce/src/main/scala/org.opalj.ce/ConfigObject.scala
@@ -0,0 +1,208 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package ce
+
+import scala.collection.mutable
+
+import org.opalj.log.GlobalLogContext
+import org.opalj.log.LogContext
+import org.opalj.log.OPALLogger
+
+import org.apache.commons.text.StringEscapeUtils
+
+/**
+ * Stores a List structure inside the ConfigNode structure.
+ * @param entries contains a K,V Map of ConfigNodes.
+ * @param comment are all the comments associated with the Object.
+ */
+case class ConfigObject(var entries: mutable.Map[String, ConfigNode], var comment: DocumentationComment)
+ extends ConfigNode {
+ implicit val logContext: LogContext = GlobalLogContext
+ /**
+ * Formats the entry into HTML code.
+ * @param label required if the Object is part of another object (Writes the key of the K,V Map there instead). Overrides the label property of the Comment object. Supply an empty string if not needed.
+ * @param HTMLHeadline accepts the HTML syntax of the Headline of the value. Can contain $ label and $ brief flags for filling with content.
+ * @param HTMLContent accepts the HTML syntax of the content frame for the value. Must contains a $ content flag for correct rendering.
+ * @param HTMLStringBuilder accepts a StringBuilder. The method adds the HTML String to this StringBuilder.
+ * @param sorted accepts a boolean to indicate if the export should sort the keys of the configObjects alphabetically.
+ * @param maximumHeadlinePreviewLength accepts an integer that determines the maximum amount of characters that the fallback brief preview can contain.
+ */
+ override def toHTML(
+ label: String,
+ HTMLHeadline: String,
+ HTMLContent: String,
+ HTMLStringBuilder: StringBuilder,
+ sorted: Boolean,
+ maximumHeadlinePreviewLength: Int
+ ): Unit = {
+ val head = if (comment.label.nonEmpty) {
+ comment.label
+ } else {
+ label
+ }
+
+ val brief = comment.getBrief(maximumHeadlinePreviewLength)
+
+ // Adds Header line with collapse + expand options
+ HTMLStringBuilder ++= HTMLHeadline.replace("$label", StringEscapeUtils.escapeHtml4(head)).replace(
+ "$brief",
+ StringEscapeUtils.escapeHtml4(brief)
+ )
+ HTMLStringBuilder ++= "\n"
+
+ // Write value into HTML code
+ val splitContent = HTMLContent.split("\\$content")
+ HTMLStringBuilder ++= splitContent(0)
+ comment.toHTML(HTMLStringBuilder)
+ if (sorted) {
+ val sortedKeys = entries.keys.toSeq.sorted
+ for (key <- sortedKeys) {
+ entries(key).toHTML(
+ key,
+ HTMLHeadline,
+ HTMLContent,
+ HTMLStringBuilder,
+ sorted,
+ maximumHeadlinePreviewLength
+ )
+ HTMLStringBuilder ++= "\n"
+ }
+ } else {
+ for ((key, node) <- entries) {
+ node.toHTML(key, HTMLHeadline, HTMLContent, HTMLStringBuilder, sorted, maximumHeadlinePreviewLength)
+ HTMLStringBuilder ++= "\n"
+ }
+ }
+ HTMLStringBuilder ++= " \n"
+ HTMLStringBuilder ++= splitContent(1)
+
+ HTMLStringBuilder.toString
+ }
+
+ /**
+ * Checks if the object is empty.
+ * @return true if both the Object and the comment are empty.
+ */
+ override def isEmpty: Boolean = {
+ if (!comment.isEmpty) return false
+ for ((key, value) <- entries) {
+ if (!value.isEmpty) return false
+ }
+ true
+ }
+
+ /**
+ * Merges two type compatible objects.
+ * This means that the objects are free of conflicting values and lists. Objects are allowed to overlap as long as there are no conflicts down the tree.
+ * @param insertingObject Is the object that is supposed to be merged into the executing one.
+ */
+ def merge(insertingObject: ConfigObject): Unit = {
+
+ // Expanding both objects guarantees compatible key naming syntax
+ expand()
+ insertingObject.expand()
+
+ // Insert object
+ for (kvpair <- insertingObject.entries) {
+ val (key, value) = kvpair
+ if (entries.contains(key)) {
+ val conflicting_entry = entries.getOrElse(key, null)
+ if (conflicting_entry.isInstanceOf[ConfigObject] && value.isInstanceOf[ConfigObject]) {
+ val conflicting_child_object = conflicting_entry.asInstanceOf[ConfigObject]
+ conflicting_child_object.merge(value.asInstanceOf[ConfigObject])
+ } else {
+ OPALLogger.error("Configuration Explorer", s"Info on incompatible keys: ${key.trim}")
+ throw new IllegalArgumentException(
+ s"Unable to merge incompatible types: ${value.getClass} & ${conflicting_entry.getClass}"
+ )
+ }
+ } else {
+ OPALLogger.info("Configuration Explorer", s"No conflict detected. Inserting ${key.trim}")
+ entries += kvpair
+ }
+ }
+
+ collapse()
+ }
+
+ /**
+ * This method collapses the object structure by joining inheriting objects containing only one value.
+ * Inverse function of expand.
+ */
+ def collapse(): Unit = {
+ for (entry <- entries) {
+ val (key, value) = entry
+ value.collapse()
+
+ // If the entry is a config object with exactly one child -> merge
+ if (value.isInstanceOf[ConfigObject]) {
+ val value_object = value.asInstanceOf[ConfigObject]
+ if (value_object.entries.size == 1) {
+ // Merge Keys
+ val (childkey, childvalue) = value_object.entries.head
+ val newkey = key.trim + "." + childkey.trim
+
+ // Merge comments
+ childvalue.comment = childvalue.comment.mergeComment(value_object.comment)
+
+ // Add new object
+ entries += (newkey -> childvalue)
+
+ // Remove old object
+ entries -= key
+ }
+ }
+ }
+ if (entries.size == 1) {
+ if (comment.isEmpty) {} else {
+ val (key, value) = entries.head
+ if (value.comment.isEmpty) {}
+ }
+ }
+ }
+
+ /**
+ * This method expands the current object to represent all objects within the structure.
+ * Inverse function of collapse.
+ */
+ def expand(): Unit = {
+ for (entry <- entries) {
+ // Expand substructure of monitored object
+ val (key, value) = entry
+ value.expand()
+
+ if (key.contains(".")) {
+ // Create expanded object
+ val newkey = key.trim.split("\\.", 2)
+ val new_entry = mutable.Map[String, ConfigNode](newkey(1).trim -> value)
+ val new_object = ConfigObject(new_entry, new DocumentationComment("", "", Seq(), "", Seq()))
+ new_object.expand()
+ if (entries.contains(newkey(0).trim)) {
+ if (entries(newkey(0).trim).isInstanceOf[ConfigObject]) {
+ entries(newkey(0).trim).asInstanceOf[ConfigObject].merge(new_object)
+ } else {
+ // If the child object already exists and is NOT a config object, the config structure has a label conflict (Problem!)
+ throw new IllegalArgumentException(
+ s"Unable to Merge ${newkey(0).trim} due to incompatible types: ${entries(newkey(0).trim).getClass}"
+ )
+ }
+ } else {
+ entries += (newkey(0).trim -> new_object)
+ }
+
+ // Delete old entry from the map to avoid duplicates
+ entries -= key
+ }
+ }
+ }
+
+ /**
+ * Iterator for replacing subclass types of all members of the Object.
+ * @param se Accepts an initialized SubclassExtractor containing the ClassHierarchy required for a successful replacement.
+ */
+ override def replaceClasses(se: SubclassExtractor): Unit = {
+ for ((key, value) <- entries) {
+ value.replaceClasses(se)
+ }
+ }
+}
diff --git a/TOOLS/ce/src/main/scala/org.opalj.ce/ConfigurationExplorer.scala b/TOOLS/ce/src/main/scala/org.opalj.ce/ConfigurationExplorer.scala
new file mode 100644
index 0000000000..87a48a7bbe
--- /dev/null
+++ b/TOOLS/ce/src/main/scala/org.opalj.ce/ConfigurationExplorer.scala
@@ -0,0 +1,61 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package ce
+
+import java.io.File
+import java.nio.file.Paths
+
+import com.typesafe.config.Config
+import com.typesafe.config.ConfigFactory
+
+import org.opalj.log.GlobalLogContext
+import org.opalj.log.LogContext
+import org.opalj.log.OPALLogger
+
+/*
+ * Standalone configuration explorer.
+ * This is the main method that runs the configuration explorer.
+ * It creates a browsable HTML File out of the configuration files present in the entire OPAL project.
+ */
+object ConfigurationExplorer extends App {
+ implicit val logContext: LogContext = GlobalLogContext
+
+ OPALLogger.info("Configuration Explorer", "Configuration Explorer Started")
+ val buildVersion = System.getProperty("build.version", "unknown")
+
+ // Load config with default filename for this application
+ val conf = LoadConfig()
+
+ val locator = new FileLocator(conf)
+ val filepaths = locator.getConfigurationPaths
+
+ // Bulk Imports all the configs
+ val CPW = new CommentParser
+ val configs = CPW.iterateConfigs(filepaths, Paths.get(locator.getProjectRoot))
+
+ // Replace class type values
+ if (conf.getBoolean("org.opalj.ce.replaceSubclasses")) {
+ val se = new SubclassExtractor(locator.findJarArchives(buildVersion).toArray)
+ for (config <- configs) {
+ config.replaceClasses(se)
+ }
+ }
+
+ // Export
+ val HE = new HTMLExporter(
+ configs,
+ Paths.get(locator.getProjectRoot + conf.getString("org.opalj.ce.html.template"))
+ )
+ HE.exportHTML(conf, new File(locator.getProjectRoot + conf.getString("org.opalj.ce.html.export")))
+
+ /**
+ * Loads default configuration of the configuration explorer
+ * @return returns the configuration of the configuration explorer
+ */
+ def LoadConfig(): Config = {
+ OPALLogger.info("Configuration Explorer", "Loading configuration")
+ val conf = ConfigFactory.load("ce")
+
+ conf
+ }
+}
diff --git a/TOOLS/ce/src/main/scala/org.opalj.ce/DocumentationComment.scala b/TOOLS/ce/src/main/scala/org.opalj.ce/DocumentationComment.scala
new file mode 100644
index 0000000000..0f986ac99f
--- /dev/null
+++ b/TOOLS/ce/src/main/scala/org.opalj.ce/DocumentationComment.scala
@@ -0,0 +1,153 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package ce
+
+import scala.collection.mutable.ListBuffer
+
+import org.apache.commons.text.StringEscapeUtils
+
+/**
+ * Container for the comments of a config node.
+ */
+class DocumentationComment(
+ val label: String,
+ val brief: String,
+ val description: Seq[String],
+ val datatype: String,
+ val constraints: Seq[String]
+) {
+
+ /**
+ * Converts the Comment object into HTML syntax.
+ * @param HTMLStringBuilder accepts a String builder. The method will add the export to this StringBuilder.
+ */
+ def toHTML(HTMLStringBuilder: StringBuilder): Unit = {
+ if (!isEmpty) {
+ if (description.mkString("").trim.nonEmpty) {
+ HTMLStringBuilder ++= "
\n"
+ }
+ }
+ }
+
+ /**
+ * Merges another comment into this comment.
+ * The datatype flag will not be merged. Reason for this is that datatypes are only used to describe entries, which cannot be merged anyways.
+ * @param comment accepts the comment that should be merged into this comment.
+ * @return returns a merged DocumentationComment.
+ */
+ def mergeComment(comment: DocumentationComment): DocumentationComment = {
+ val mergedLabel = if (label != "" && comment.label != "") {
+ s"${comment.label}.$label"
+ } else {
+ s"${comment.label}$label"
+ }
+ val mergedBrief = s"${comment.brief} $brief".trim
+ val mergedDescription = description ++ comment.description
+ val mergedConstraints = constraints ++ comment.constraints
+
+ new DocumentationComment(mergedLabel, mergedBrief, mergedDescription, "", mergedConstraints)
+ }
+
+ /**
+ * Checks if the comment is empty.
+ * @return returns true if the comment is empty but the label property (the label property is set automatically for config files.).
+ */
+ def isEmpty: Boolean = {
+ if (description.isEmpty && constraints.isEmpty && datatype.isEmpty) return true
+ false
+ }
+
+ /**
+ * Method used for fetching information of the brief field.
+ * @param previewDescriptionLength accepts an integer that determines the maximum amount of characters that the fallback brief preview can contain.
+ * @return Returns the brief field of the DocumentationComment if it exists. If it does not exist, it returns a preview of the description.
+ */
+ def getBrief(previewDescriptionLength: Int): String = {
+ if (brief.isEmpty) {
+ if (description.nonEmpty) {
+ return description.head.substring(0, description.head.length.min(previewDescriptionLength)) + "..."
+ }
+ }
+ brief
+ }
+
+ /**
+ * This method is responsible for finding all subclasses to a subclass type and adds them to the constraints.
+ * Then, it changes its datatype to enum to show that all allowed values are the listed classes.
+ * @param se Accepts an initialized subclass extractor. It accesses the ClassHierarchy that was extracted by the subclass extractor and finds its subclasses within the structure.
+ * @return Returns an Updated DocumentationComment if there were classes to replace. Returns itself otherwise.
+ */
+ def replaceClasses(se: SubclassExtractor): DocumentationComment = {
+ if (datatype.equals("subclass")) {
+ // Get a Set of all subclasses
+ val root = constraints.head
+
+ // Replace Types
+ val updatedConstraints = constraints ++ se.extractSubclasses(root)
+
+ new DocumentationComment(label, brief, description, "enum", updatedConstraints)
+ } else {
+ this
+ }
+ }
+ /**
+ * Prints the Comment object to the console.
+ * Debug purposes.
+ */
+ def printObject(): Unit = {
+ println("Constraints: " + constraints.toString())
+ println("Description: " + description.toString())
+ println("Label: " + label)
+ println("Brief: " + brief)
+ println("Type: " + datatype)
+ }
+}
+
+/**
+ * Factory method for creating a Comment.
+ */
+object DocumentationComment {
+ /**
+ * Factory method for creating a comment.
+ * @param commentBuffer accepts a ListBuffer that contains the raw content of the comment.
+ * @return is a fully functional Comment.
+ */
+ def fromString(commentBuffer: ListBuffer[String]): DocumentationComment = {
+ var label = ""
+ var brief = ""
+ val description = ListBuffer[String]()
+ var datatype = ""
+ val constraints = ListBuffer[String]()
+ for (line <- commentBuffer) {
+ val trimmedLine = line.trim
+ if (trimmedLine.startsWith("@label")) {
+ label = trimmedLine.stripPrefix("@label").trim
+ } else if (trimmedLine.startsWith("@brief")) {
+ brief = trimmedLine.stripPrefix("@brief").trim
+ } else if (trimmedLine.startsWith("@constraint")) {
+ constraints += trimmedLine.stripPrefix("@constraint").trim
+ } else if (trimmedLine.startsWith("@type")) {
+ datatype = trimmedLine.stripPrefix("@type").trim
+ } else {
+ description += trimmedLine.stripPrefix("@description").trim
+ }
+ }
+ new DocumentationComment(label, brief, description.toSeq, datatype, constraints.toSeq)
+ }
+}
diff --git a/TOOLS/ce/src/main/scala/org.opalj.ce/FileLocator.scala b/TOOLS/ce/src/main/scala/org.opalj.ce/FileLocator.scala
new file mode 100644
index 0000000000..09ee863285
--- /dev/null
+++ b/TOOLS/ce/src/main/scala/org.opalj.ce/FileLocator.scala
@@ -0,0 +1,114 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package ce
+
+import java.io.File
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.nio.file.attribute.BasicFileAttributes
+import scala.collection.immutable.Seq
+import scala.collection.mutable.ListBuffer
+import scala.jdk.CollectionConverters._
+
+import com.typesafe.config.Config
+
+import org.opalj.log.GlobalLogContext
+import org.opalj.log.LogContext
+import org.opalj.log.OPALLogger
+
+/**
+ * File Locator aids locating the Files that the configuration Explorer needs to parse.
+ * It can locate a range of files, but its internal usage by Configuration Explorer is locating configuration Files and Jar Archives containing the current build number.
+ * @param config accepts the config of the Configuration Explorer.
+ */
+class FileLocator(config: Config) {
+ implicit val logContext: LogContext = GlobalLogContext
+
+ /**
+ * Small helper method for finding the project root.
+ * @return returns the root directory of the opal project.
+ */
+ def getProjectRoot: String = {
+ val subprojectDirectory = config.getString("user.dir")
+ val projectRoot = Paths.get(subprojectDirectory).getParent.getParent.toString
+ OPALLogger.info("Configuration Explorer", s"Searching in the following directory: $projectRoot")
+ projectRoot
+ }
+
+ /**
+ * Loads the filenames of the configuration files that shall be parsed.
+ * @return is a List of the filenames that shall be parsed by the Configuration Explorer.
+ */
+ def getConfigurationFilenames: Seq[String] = {
+ val projectNames = config.getStringList("org.opalj.ce.configurationFilenames").asScala
+
+ OPALLogger.info("Configuration Explorer", "Loaded the following Filenames: ")
+ for (filename <- projectNames) {
+ OPALLogger.info("Configuration Explorer", filename)
+ }
+ projectNames.toSeq
+ }
+
+ /**
+ * Finds all files that are named after one of the configuration filenames and are NOT within the target folder structure.
+ * @return returns a List of full FilePaths to all found config files.
+ */
+ def getConfigurationPaths: Seq[Path] = {
+ val projectNames = getConfigurationFilenames
+ searchFiles(projectNames)
+ }
+
+ /**
+ * Finds all files that match the filename within the.
+ * @param Filenames Accepts a List of all filenames that should be included in the result.
+ * @return returns a List of full FilePaths to all found files.
+ */
+ def searchFiles(Filenames: Seq[String]): Seq[Path] = {
+ val projectRoot = Paths.get(getProjectRoot)
+ val foundFiles = ListBuffer[Path]()
+
+ Files.walkFileTree(
+ projectRoot,
+ new java.nio.file.SimpleFileVisitor[Path]() {
+ override def visitFile(file: Path, attrs: BasicFileAttributes): java.nio.file.FileVisitResult = {
+ if (Filenames.contains(file.getFileName.toString) && !file.toAbsolutePath.toString.contains(
+ "target\\scala"
+ ) && !file.toAbsolutePath.toString.contains("target/scala")
+ ) {
+ foundFiles += file
+ OPALLogger.info("Configuration Explorer", s"Found file: ${file.toString}")
+ }
+ java.nio.file.FileVisitResult.CONTINUE
+ }
+ }
+ )
+ foundFiles.toSeq
+ }
+
+ /**
+ * Finds all jar archives in the project, where the file name contains the pathWildcard.
+ * @param pathWildcard accepts a String to filter the filenames of the jar archives. Will only return jar archives that contain the parameter in their file name.
+ * @return Will only return jar archives that contain the parameter in their file name and that are not in the bg-jobs folder.
+ */
+ def findJarArchives(pathWildcard: String): Seq[File] = {
+ val projectRoot = Paths.get(getProjectRoot)
+ val foundFiles = ListBuffer[File]()
+ Files.walkFileTree(
+ projectRoot,
+ new java.nio.file.SimpleFileVisitor[Path]() {
+ override def visitFile(file: Path, attrs: BasicFileAttributes): java.nio.file.FileVisitResult = {
+ if (file.getFileName.toString.endsWith(".jar") && file.getFileName.toString.contains(
+ pathWildcard
+ ) && !file.toString.contains("bg-jobs")
+ ) {
+ foundFiles += file.toFile
+ OPALLogger.info("Configuration Explorer", s"Found file: ${file.toString}")
+ }
+ java.nio.file.FileVisitResult.CONTINUE
+ }
+ }
+ )
+ foundFiles.toSeq
+ }
+}
diff --git a/TOOLS/ce/src/main/scala/org.opalj.ce/HTMLExporter.scala b/TOOLS/ce/src/main/scala/org.opalj.ce/HTMLExporter.scala
new file mode 100644
index 0000000000..7cd37ced42
--- /dev/null
+++ b/TOOLS/ce/src/main/scala/org.opalj.ce/HTMLExporter.scala
@@ -0,0 +1,58 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package ce
+
+import java.io.File
+import java.io.PrintWriter
+import java.nio.file.Path
+import scala.io.Source
+import scala.util.Using
+
+import com.typesafe.config.Config
+
+/**
+ * Exports the Config structure into an HTML file.
+ * @param ConfigList Accepts a List of parsed Configuration Files.
+ * @param templatePath Accepts a Path to the HTML Template that should be used.
+ */
+class HTMLExporter(ConfigList: Iterable[ConfigNode], templatePath: Path) {
+ /**
+ * Exports the ConfigList into an HTML file.
+ * The following parameters are all read from the Configuration Explorer config, however, the CE config was not handed over due to namespace conflicts with the internally used ConfigNode.
+ * @param config Accepts the config of the ConfigurationExplorer in order to read necessary values from it directly.
+ * @param exportFile Accepts a Path to the file that the Config shall be written to.
+ */
+ def exportHTML(config: Config, exportFile: File): Unit = {
+ val HTMLHeadline = config.getString("org.opalj.ce.html.headline")
+ val HTMLContent = config.getString("org.opalj.ce.html.content")
+ val HTMLStringBuilder = new StringBuilder()
+ val sort_alphabetically = config.getBoolean("org.opalj.ce.html.sort_alphabetically")
+ val maximumHeadlinePreviewLength = config.getInt("org.opalj.ce.html.maximum_headline_preview_length")
+
+ // Generate HTML
+ var fileContent = ""
+ val template = Using(Source.fromFile(templatePath.toString)) { source =>
+ source.getLines().mkString("\n")
+ }.getOrElse("")
+ for (config <- ConfigList) {
+ if (!config.isEmpty) {
+ config.toHTML(
+ "",
+ HTMLHeadline,
+ HTMLContent,
+ HTMLStringBuilder,
+ sort_alphabetically,
+ maximumHeadlinePreviewLength
+ )
+ HTMLStringBuilder ++= "\n"
+ }
+ }
+ fileContent = template.replace("$body", HTMLStringBuilder.toString())
+
+ // Write to file
+ val printWriter = new PrintWriter(exportFile)
+ printWriter.write(fileContent)
+ printWriter.close()
+ }
+
+}
diff --git a/TOOLS/ce/src/main/scala/org.opalj.ce/SubclassExtractor.scala b/TOOLS/ce/src/main/scala/org.opalj.ce/SubclassExtractor.scala
new file mode 100644
index 0000000000..27a29cf921
--- /dev/null
+++ b/TOOLS/ce/src/main/scala/org.opalj.ce/SubclassExtractor.scala
@@ -0,0 +1,38 @@
+/* BSD 2-Clause License - see OPAL/LICENSE for details. */
+package org.opalj
+package ce
+
+import java.io.File
+import java.net.URL
+import scala.collection.mutable
+
+import org.opalj.br.ClassHierarchy
+import org.opalj.br.ObjectType
+import org.opalj.br.ObjectType.unapply
+import org.opalj.br.analyses.Project
+
+/**
+ * The class subclassExtractor is a wrapper class around the bytecode representation project.
+ * It will fetch all subclasses from the opal project and prepares the bytecode hierarchies for querying.
+ * @param files accepts an array of files of the jar archives that should be included in the ClassHierarchies
+ */
+class SubclassExtractor(files: Array[File]) {
+ val p: Project[URL] = Project.apply(files, Array(org.opalj.bytecode.RTJar))
+ val classHierarchy: ClassHierarchy = p.classHierarchy
+
+ /**
+ * This method queries the extracted class hierarchies for a class.
+ * @param root accepts a string class name in dot notation. E.g. "org.opalj.ce.ConfigNode".
+ * @return returns a set of the names of all subclasses of the root subclass.
+ */
+ def extractSubclasses(root: String): Seq[String] = {
+ val results = mutable.Set[String]()
+ val unformattedresult = classHierarchy.subtypeInformation(ObjectType(root.replace(".", "/"))).orNull
+ if (unformattedresult != null) {
+ for (entry <- unformattedresult.classTypes) {
+ results += unapply(entry).getOrElse("").replace("/", ".")
+ }
+ }
+ results.toSeq
+ }
+}
diff --git a/build.sbt b/build.sbt
index 4e5e85a927..81ca6784e7 100644
--- a/build.sbt
+++ b/build.sbt
@@ -184,6 +184,7 @@ lazy val `OPAL` = (project in file("."))
// bp, (just temporarily...)
tools,
hermes,
+ ce,
validate, // Not deployed to maven central
demos // Not deployed to maven central
)
@@ -444,6 +445,31 @@ lazy val `Tools` = (project in file("DEVELOPING_OPAL/tools"))
.dependsOn(framework % "it->it;it->test;test->test;compile->compile")
.configs(IntegrationTest)
+lazy val ce = (project in file("TOOLS/ce"))
+ .settings(buildSettings: _*)
+ .settings(
+ fork := true,
+ javaOptions += s"-Dbuild.version=${version.value}",
+ name := "ce",
+ libraryDependencies ++= Dependencies.ce,
+ Compile / doc := {
+ // Overrides doc method to include config documentation at doc
+ val originalDoc = (Compile / doc).value
+ (Compile / compile).value
+ (Compile / run).toTask("").value
+ originalDoc
+ },
+ Compile / doc / scalacOptions ++= Opts.doc.title("OPAL - Configuration Explorer")
+ )
+.dependsOn(
+ br % "compile->compile",
+ apk % "runtime->compile",
+ demos % "runtime->compile",
+ // bp % "runtime->compile",
+ hermes % "runtime->compile"
+)
+.configs(IntegrationTest)
+
/** ***************************************************************************
*
* PROJECTS BELONGING TO THE OPAL ECOSYSTEM
diff --git a/project/Dependencies.scala b/project/Dependencies.scala
index be2ea989e3..7349e57603 100644
--- a/project/Dependencies.scala
+++ b/project/Dependencies.scala
@@ -82,4 +82,5 @@ object Dependencies {
val apk = Seq(apkparser, scalaxml)
val llvm = Seq(javacpp, javacpp_llvm)
+ val ce = Seq(commonstext)
}