Skip to content

Commit e335e06

Browse files
authored
Add Java generation target (#460)
We introduce the generator for the Java SDK.
1 parent 5c264e1 commit e335e06

File tree

259 files changed

+101339
-2
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

259 files changed

+101339
-2
lines changed

README.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ Call the generator with the appropriate target:
139139
140140
usage: aas-core-codegen [-h] --model_path MODEL_PATH --snippets_dir
141141
SNIPPETS_DIR --output_dir OUTPUT_DIR --target
142-
{csharp,cpp,golang,jsonschema,python,typescript,rdf_shacl,xsd,jsonld_context}
142+
{csharp,cpp,golang,java,jsonschema,python,typescript,rdf_shacl,xsd,jsonld_context}
143143
[--version]
144144
145145
Generate implementations and schemas based on an AAS meta-model.
@@ -153,7 +153,7 @@ Call the generator with the appropriate target:
153153
specific code snippets
154154
--output_dir OUTPUT_DIR
155155
path to the generated code
156-
--target {csharp,cpp,golang,jsonschema,python,typescript,rdf_shacl,xsd,jsonld_context}
156+
--target {csharp,cpp,golang,java,jsonschema,python,typescript,rdf_shacl,xsd,jsonld_context}
157157
target language or schema
158158
--version show the current version and exit
159159

aas_core_codegen/java/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Generate Java code based on the intermediate meta-model."""

aas_core_codegen/java/common.py

+197
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
"""Provide common functions shared among different Java code generation modules."""
2+
import re
3+
from typing import List, cast, Optional
4+
5+
from icontract import ensure, require
6+
7+
from aas_core_codegen import intermediate
8+
from aas_core_codegen.common import Stripped, assert_never
9+
from aas_core_codegen.java import naming as java_naming
10+
11+
12+
@ensure(lambda result: result.startswith('"'))
13+
@ensure(lambda result: result.endswith('"'))
14+
def string_literal(text: str) -> Stripped:
15+
"""Generate a Java string literal from the ``text``."""
16+
escaped = [] # type: List[str]
17+
18+
for character in text:
19+
if character == "\t":
20+
escaped.append("\\t")
21+
elif character == "\b":
22+
escaped.append("\\b")
23+
elif character == "\n":
24+
escaped.append("\\n")
25+
elif character == "\r":
26+
escaped.append("\\r")
27+
elif character == "\f":
28+
escaped.append("\\f")
29+
elif character == "'":
30+
escaped.append("\\'")
31+
elif character == '"':
32+
escaped.append('\\"')
33+
elif character == "\\":
34+
escaped.append("\\\\")
35+
else:
36+
escaped.append(character)
37+
38+
return Stripped('"{}"'.format("".join(escaped)))
39+
40+
41+
def needs_escaping(text: str) -> bool:
42+
"""Check whether the ``text`` contains a character that needs escaping."""
43+
for character in text:
44+
if character == "\t":
45+
return True
46+
elif character == "\b":
47+
return True
48+
elif character == "\n":
49+
return True
50+
elif character == "\r":
51+
return True
52+
elif character == "\f":
53+
return True
54+
elif character == "'":
55+
return True
56+
elif character == '"':
57+
return True
58+
elif character == "\\":
59+
return True
60+
else:
61+
pass
62+
63+
return False
64+
65+
66+
PRIMITIVE_TYPE_MAP = {
67+
intermediate.PrimitiveType.BOOL: Stripped("Boolean"),
68+
intermediate.PrimitiveType.INT: Stripped("Long"),
69+
intermediate.PrimitiveType.FLOAT: Stripped("Float"),
70+
intermediate.PrimitiveType.STR: Stripped("String"),
71+
intermediate.PrimitiveType.BYTEARRAY: Stripped("byte[]"),
72+
}
73+
74+
75+
# fmt: off
76+
@require(
77+
lambda our_type_qualifier:
78+
not (our_type_qualifier is not None)
79+
or not our_type_qualifier.endswith('.')
80+
)
81+
# fmt: on
82+
def generate_type(
83+
type_annotation: intermediate.TypeAnnotationUnion,
84+
our_type_qualifier: Optional[Stripped] = None,
85+
) -> Stripped:
86+
"""
87+
Generate the Java type for the given type annotation.
88+
89+
``our_type_prefix`` is appended to all our types, if specified.
90+
"""
91+
our_type_prefix = "" if our_type_qualifier is None else f"{our_type_qualifier}."
92+
# BEFORE-RELEASE (empwilli, 2023-12-14): test in isolation
93+
if isinstance(type_annotation, intermediate.PrimitiveTypeAnnotation):
94+
return PRIMITIVE_TYPE_MAP[type_annotation.a_type]
95+
96+
elif isinstance(type_annotation, intermediate.OurTypeAnnotation):
97+
our_type = type_annotation.our_type
98+
99+
if isinstance(our_type, intermediate.Enumeration):
100+
return Stripped(
101+
our_type_prefix + java_naming.enum_name(type_annotation.our_type.name)
102+
)
103+
104+
elif isinstance(our_type, intermediate.ConstrainedPrimitive):
105+
return PRIMITIVE_TYPE_MAP[our_type.constrainee]
106+
107+
elif isinstance(our_type, intermediate.Class):
108+
# NOTE (empwilli, 2023-12-14):
109+
# We want to allow custom enhancements and wrappings around
110+
# our model classes. Therefore, we always operate over Java interfaces
111+
# instead of concrete classes, even if the class is a concrete one and
112+
# has no concrete descendants.
113+
114+
return Stripped(our_type_prefix + java_naming.interface_name(our_type.name))
115+
116+
elif isinstance(type_annotation, intermediate.ListTypeAnnotation):
117+
item_type = generate_type(
118+
type_annotation=type_annotation.items, our_type_qualifier=our_type_qualifier
119+
)
120+
121+
return Stripped(f"List<{item_type}>")
122+
123+
elif isinstance(type_annotation, intermediate.OptionalTypeAnnotation):
124+
value = generate_type(
125+
type_annotation=type_annotation.value, our_type_qualifier=our_type_qualifier
126+
)
127+
return Stripped(f"Optional<{value}>")
128+
129+
else:
130+
assert_never(type_annotation)
131+
132+
raise AssertionError("Should not have gotten here")
133+
134+
135+
INDENT = " "
136+
INDENT2 = INDENT * 2
137+
INDENT3 = INDENT * 3
138+
INDENT4 = INDENT * 4
139+
INDENT5 = INDENT * 5
140+
INDENT6 = INDENT * 6
141+
142+
143+
INTERFACE_PKG = "model"
144+
CLASS_PKG = "impl"
145+
ENUM_PKG = "enums"
146+
147+
148+
def interface_package_path(name: Stripped) -> Stripped:
149+
"""Create the package path for an interface file."""
150+
return Stripped(f"{INTERFACE_PKG}/{name}.java")
151+
152+
153+
def class_package_path(name: Stripped) -> Stripped:
154+
"""Create the package path for an interface file."""
155+
return Stripped(f"{CLASS_PKG}/{name}.java")
156+
157+
158+
def enum_package_path(name: Stripped) -> Stripped:
159+
"""Create the package path for an interface file."""
160+
return Stripped(f"{ENUM_PKG}/{name}.java")
161+
162+
163+
# noinspection RegExpSimplifiable
164+
PACKAGE_IDENTIFIER_RE = re.compile(r"[a-z][a-z_0-9]*(\.[a-z][a-z_0-9]*)*")
165+
166+
167+
class PackageIdentifier(str):
168+
"""Capture a package identifier."""
169+
170+
@require(lambda identifier: PACKAGE_IDENTIFIER_RE.fullmatch(identifier))
171+
def __new__(cls, identifier: str) -> "PackageIdentifier":
172+
return cast(PackageIdentifier, identifier)
173+
174+
175+
WARNING = Stripped(
176+
"""\
177+
/*
178+
* This code has been automatically generated by aas-core-codegen.
179+
* Do NOT edit or append.
180+
*/"""
181+
)
182+
183+
184+
class JavaFile:
185+
"""Representation of a Java source file."""
186+
187+
# fmt: off
188+
@require(lambda name, content: (len(name) > 0) and (len(content) > 0))
189+
@require(lambda content: content.endswith('\n'), "Trailing newline mandatory for valid end-of-files")
190+
# fmt: on
191+
def __init__(
192+
self,
193+
name: str,
194+
content: str,
195+
):
196+
self.name = name
197+
self.content = content
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Generate the Java constants corresponding to the constants of the meta-model."""
2+
3+
from aas_core_codegen.java.constants import _generate
4+
5+
generate = _generate.generate

0 commit comments

Comments
 (0)