Skip to content

Commit

Permalink
feat: outputter allow indent
Browse files Browse the repository at this point in the history
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
  • Loading branch information
jkowalleck committed Oct 2, 2023
1 parent 16843b2 commit 8e9400d
Show file tree
Hide file tree
Showing 5 changed files with 60 additions and 59 deletions.
8 changes: 5 additions & 3 deletions cyclonedx/output/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,12 @@ def generate(self, force_regeneration: bool = False) -> None:
...

@abstractmethod
def output_as_string(self) -> str:
def output_as_string(self, *,
indent: Optional[Union[int, str]] = None) -> str:
...

def output_to_file(self, filename: str, allow_overwrite: bool = False) -> None:
def output_to_file(self, filename: str, allow_overwrite: bool = False, *,
indent: Optional[Union[int, str]] = None) -> None:
# Check directory writable
output_filename = os.path.realpath(filename)
output_directory = os.path.dirname(output_filename)
Expand All @@ -84,7 +86,7 @@ def output_to_file(self, filename: str, allow_overwrite: bool = False) -> None:
if os.path.exists(output_filename) and not allow_overwrite:
raise FileExistsError(output_filename)
with open(output_filename, mode='wb') as f_out:
f_out.write(self.output_as_string().encode('utf-8'))
f_out.write(self.output_as_string(indent=indent).encode('utf-8'))


def get_instance(bom: Bom, output_format: OutputFormat = OutputFormat.XML,
Expand Down
41 changes: 18 additions & 23 deletions cyclonedx/output/json.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) OWASP Foundation. All Rights Reserved.

import json
from abc import abstractmethod
from typing import Dict, Optional, Type
from json import dumps as json_dumps, loads as json_loads
from typing import Any, Dict, Optional, Type, Union

from ..exception.output import FormatNotSupportedException
from ..model.bom import Bom
Expand All @@ -40,7 +40,7 @@ class Json(BaseOutput, BaseSchemaVersion):

def __init__(self, bom: Bom) -> None:
super().__init__(bom=bom)
self._json_output: str = ''
self._bom_json: Dict[str, Any] = dict()

@property
def schema_version(self) -> SchemaVersion:
Expand All @@ -51,7 +51,9 @@ def output_format(self) -> OutputFormat:
return OutputFormat.JSON

def generate(self, force_regeneration: bool = False) -> None:
# New Way
if self.generated and not force_regeneration:
return

schema_uri: Optional[str] = self._get_schema_uri()
if not schema_uri:
raise FormatNotSupportedException(
Expand All @@ -63,26 +65,19 @@ def generate(self, force_regeneration: bool = False) -> None:
'specVersion': self.schema_version.to_version()
}
_view = SCHEMA_VERSIONS.get(self.schema_version_enum)
if self.generated and force_regeneration:
self.get_bom().validate()
bom_json = json.loads(self.get_bom().as_json(view_=_view)) # type: ignore
bom_json.update(_json_core)
self._json_output = json.dumps(bom_json)
self.generated = True
return
elif self.generated:
return
else:
self.get_bom().validate()
bom_json = json.loads(self.get_bom().as_json(view_=_view)) # type: ignore
bom_json.update(_json_core)
self._json_output = json.dumps(bom_json)
self.generated = True
return

def output_as_string(self) -> str:
self.get_bom().validate()
bom_json: Dict[str, Any] = json_loads(
self.get_bom().as_json( # type:ignore[attr-defined]
view_=_view))
bom_json.update(_json_core)
self._bom_json = bom_json
self.generated = True

def output_as_string(self, *,
indent: Optional[Union[int, str]] = None) -> str:
self.generate()
return self._json_output
return json_dumps(self._bom_json,
indent=indent)

@abstractmethod
def _get_schema_uri(self) -> Optional[str]:
Expand Down
62 changes: 32 additions & 30 deletions cyclonedx/output/xml.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@
# Copyright (c) OWASP Foundation. All Rights Reserved.


from typing import Dict, Optional, Type
from xml.etree import ElementTree
from typing import Dict, Optional, Type, Union
from xml.dom.minidom import parseString as dom_parseString
from xml.etree.ElementTree import Element as XmlElement, tostring as xml_dumps

