Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

use JSON Serialization to provide the value returned by the metaquery #24

Merged
merged 3 commits into from
Oct 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

package gov.nist.secauto.oscal.tools.server.core.commands

import gov.nist.secauto.metaschema.core.metapath.item.JsonItemWriter;
import gov.nist.secauto.metaschema.core.metapath.item.IItem;
import gov.nist.secauto.oscal.lib.OscalModelConstants;
import gov.nist.secauto.oscal.lib.model.OscalCompleteModule
import gov.nist.secauto.oscal.lib.OscalBindingContext
Expand Down Expand Up @@ -107,15 +109,9 @@ class QueryCommand : AbstractTerminalCommand() {
val cwd = Paths.get("").toAbsolutePath().toUri()
LOGGER.info("Current working directory: $cwd")

val (module, item) = when {
cmdLine.hasOption(METASCHEMA_OPTION) -> {
LOGGER.info("Metaschema option detected")
val item: IItem? = when {
cmdLine.hasOption(CONTENT_OPTION) -> {
try {
val moduleName = cmdLine.getOptionValue(METASCHEMA_OPTION)
LOGGER.info("Module name: $moduleName")
val moduleUri = UriUtils.toUri(moduleName, cwd)
LOGGER.info("Module URI: $moduleUri")

val oscalBindingContext = OscalBindingContext.instance()
LOGGER.info("Created OSCAL binding context")
val module = oscalBindingContext.registerModule(OscalCompleteModule::class.java)
Expand All @@ -138,7 +134,7 @@ class QueryCommand : AbstractTerminalCommand() {

try {
LOGGER.info("Loading content as node item")
Pair(module, loader.loadAsNodeItem(contentResource))
loader.loadAsNodeItem(contentResource)
} catch (ex: IOException) {
LOGGER.error("Failed to load content", ex)
return ExitCode.INVALID_ARGUMENTS
Expand All @@ -147,7 +143,7 @@ class QueryCommand : AbstractTerminalCommand() {
}
} else {
LOGGER.info("No content option, creating new module node item")
Pair(module, INodeItemFactory.instance().newModuleNodeItem(module))
INodeItemFactory.instance().newModuleNodeItem(module)
}
} catch (ex: URISyntaxException) {
LOGGER.error("Invalid URI syntax", ex)
Expand All @@ -162,15 +158,9 @@ class QueryCommand : AbstractTerminalCommand() {
return ExitCode.PROCESSING_ERROR.exit().withThrowable(ex)
}
}
cmdLine.hasOption(CONTENT_OPTION) -> {
LOGGER.warn("Content option provided without Metaschema option")
return ExitCode.INVALID_ARGUMENTS.exitMessage(
"Must use '${CONTENT_OPTION.argName}' to specify the Metaschema module."
)
}
else -> {
LOGGER.info("No Metaschema or Content option provided")
Pair(null, null)
LOGGER.info("No Content option provided")
null
Comment on lines -165 to +163
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: in some cases this should work, there has been a regression potentially. FYSA:

metaschema-framework/metaschema-java#228

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should pursue this in another PR, but this is good to know, so a metapath just executing inline?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should pursue this in another PR

Agreed, feel free to resolve.

but this is good to know, so a metapath just executing inline?

Correct, when there are lower-level syntax errors, I actually use this, you may have noticed I also use it during our pairing sessions, the local API approach will make it much faster. 😉

% oscal-cli --version                                                  
oscal-cli 2.2.0 built at 2024-10-08 23:48 from branch 0b9478792d27837a8967cc72a0c98776b24f7102 (0b94787) at https://github.com/metaschema-framework/oscal-cli
liboscal-java  built at 2024-10-08 22:12 from branch 0e7de882592dedef37a1fc30101393e6c4fe71f3 (0e7de88) at https://github.com/metaschema-framework/liboscal-java
oscal v1.1.2 built at 2024-10-08 22:12 from branch 4f02dac6f698efda387cc5f55bc99581eaf494b6 (4f02dac) at https://github.com/usnistgov/OSCAL.git
metaschema-java 1.2.0 built at 2024-10-08T20:00:42+0000 from branch 46df8d8fc25c5de1d7cb0485e534f31efe61b2b7 (46df8d8) at https://github.com/metaschema-framework/metaschema-java
metaschema  built at 2024-10-08T20:00:42+0000 from branch 7c03ce5844e46cf9d047193a37e44422ae6a7d61 (7c03ce5) at https://github.com/metaschema-framework/metaschema.git
% oscal-cli metaschema metapath eval -e "2 * 2"
4

I use this to check syntax before/with constraint authoring. This found some bugs with the output wrapping with sequence () and other output weirdness in #228.

}
}

Expand All @@ -184,10 +174,15 @@ class QueryCommand : AbstractTerminalCommand() {
.defaultModelNamespace(OscalModelConstants.NS_URI_OSCAL)
.build());
LOGGER.info("Compiling Metapath expression")
LOGGER.info("Compiling Metapath expression")
LOGGER.info("Compiling Metapath expression")
val compiledMetapath: MetapathExpression = MetapathExpression.compile(expression, staticContext)
LOGGER.info("Metapath expression compiled successfully")
val compiledMetapath: MetapathExpression = try {
MetapathExpression.compile(expression, staticContext).also {
LOGGER.info("Metapath expression compiled successfully")
}
} catch (ex: Exception) { // Replace with actual exception type
LOGGER.error("Metapath expression did not compile", ex)
return ExitCode.FAIL.exit()
}


LOGGER.info("Evaluating compiled Metapath expression")
val sequence: ISequence<INodeItem> = compiledMetapath.evaluate<INodeItem>(item, dynamicContext)
Expand All @@ -196,7 +191,7 @@ class QueryCommand : AbstractTerminalCommand() {
val stringWriter = StringWriter()
PrintWriter(stringWriter).use { writer ->
LOGGER.info("Writing sequence to string")
val itemWriter: IItemWriter = DefaultItemWriter(writer)
val itemWriter: IItemWriter = JsonItemWriter(writer,OscalBindingContext.instance())
wandmagic marked this conversation as resolved.
Show resolved Hide resolved
itemWriter.writeSequence(sequence)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
package gov.nist.secauto.metaschema.core.metapath.item
import gov.nist.secauto.metaschema.databind.io.SerializationFeature;
import gov.nist.secauto.metaschema.core.model.IBoundObject;
import gov.nist.secauto.metaschema.core.metapath.ICollectionValue
import gov.nist.secauto.metaschema.core.metapath.ISequence
import gov.nist.secauto.metaschema.core.metapath.item.atomic.IAnyAtomicItem
import gov.nist.secauto.metaschema.core.metapath.item.function.IArrayItem
import gov.nist.secauto.metaschema.core.metapath.item.function.IMapItem
import gov.nist.secauto.metaschema.core.metapath.item.node.INodeItem
import gov.nist.secauto.metaschema.databind.IBindingContext
import gov.nist.secauto.metaschema.databind.io.Format
import java.io.PrintWriter
import java.io.StringWriter

/**
* Produces a JSON representation of a Metapath sequence.
*/
class JsonItemWriter(
private val writer: PrintWriter,
private val bindingContext: IBindingContext
) : IItemWriter {
private var indentLevel = 0
private val visitor = Visitor()

companion object {
private const val INDENT = " " // 2 spaces for indentation
}

private fun writeIndent() {
repeat(indentLevel) {
writer.append(INDENT)
}
}

private fun Any?.serializeValue(): String {
return try {
when (this) {
null -> "null"
is String -> "\"${escapeJson()}\""
is Number, is Boolean -> toString()
else -> try {
StringWriter().use { stringWriter ->
val boundObject = this as? IBoundObject ?: run {
// Add debug information before potential NPE
println("Debug: toString() called on object of type: ${this?.javaClass}")
return "\"${toString()?.escapeJson() ?: "null"}\""
}

val boundClass = boundObject::class.java
println("Debug: Processing bound class: ${boundClass.name}")

val boundDefinition = bindingContext.getBoundDefinitionForClass(boundClass)
println("Debug: Bound definition: ${boundDefinition != null}")
if (boundDefinition != null) {
val serializer = bindingContext.newSerializer(Format.JSON, boundClass)
serializer.set(SerializationFeature.SERIALIZE_ROOT, false);
serializer.serialize(boundObject, stringWriter)
println(stringWriter.toString());
stringWriter.toString()
} else {
"\"${boundObject.toString()?.escapeJson() ?: "null"}\""
}
}
} catch (e: Exception) {
StringWriter().use { sw ->
PrintWriter(sw).use { pw ->
e.printStackTrace(pw)
println("Inner Exception Stack Trace:")
println(sw.toString())
"\"Error during serialization: ${e.message}\nStack trace: ${sw.toString().escapeJson()}\""
}
}
}
}
} catch (e: Exception) {
StringWriter().use { sw ->
PrintWriter(sw).use { pw ->
e.printStackTrace(pw)
println("Outer Exception Stack Trace:")
println(sw.toString())
"\"Error in main serializeValue: ${e.message}\nStack trace: ${sw.toString().escapeJson()}\""
}
}
}
}

private fun writeJsonObject(
type: String,
item: IItem,
additionalContent: JsonItemWriter.() -> Unit = {}
) {
writer.append("{\n")
indentLevel++
writeIndent()
writer.append("\"type\": \"$type\",\n")
writeIndent()
writer.append("\"value\": ${item.getValue().serializeValue()},\n")
additionalContent()
indentLevel--
writeIndent()
writer.append("}")
}

private fun writeJsonArray(
name: String,
content: JsonItemWriter.() -> Unit
) {
writeIndent()
writer.append("\"$name\": [\n")
indentLevel++
content()
writer.append('\n')
indentLevel--
writeIndent()
writer.append("]\n")
}

override fun writeSequence(sequence: ISequence<*>) {
writer.append("{\n")
indentLevel++
writeIndent()
writer.append("\"type\": \"sequence\",\n")
writeJsonArray("items") {
var first = true
sequence.forEach { item ->
if (!first) writer.append(",\n")
writeIndent()
item.accept(visitor)
first = false
}
}
indentLevel--
writeIndent()
writer.append("}")
}

override fun writeArray(array: IArrayItem<*>) {
writeJsonObject("array", array) {
writeJsonArray("values") {
var first = true
array.forEach { value ->
checkNotNull(value)
if (!first) writer.append(",\n")
writeIndent()
writeCollectionValue(value)
first = false
}
}
}
}

override fun writeMap(map: IMapItem<*>) {
writeJsonObject("map", map) {
writeJsonArray("entries") {
var first = true
val mapValues = map.values
mapValues.forEach { value ->
checkNotNull(value)
if (!first) writer.append(",\n")
writeIndent()
writeCollectionValue(value)
first = false
}
}
}
}

override fun writeNode(node: INodeItem) {
writeJsonObject("node", node) {
writeIndent()
writer.append("\"baseUri\": \"${node.baseUri.toString().escapeJson()}\",\n")
writeIndent()
writer.append("\"path\": \"${node.metapath.escapeJson()}\"\n")
}
}

override fun writeAtomicValue(node: IAnyAtomicItem) {
writeJsonObject("atomic", node) {
writeIndent()
writer.append("\"text\": \"${node.asString().escapeJson()}\"\n")
}
}

protected fun writeCollectionValue(value: ICollectionValue) {
when (value) {
is IItem -> value.accept(visitor)
is ISequence<*> -> writeSequence(value)
}
}

/**
* Escapes special characters in JSON strings.
*/
private fun String.escapeJson(): String = buildString {
[email protected] { char ->
when (char) {
'"' -> append("\\\"")
'\\' -> append("\\\\")
'\b' -> append("\\b")
'\u000C' -> append("\\f")
'\n' -> append("\\n")
'\r' -> append("\\r")
'\t' -> append("\\t")
else -> if (char < ' ') {
append(String.format("\\u%04x", char.code))
} else {
append(char)
}
}
}
}

private inner class Visitor : IItemVisitor {
override fun visit(array: IArrayItem<*>) = writeArray(array)
override fun visit(map: IMapItem<*>) = writeMap(map)
override fun visit(node: INodeItem) = writeNode(node)
override fun visit(node: IAnyAtomicItem) = writeAtomicValue(node)
}
}
Loading