mirror of https://github.com/ARMmbed/mbed-os.git
NFC: Amend ndef parsing design.
* Decouple Message parsing from Record parsing * Decouple record parsing from Common objects * Provide an easy to use parser that parse the most common objects.pull/7426/head
parent
33fd8f0071
commit
7ed2a7802b
|
@ -261,72 +261,13 @@ The `NFCRemoteTarget` class derives from `NFCTarget` and additionally from `NFCE
|
|||
|
||||
## NDEF API
|
||||
|
||||
![ndef_diagram]
|
||||
|
||||
The NDEF API is constructed with these requirements in mind:
|
||||
* Minimizing memory allocation/copies
|
||||
* NFC Forum compliance
|
||||
* Ease of use
|
||||
* Extensibility
|
||||
|
||||
#### NDEF Message
|
||||
|
||||
A NDEF Message is made of multiple NDEF Records which is reflected by the API:
|
||||
|
||||
```cpp
|
||||
bool parse(const uint8_t* buffer, size_t sz)
|
||||
size_t count()
|
||||
NDEFRecord operator[](size_t n)
|
||||
```
|
||||
|
||||
The message can be mapped with a byte array and individual records are decoded/populated on the fly.
|
||||
|
||||
#### NDEF Message builder
|
||||
|
||||
We're using a builder pattern to encode an NDEF message over a byte array.
|
||||
|
||||
```cpp
|
||||
NDEFMessageBuilder(uint8_t* buffer, size_t max_sz)
|
||||
bool add_record(const NDEFRecord& record)
|
||||
NDEFMessage build()
|
||||
```
|
||||
|
||||
A reference to the array is provided in the constructor and records can be appended by the user (within memory limits).
|
||||
|
||||
Once done a NDEFMessage instance mapped to a subset of the byte array can be generated.
|
||||
|
||||
#### NDEF Record
|
||||
|
||||
The NDEF Record class is closely mapped with the NFC NDEF specification.
|
||||
|
||||
Each record holds:
|
||||
* A Type Name Format indicator - indicates which namespace the type field belongs to ('Well-known NDEF types', MIME, Absolute URI, etc.)
|
||||
* A type field
|
||||
* An optional ID field
|
||||
* The record's value
|
||||
|
||||
All arrays are passed by reference (no copy made).
|
||||
|
||||
```cpp
|
||||
static bool parse(const uint8_t* buffer, size_t max_sz)
|
||||
ssize_t build(const uint8_t* buffer, size_t max_sz)
|
||||
|
||||
uint8_t tnf()
|
||||
void set_tnf(uint8_t tnf)
|
||||
|
||||
const uint8_t* type() const
|
||||
size_t type_size() const
|
||||
void set_type(const uint8_t* type, size_t type_size)
|
||||
|
||||
const uint8_t* id() const
|
||||
size_t id_size() const
|
||||
void set_id(const uint8_t* id, size_t id_size)
|
||||
|
||||
const uint8_t* value() const
|
||||
size_t value_size() const
|
||||
void set_value(const uint8_t* type, size_t type_size)
|
||||
```
|
||||
|
||||
**Helpers**
|
||||
### Common objects
|
||||
|
||||
We will provide multiple helpers to make it easy to create/parse common record types:
|
||||
* URI
|
||||
|
@ -334,11 +275,8 @@ We will provide multiple helpers to make it easy to create/parse common record t
|
|||
* Smart Poster
|
||||
* MIME data
|
||||
|
||||
For instance, the `URIRecord`'s class API is as follows:
|
||||
For instance, the `URI`'s class API is as follows:
|
||||
```cpp
|
||||
static bool is_uri_record(const NDEFRecord& record)
|
||||
static URIRecord as_uri_record(const NDEFRecord& record)
|
||||
|
||||
uri_prefix_t uri_prefix() const
|
||||
void set_uri_prefix(uri_prefix_t prefix)
|
||||
|
||||
|
@ -351,9 +289,154 @@ size_t full_uri_size() const
|
|||
void set_full_uri(const char* uri)
|
||||
```
|
||||
|
||||
This includes some helper classes to check whether a record is an URI record, and if so to construct an `URIRecord` instance from a `NDEFRecord`.
|
||||
**Note:** These types can be replaced by user defined ones if parsing and serialization logic is provided.
|
||||
|
||||
In this case buffers are copied to account for the NULL-terminator character that is not present in the underlying byte buffer.
|
||||
### Parsing
|
||||
|
||||
#### ndef::MessageParser
|
||||
|
||||
![ndef_message_parser_diagram]
|
||||
|
||||
Messages incoming from the peer are parsed by a `MessageParser` which produce
|
||||
`Record` instances to its client. The parsing operation is event-driven: a
|
||||
message parser client register a delegate inside the message parser. This delegate
|
||||
get notified whenever an interesting event happen during the parsing.
|
||||
|
||||
```cpp
|
||||
void set_delegate(Delegate* delegate);
|
||||
void parse(const ac_buffer_t& data_buffer);
|
||||
```
|
||||
|
||||
It is important to note that the data_buffer in entry of the parse function must
|
||||
contain the entire NDEF message.
|
||||
|
||||
##### ndef::MessageParser::Delegate
|
||||
|
||||
```cpp
|
||||
virtual void on_parsing_started() { }
|
||||
virtual void on_record_parsed(const Record& record) { }
|
||||
virtual void on_parsing_terminated() { }
|
||||
virtual void on_parsing_error(error_t error) { }
|
||||
```
|
||||
|
||||
The delegate is notified by the parser when the parsing start or end; when an error
|
||||
is encountered or when an ndef `Record` has been parsed.
|
||||
|
||||
To reduce memory consumption `Record` instances generated by the parser are short
|
||||
lived. They are only valid during the callback invocation. If a client is interested
|
||||
by the content of a message parsed and wants to use it after the parsing callback
|
||||
then it must make a copy of the record object.
|
||||
|
||||
#### NDEF Record parsing
|
||||
|
||||
![ndef_record_parser_diagram]
|
||||
|
||||
NDEF records can contain any type of content. Therefore parsing of records is
|
||||
specific to the application. To help the developer; an optional ndef record
|
||||
parsing framework is included. It follows the _chain-of-responsibility_ design
|
||||
pattern that facilitate the integration of record parsers defined by client code.
|
||||
|
||||
##### ndef::RecordParser
|
||||
|
||||
Is is the base building block of the record parsing frame working. It parses a
|
||||
record then return true if the record has been parsed or false otherwise.
|
||||
|
||||
```cpp
|
||||
virtual bool parse(const Record&);
|
||||
```
|
||||
|
||||
##### ndef::RecordParserChain
|
||||
|
||||
It aggregate `RecordParser` instances and defer parsing to the instances it contains.
|
||||
|
||||
```cpp
|
||||
bool parse(const Record& record);
|
||||
void set_next_parser(RecordParser* parser);
|
||||
```
|
||||
|
||||
##### ndef::GenericRecordParser<ParserImplementation, ParsingResult>
|
||||
|
||||
This is a partial implementation of the `RecordParser` interface. It exposes a
|
||||
delegate type that can be implemented and registered by clients of this parser.
|
||||
This delegate expects objects of the parsing result type.
|
||||
|
||||
```cpp
|
||||
bool parse(const Record&)
|
||||
void set_delegate(Delegate* delegate)
|
||||
```
|
||||
|
||||
Implementation of this class must expose the following non virtual function:
|
||||
|
||||
```c++
|
||||
bool do_parse(const Record& record, ParsingResult& parsing_result);
|
||||
```
|
||||
|
||||
If the parsing is successful then it should return true and fill `parsing_result`
|
||||
otherwise it should return false and leave `parsing_result` untouched.
|
||||
|
||||
**Note:** The Curiously recurring template pattern (CRTP) is used to implement
|
||||
the delegation mechanism in a type-safe fashion. This is not achievable with
|
||||
_regular_ polymorphism.
|
||||
|
||||
###### ndef::GenericRecordParser<ParserImplementation, ParsingResult>::Delegate
|
||||
|
||||
This delegate must be implemented by clients of this class. It receives the objects
|
||||
parsed.
|
||||
|
||||
```cpp
|
||||
virtual void on_record_parsed(const ParsingResult& record, const RecordID* id);
|
||||
```
|
||||
|
||||
**Note:** Usually clients are client of an implementation of an
|
||||
ndef::GenericRecordParser<ParserImplementation, ParsingResult> . They can refer
|
||||
to the delegate as `ImplementationName::Delegate`.
|
||||
|
||||
#### Common parsers
|
||||
|
||||
![ndef_common_parsers_diagram]
|
||||
|
||||
Parsers for each common record type exists. They inherit from the
|
||||
`GenericRecordParser` to exposes a common delegate interface:
|
||||
|
||||
```cpp
|
||||
virtual void on_record_parsed(const <ParsedType>& result, const ndef::RecordID* id)
|
||||
```
|
||||
|
||||
#### Simple parser
|
||||
|
||||
The API provide a class named `SimpleMessageParser` that glues together a
|
||||
`MessageParser` and a chain `RecordParser`'s containing the parsers for the common
|
||||
types.
|
||||
|
||||
![ndef_simple_parser_diagram]
|
||||
|
||||
Clients of the class can register a delegate, parse a message or add a new
|
||||
`RecordParser` in the parsing chain.
|
||||
|
||||
```cpp
|
||||
void set_delegate(Delegate* delegate);
|
||||
void parse(const ac_buffer_t& data_buffer);
|
||||
void add_record_parser(ndef::RecordParser* parser);
|
||||
```
|
||||
|
||||
##### Delegate
|
||||
|
||||
This delegate must be implemented by clients of this class. It receives events
|
||||
from the parsing process:
|
||||
|
||||
```cpp
|
||||
virtual void on_parsing_error(ndef::MessageParser::error_t error);
|
||||
virtual void on_parsing_started();
|
||||
virtual void on_text_parsed(const Text& text, const ndef::RecordID* id);
|
||||
virtual void on_mime_parsed(const Mime& text, const ndef::RecordID* id);
|
||||
virtual void on_uri_parsed(const URI& uri, const ndef::RecordID* id);
|
||||
virtual void on_unknown_record_parsed(const ndef::Record& record);
|
||||
virtual void on_parsing_terminated();
|
||||
```
|
||||
|
||||
### Serialization
|
||||
|
||||
**TBD**
|
||||
|
||||
## HAL APIs
|
||||
|
||||
|
@ -422,3 +505,7 @@ There are currently at least four event queues (Plaftorm, BLE, USB, IP) in mbed
|
|||
[nfc_endpoints_diagram]: uml_diagram_endpoints.png
|
||||
[ndef_diagram]: uml_diagram_ndef.png
|
||||
[interop_test_rig]: interop_test_rig.png
|
||||
[ndef_message_parser_diagram]: uml_diagram_ndef_message_parser.png
|
||||
[ndef_record_parser_diagram]: uml_diagram_ndef_record_parser.png
|
||||
[ndef_common_parsers_diagram]: uml_diagram_ndef_common_parsers.png
|
||||
[ndef_simple_parser_diagram]: uml_diagram_ndef_simple_parser.png
|
Binary file not shown.
Before Width: | Height: | Size: 52 KiB |
|
@ -1,86 +0,0 @@
|
|||
@startuml
|
||||
|
||||
class NDEFMessage {
|
||||
+NDEFMessage()
|
||||
+bool parse(const uint8_t* buffer, size_t sz)
|
||||
+size_t count()
|
||||
+NDEFRecord operator[](size_t n)
|
||||
}
|
||||
|
||||
class NDEFMessageBuilder {
|
||||
+NDEFMessageBuilder(uint8_t* buffer, size_t max_sz)
|
||||
+bool add_record(const NDEFRecord& record)
|
||||
+NDEFMessage build()
|
||||
}
|
||||
|
||||
class NDEFRecord {
|
||||
+bool parse(const uint8_t* buffer, size_t max_sz)
|
||||
+ssize_t build(const uint8_t* buffer, size_t max_sz)
|
||||
|
||||
+uint8_t tnf()
|
||||
+void set_tnf(uint8_t tnf)
|
||||
|
||||
+const uint8_t* type() const
|
||||
+size_t type_size() const
|
||||
+void set_type(const uint8_t* type, size_t type_size)
|
||||
|
||||
+const uint8_t* id() const
|
||||
+size_t id_size() const
|
||||
+void set_id(const uint8_t* id, size_t id_size)
|
||||
|
||||
+const uint8_t* value() const
|
||||
+size_t value_size() const
|
||||
+void set_value(const uint8_t* type, size_t type_size)
|
||||
}
|
||||
|
||||
NDEFMessage *-- NDEFRecord
|
||||
NDEFMessageBuilder *-- NDEFRecord
|
||||
|
||||
class URIRecord {
|
||||
{static} +bool is_uri_record(const NDEFRecord& record)
|
||||
{static} +URIRecord as_uri_record(const NDEFRecord& record)
|
||||
|
||||
+uri_prefix_t uri_prefix() const
|
||||
+void set_uri_prefix(uri_prefix_t prefix)
|
||||
|
||||
+bool get_uri(char* uri, size_t max_sz) const
|
||||
+size_t uri_size() const
|
||||
+void set_uri(const char* uri)
|
||||
|
||||
+bool get_full_uri(char* uri, size_t max_sz) const
|
||||
+size_t full_uri_size() const
|
||||
+void set_full_uri(const char* uri)
|
||||
}
|
||||
|
||||
NDEFRecord <-- URIRecord
|
||||
|
||||
class TextRecord {
|
||||
{static} +bool is_text_record(const NDEFRecord& record)
|
||||
{static} +TextRecord as_text_record(const NDEFRecord& record)
|
||||
|
||||
+text_encoding_t encoding() const
|
||||
+void set_encoding(text_encoding_t encoding)
|
||||
|
||||
+bool get_text(char* text, size_t max_sz) const
|
||||
+size_t text_size() const
|
||||
+void set_text(const char* text)
|
||||
}
|
||||
|
||||
NDEFRecord <-- TextRecord
|
||||
|
||||
class MIMERecord {
|
||||
{static} +bool is_mime_record(const NDEFRecord& record)
|
||||
{static} +MIMERecord as_mime_record(const NDEFRecord& record)
|
||||
|
||||
+const char* mime_type() const
|
||||
+size_t mime_type_size() const
|
||||
+void set_mime_type(const char* text)
|
||||
|
||||
+const uint8_t* mime_data() const
|
||||
+size_t mime_data_size() const
|
||||
+void set_mime_data(const uint8_t* type, size_t type_size)
|
||||
}
|
||||
|
||||
NDEFRecord <-- MIMERecord
|
||||
|
||||
@enduml
|
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
|
@ -0,0 +1,58 @@
|
|||
@startuml
|
||||
|
||||
package ndef {
|
||||
|
||||
abstract RecordParser {
|
||||
+RecordParser()
|
||||
+{abstract} bool parse(const Record&)
|
||||
#~RecordParser()
|
||||
}
|
||||
|
||||
abstract GenericRecordParser<ParserImplementation, ParsingResult> {
|
||||
+GenericRecordParser()
|
||||
+bool parse(const Record&)
|
||||
+void set_delegate(Delegate* delegate)
|
||||
#~GenericRecordParser()
|
||||
}
|
||||
|
||||
interface GenericRecordParser::Delegate<ParsingResult> {
|
||||
+{abstract} void on_record_parsed(const ParsingResult& result, const RecordID* id)
|
||||
#~Delegate()
|
||||
}
|
||||
|
||||
RecordParser <|- GenericRecordParser
|
||||
GenericRecordParser +- "0..1" GenericRecordParser::Delegate
|
||||
|
||||
}
|
||||
|
||||
package common {
|
||||
class URI {
|
||||
}
|
||||
class Mime {
|
||||
}
|
||||
class Text {
|
||||
}
|
||||
|
||||
class URIParser {
|
||||
bool do_parse(const ndef::Record& record, URI& uri)
|
||||
}
|
||||
|
||||
class TextParser {
|
||||
bool do_parse(const ndef::Record& record, Text& text)
|
||||
}
|
||||
|
||||
class MimeParser {
|
||||
bool do_parse(const ndef::Record& record, Mime& mime)
|
||||
}
|
||||
|
||||
URI -- URIParser: Produce <
|
||||
Text -- TextParser: Produce <
|
||||
Mime -- MimeParser: Produce <
|
||||
|
||||
URIParser --|> GenericRecordParser
|
||||
TextParser --|> GenericRecordParser
|
||||
MimeParser --|> GenericRecordParser
|
||||
}
|
||||
|
||||
|
||||
@enduml
|
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
|
@ -0,0 +1,46 @@
|
|||
@startuml
|
||||
|
||||
package ndef {
|
||||
|
||||
class MessageParser {
|
||||
+MessageParser()
|
||||
+void set_delegate(Delegate* delegate)
|
||||
+void parse(const ac_buffer_t& data_buffer)
|
||||
}
|
||||
|
||||
interface MessageParser::Delegate {
|
||||
+{abstract} void on_parsing_started()
|
||||
+{abstract} void on_record_parsed(const Record& record)
|
||||
+{abstract} void on_parsing_terminated()
|
||||
+{abstract} void on_parsing_error(error_t error)
|
||||
# ~Delegate()
|
||||
}
|
||||
|
||||
enum MessageParser::error_t {
|
||||
}
|
||||
|
||||
MessageParser +-- "0..1" MessageParser::Delegate
|
||||
MessageParser +-- MessageParser::error_t
|
||||
|
||||
note top of "MessageParser::Delegate"
|
||||
Implemented by the client of the parsing operation.
|
||||
end note
|
||||
|
||||
class Record {
|
||||
}
|
||||
class RecordType {
|
||||
}
|
||||
class RecordPayload {
|
||||
}
|
||||
class RecordID {
|
||||
}
|
||||
|
||||
Record *-- RecordType
|
||||
Record *-- "0..1" RecordPayload
|
||||
Record *-- "0..1" RecordID
|
||||
|
||||
MessageParser - Record: Produce >
|
||||
|
||||
}
|
||||
|
||||
@enduml
|
Binary file not shown.
After Width: | Height: | Size: 58 KiB |
|
@ -0,0 +1,73 @@
|
|||
@startuml
|
||||
|
||||
package ndef {
|
||||
|
||||
class Record {
|
||||
}
|
||||
class RecordType {
|
||||
}
|
||||
class RecordPayload {
|
||||
}
|
||||
class RecordID {
|
||||
}
|
||||
|
||||
Record *-- RecordType
|
||||
Record *-- "0..1" RecordPayload
|
||||
Record *-- "0..1" RecordID
|
||||
|
||||
abstract RecordParser {
|
||||
+RecordParser()
|
||||
+{abstract} bool parse(const Record&)
|
||||
#~RecordParser()
|
||||
}
|
||||
|
||||
abstract GenericRecordParser<ParserImplementation, ParsingResult> {
|
||||
+GenericRecordParser()
|
||||
+bool parse(const Record&)
|
||||
+void set_delegate(Delegate* delegate)
|
||||
#~GenericRecordParser()
|
||||
}
|
||||
|
||||
interface GenericRecordParserConcept<ParsingResult> {
|
||||
+bool do_parse(const Record& record, ParsingResult& parsing_result)
|
||||
}
|
||||
|
||||
interface GenericRecordParser::Delegate<ParsingResult> {
|
||||
+{abstract} void on_record_parsed(const ParsingResult& record, const RecordID* id)
|
||||
#~Delegate()
|
||||
}
|
||||
|
||||
RecordParser <|-- GenericRecordParser
|
||||
GenericRecordParser <|-- GenericRecordParserConcept
|
||||
GenericRecordParser +-- "0..1" GenericRecordParser::Delegate
|
||||
|
||||
note as N1
|
||||
GenericRecordParserConcept model the concept that must
|
||||
be implemented by GenericRecordParser childs.
|
||||
It doesn't exist in the hierarchy.
|
||||
end note
|
||||
|
||||
N1 - GenericRecordParser
|
||||
N1 - GenericRecordParserConcept
|
||||
|
||||
note bottom of "GenericRecordParser::Delegate"
|
||||
Implemented by the client of the parsing operation.
|
||||
end note
|
||||
|
||||
class RecordParserChain {
|
||||
+RecordParserChain()
|
||||
+~RecordParserChain()
|
||||
+bool parse(const Record& record)
|
||||
+void set_next_parser(RecordParser* parser)
|
||||
}
|
||||
|
||||
note bottom of "RecordParserChain"
|
||||
Chain of responsibility pattern.
|
||||
end note
|
||||
|
||||
Record - RecordParserChain: Parse >
|
||||
RecordParserChain o- "*" RecordParser
|
||||
|
||||
}
|
||||
|
||||
@enduml
|
Binary file not shown.
After Width: | Height: | Size: 94 KiB |
|
@ -0,0 +1,105 @@
|
|||
@startuml
|
||||
|
||||
package ndef {
|
||||
|
||||
class RecordParserChain {
|
||||
|
||||
}
|
||||
|
||||
abstract RecordParser {
|
||||
|
||||
}
|
||||
|
||||
abstract GenericRecordParser<ParserImplementation, ParsingResult> {
|
||||
|
||||
}
|
||||
|
||||
interface GenericRecordParser::Delegate<ParsingResult> {
|
||||
+{abstract} void on_record_parsed(const ParsingResult& record, const RecordID* id)
|
||||
#~Delegate()
|
||||
}
|
||||
|
||||
class MessageParser {
|
||||
|
||||
}
|
||||
|
||||
interface MessageParser::Delegate {
|
||||
+{abstract} void on_parsing_started()
|
||||
+{abstract} void on_record_parsed(const Record& record)
|
||||
+{abstract} void on_parsing_terminated()
|
||||
+{abstract} void on_parsing_error(error_t error)
|
||||
# ~Delegate()
|
||||
}
|
||||
|
||||
|
||||
MessageParser +-- "0..1" MessageParser::Delegate
|
||||
|
||||
|
||||
RecordParserChain -o RecordParser
|
||||
GenericRecordParser --|> RecordParser
|
||||
GenericRecordParser +- "0..1" GenericRecordParser::Delegate
|
||||
|
||||
RecordParserChain -[hidden]- MessageParser
|
||||
|
||||
}
|
||||
|
||||
package common {
|
||||
|
||||
class SimpleMessageParser {
|
||||
+ SimpleMessageParser()
|
||||
+ ~SimpleMessageParser()
|
||||
+ void set_delegate(Delegate* delegate)
|
||||
+ void parse(const ac_buffer_t& data_buffer)
|
||||
+ void add_record_parser(ndef::RecordParser* parser)
|
||||
- void on_parsing_error(ndef::MessageParser::error_t error)
|
||||
- void on_parsing_started()
|
||||
- void on_record_parsed(const ndef::Record& record)
|
||||
- void on_parsing_terminated()
|
||||
- void on_record_parsed(const URI& uri, const ndef::RecordID* id)
|
||||
- void on_record_parsed(const Text& uri, const ndef::RecordID* id)
|
||||
- void on_record_parsed(const Mime& uri, const ndef::RecordID* id)
|
||||
}
|
||||
|
||||
interface SimpleMessageParser::Delegate {
|
||||
+ {abstract} void on_parsing_error(ndef::MessageParser::error_t error)
|
||||
+ {abstract} void on_parsing_started()
|
||||
+ {abstract} void on_text_parsed(const Text& text, const ndef::RecordID* id)
|
||||
+ {abstract} void on_mime_parsed(const Mime& text, const ndef::RecordID* id)
|
||||
+ {abstract} void on_uri_parsed(const URI& uri, const ndef::RecordID* id)
|
||||
+ {abstract} void on_unknown_record_parsed(const ndef::Record& record)
|
||||
+ {abstract} void on_parsing_terminated()
|
||||
# ~Delegate()
|
||||
}
|
||||
|
||||
SimpleMessageParser::Delegate +- SimpleMessageParser
|
||||
|
||||
class URIParser {
|
||||
|
||||
}
|
||||
|
||||
class TextParser {
|
||||
|
||||
}
|
||||
|
||||
class MimeParser {
|
||||
|
||||
}
|
||||
|
||||
URIParser --|> GenericRecordParser
|
||||
TextParser --|> GenericRecordParser
|
||||
MimeParser --|> GenericRecordParser
|
||||
|
||||
SimpleMessageParser o-- URIParser
|
||||
SimpleMessageParser o-- TextParser
|
||||
SimpleMessageParser o-- MimeParser
|
||||
SimpleMessageParser o-- RecordParserChain
|
||||
SimpleMessageParser o-- MessageParser
|
||||
SimpleMessageParser <|-- MessageParser::Delegate
|
||||
|
||||
SimpleMessageParser <|-- GenericRecordParser::Delegate
|
||||
SimpleMessageParser <|-- GenericRecordParser::Delegate
|
||||
SimpleMessageParser <|-- GenericRecordParser::Delegate
|
||||
|
||||
}
|
||||
|
||||
@enduml
|
Loading…
Reference in New Issue