from ..exception.output import BomGenerationErrorException
from ..model.bom import Bom
from ..schema import OutputFormat, SchemaVersion
from ..schema.schema import (
Expand All @@ -37,11 +37,9 @@


class Xml(BaseSchemaVersion, BaseOutput):
XML_VERSION_DECLARATION: str = '<?xml version="1.0" encoding="UTF-8"?>'

def __init__(self, bom: Bom) -> None:
super().__init__(bom=bom)
self._root_bom_element: Optional[ElementTree.Element] = None
self._bom_xml: str = ''

@property
def schema_version(self) -> SchemaVersion:
Expand All @@ -52,40 +50,44 @@ def output_format(self) -> OutputFormat:
return OutputFormat.XML

def generate(self, force_regeneration: bool = False) -> None:
# New way
_view = SCHEMA_VERSIONS[self.schema_version_enum]
if self.generated and force_regeneration:
self.get_bom().validate()
self._root_bom_element = self.get_bom().as_xml( # type: ignore
view_=_view, as_string=False, xmlns=self.get_target_namespace()
)
self.generated = True
return
elif self.generated:
return
else:
self.get_bom().validate()
self._root_bom_element = self.get_bom().as_xml( # type: ignore
view_=_view, as_string=False, xmlns=self.get_target_namespace()
)
self.generated = True
if self.generated and not force_regeneration:
return

def output_as_string(self) -> str:
_view = SCHEMA_VERSIONS[self.schema_version_enum]
self.get_bom().validate()
xmlns = self.get_target_namespace()
self._bom_xml = xml_dumps(
self.get_bom().as_xml( # type:ignore[attr-defined]
_view, as_string=False, xmlns=xmlns),
method='xml',
encoding='unicode', xml_declaration=True,
default_namespace=xmlns)

self.generated = True

@staticmethod
def __make_indent(v: Optional[Union[int, str]]) -> str:
if isinstance(v, int):
return ' ' * v
if isinstance(v, str):
return v
return ''

def output_as_string(self, *, indent: Optional[Union[int, str]] = None) -> str:
self.generate()
if self.generated and self._root_bom_element is not None:
return str(Xml.XML_VERSION_DECLARATION + ElementTree.tostring(self._root_bom_element, encoding='unicode'))

raise BomGenerationErrorException('There was no Root XML Element after BOM generation.')
return self._bom_xml if indent is None else dom_parseString(self._bom_xml).toprettyxml(
indent=self.__make_indent(indent)
# do not set `encoding` - this would convert result to binary, not string
)

def get_target_namespace(self) -> str:
return f'http://cyclonedx.org/schema/bom/{self.get_schema_version()}'


class XmlV1Dot0(Xml, SchemaVersion1Dot0):

def _create_bom_element(self) -> ElementTree.Element:
return ElementTree.Element('bom', {'xmlns': self.get_target_namespace(), 'version': '1'})
def _create_bom_element(self) -> XmlElement:
return XmlElement('bom', {'xmlns': self.get_target_namespace(), 'version': '1'})


class XmlV1Dot1(Xml, SchemaVersion1Dot1):
Expand Down
6 changes: 4 additions & 2 deletions examples/complex.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
# endregion build the BOM


serialized_json = JsonV1Dot4(bom).output_as_string()
serialized_json = JsonV1Dot4(bom).output_as_string(indent=2)
print(serialized_json)
try:
validation_errors = JsonStrictValidator(SchemaVersion.V1_4).validate_str(serialized_json)
Expand All @@ -63,8 +63,10 @@
except MissingOptionalDependencyException as error:
print('JSON-validation was skipped due to', error)

print('', '=' * 30, '', sep='\n')

my_outputter = get_outputter(bom, OutputFormat.XML, SchemaVersion.V1_4)
serialized_xml = my_outputter.output_as_string()
serialized_xml = my_outputter.output_as_string(indent=2)
print(serialized_xml)
try:
validation_errors = get_validator(my_outputter.output_format,
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ keywords = [
# ATTENTION: keep `deps.lowest.r` file in sync
python = "^3.8"
packageurl-python = ">= 0.11"
py-serializable = "^0.11.1"
py-serializable = "^0.13.0"
sortedcontainers = "^2.4.0"
license-expression = "^30"
jsonschema = { version = "^4.18", extras=['format'], optional=true, python="^3.8" }
Expand Down

0 comments on commit 8e9400d

Please sign in to comment.