|
12 | 12 | */ |
13 | 13 | package org.eclipse.uprotocol.uri.serializer; |
14 | 14 |
|
15 | | -import org.eclipse.uprotocol.uri.factory.UriFactory; |
| 15 | +import java.net.URI; |
| 16 | +import java.util.Objects; |
| 17 | +import java.util.Optional; |
| 18 | + |
16 | 19 | import org.eclipse.uprotocol.uri.validator.UriValidator; |
17 | 20 | import org.eclipse.uprotocol.v1.UUri; |
18 | 21 |
|
19 | 22 | /** |
20 | | - * UUri Serializer that serializes a UUri to a long format string per |
21 | | - * https://github.com/eclipse-uprotocol/uprotocol-spec/blob/main/basics/uri.adoc. |
| 23 | + * Provides functionality for serializing and deserializing {@link UUri}s to/from their |
| 24 | + * corresponding URI representation as defined by the uProtocol specification. |
| 25 | + * |
| 26 | + * @see <a href="https://github.com/eclipse-uprotocol/uprotocol-spec/blob/v1.6.0-alpha.4/basics/uri.adoc"> |
| 27 | + * uProtocol URI Specification</a> |
22 | 28 | */ |
23 | 29 | public interface UriSerializer { |
24 | 30 |
|
25 | | - |
| 31 | + String SCHEME_UP = "up"; |
26 | 32 |
|
27 | 33 | /** |
28 | | - * Support for serializing {@link UUri} objects into their String format. |
| 34 | + * Serializes a {@link UUri} into its URI representation. |
29 | 35 | * |
30 | | - * @param uri {@link UUri} object to be serialized to the String format. |
31 | | - * @return Returns the String format of the supplied {@link UUri} that can be |
32 | | - * used as a sink or a source in a uProtocol publish communication. |
| 36 | + * @param uuri The UUri to be serialized. |
| 37 | + * @return The URI. |
| 38 | + * @throws NullPointerException if the UUri is null. |
| 39 | + * @throws IllegalArgumentException if the UUri does not comply with the UUri specification. |
33 | 40 | */ |
34 | | - static String serialize(UUri uri) { |
35 | | - if (uri == null || UriValidator.isEmpty(uri)) { |
36 | | - return ""; |
37 | | - } |
38 | | - |
| 41 | + // [impl->dsn~uri-authority-mapping~1] |
| 42 | + // [impl->dsn~uri-path-mapping~1] |
| 43 | + // [impl->req~uri-serialization~1] |
| 44 | + static String serialize(UUri uuri) { |
| 45 | + Objects.requireNonNull(uuri); |
| 46 | + UriValidator.validate(uuri); |
39 | 47 | StringBuilder sb = new StringBuilder(); |
40 | 48 |
|
41 | | - if (!uri.getAuthorityName().isBlank()) { |
| 49 | + if (!uuri.getAuthorityName().isBlank()) { |
42 | 50 | sb.append("//"); |
43 | | - sb.append(uri.getAuthorityName()); |
| 51 | + sb.append(uuri.getAuthorityName()); |
44 | 52 | } |
45 | 53 |
|
46 | 54 | sb.append("/"); |
47 | 55 | final var pathSegments = String.format("%X/%X/%X", |
48 | | - uri.getUeId(), |
49 | | - uri.getUeVersionMajor(), |
50 | | - uri.getResourceId()); |
| 56 | + uuri.getUeId(), |
| 57 | + uuri.getUeVersionMajor(), |
| 58 | + uuri.getResourceId()); |
51 | 59 | sb.append(pathSegments); |
52 | 60 | return sb.toString(); |
53 | 61 | } |
54 | 62 |
|
55 | 63 | /** |
56 | | - * Deserialize a String into a UUri object. |
| 64 | + * Deserializes a URI into a UUri. |
57 | 65 | * |
58 | | - * @param uProtocolUri A long format uProtocol URI. |
59 | | - * @return Returns an UUri data object. |
| 66 | + * @param uProtocolUri The URI to deserialize. |
| 67 | + * @return The UUri. |
| 68 | + * @throws NullPointerException if the URI is null. |
| 69 | + * @throws IllegalArgumentException if the URI is invalid. |
60 | 70 | */ |
| 71 | + // [impl->dsn~uri-authority-name-length~1] |
| 72 | + // [impl->dsn~uri-scheme~1] |
| 73 | + // [impl->dsn~uri-host-only~2] |
| 74 | + // [impl->dsn~uri-authority-mapping~1] |
| 75 | + // [impl->dsn~uri-path-mapping~1] |
| 76 | + // [impl->req~uri-serialization~1] |
61 | 77 | static UUri deserialize(String uProtocolUri) { |
62 | | - if (uProtocolUri == null) { |
63 | | - return UUri.getDefaultInstance(); |
64 | | - } |
| 78 | + Objects.requireNonNull(uProtocolUri); |
| 79 | + final var parsedUri = URI.create(uProtocolUri); |
| 80 | + return deserialize(parsedUri); |
| 81 | + } |
65 | 82 |
|
66 | | - String uri = uProtocolUri.contains(":") ? uProtocolUri.substring(uProtocolUri.indexOf(":") + 1) |
67 | | - : uProtocolUri |
68 | | - .replace('\\', '/'); |
| 83 | + /** |
| 84 | + * Deserializes a URI into a UUri. |
| 85 | + * |
| 86 | + * @param uProtocolUri The URI to deserialize. |
| 87 | + * @return The UUri. |
| 88 | + * @throws NullPointerException if the URI is null. |
| 89 | + * @throws IllegalArgumentException if the URI is invalid. |
| 90 | + */ |
| 91 | + // [impl->dsn~uri-authority-name-length~1] |
| 92 | + // [impl->dsn~uri-scheme~1] |
| 93 | + // [impl->dsn~uri-host-only~2] |
| 94 | + // [impl->dsn~uri-authority-mapping~1] |
| 95 | + // [impl->dsn~uri-path-mapping~1] |
| 96 | + // [impl->req~uri-serialization~1] |
| 97 | + static UUri deserialize(URI uProtocolUri) { |
| 98 | + Objects.requireNonNull(uProtocolUri); |
| 99 | + |
| 100 | + if (uProtocolUri.getScheme() != null && !SCHEME_UP.equals(uProtocolUri.getScheme())) { |
| 101 | + throw new IllegalArgumentException("uProtocol URI must use '%s' scheme".formatted(SCHEME_UP)); |
| 102 | + } |
| 103 | + if (uProtocolUri.getQuery() != null) { |
| 104 | + throw new IllegalArgumentException("uProtocol URI must not contain query"); |
| 105 | + } |
| 106 | + if (uProtocolUri.getFragment() != null) { |
| 107 | + throw new IllegalArgumentException("uProtocol URI must not contain fragment"); |
| 108 | + } |
| 109 | + UriValidator.validateParsedAuthority(uProtocolUri); |
69 | 110 |
|
70 | | - boolean isLocal = !uri.startsWith("//"); |
| 111 | + final var pathSegments = uProtocolUri.getPath().split("/"); |
| 112 | + if (pathSegments.length != 4) { |
| 113 | + throw new IllegalArgumentException("uProtocol URI must have exactly 3 path segments"); |
| 114 | + } |
71 | 115 |
|
72 | | - final String[] uriParts = uri.split("/"); |
73 | | - final int numberOfPartsInUri = uriParts.length; |
| 116 | + final var builder = UUri.newBuilder(); |
| 117 | + Optional.ofNullable(uProtocolUri.getAuthority()).ifPresent(builder::setAuthorityName); |
74 | 118 |
|
75 | | - if (numberOfPartsInUri == 0 || numberOfPartsInUri == 1) { |
76 | | - return UUri.getDefaultInstance(); |
| 119 | + if (pathSegments[1].isEmpty()) { |
| 120 | + throw new IllegalArgumentException("URI must contain non-empty entity ID"); |
77 | 121 | } |
78 | | - |
79 | | - UUri.Builder builder = UUri.newBuilder(); |
80 | 122 | try { |
81 | | - if (isLocal) { |
82 | | - builder.setUeId(Integer.parseUnsignedInt(uriParts[1], 16)); |
83 | | - if (numberOfPartsInUri > 2) { |
84 | | - builder.setUeVersionMajor(Integer.parseUnsignedInt(uriParts[2], 16)); |
85 | | - |
86 | | - if (numberOfPartsInUri > 3) { |
87 | | - builder.setResourceId(Integer.parseUnsignedInt(uriParts[3], 16)); |
88 | | - } |
89 | | - } |
90 | | - } else { |
91 | | - // If authority is blank, it is an error |
92 | | - if (uriParts[2].isBlank()) { |
93 | | - return UUri.getDefaultInstance(); |
94 | | - } |
95 | | - builder.setAuthorityName(uriParts[2]); |
96 | | - |
97 | | - if (uriParts.length > 3) { |
98 | | - builder.setUeId(Integer.parseUnsignedInt(uriParts[3], 16)); |
99 | | - if (numberOfPartsInUri > 4) { |
100 | | - builder.setUeVersionMajor(Integer.parseUnsignedInt(uriParts[4], 16)); |
101 | | - |
102 | | - if (numberOfPartsInUri > 5) { |
103 | | - builder.setResourceId(Integer.parseUnsignedInt(uriParts[5], 16)); |
104 | | - } |
105 | | - |
106 | | - } |
107 | | - } |
108 | | - } |
| 123 | + builder.setUeId(Integer.parseUnsignedInt(pathSegments[1], 16)); |
109 | 124 | } catch (NumberFormatException e) { |
110 | | - return UUri.getDefaultInstance(); |
| 125 | + throw new IllegalArgumentException("URI must contain 32 bit hex-encoded entity ID", e); |
111 | 126 | } |
112 | 127 |
|
113 | | - // Ensure the major version is less than the wildcard |
114 | | - if (builder.getUeVersionMajor() > UriFactory.WILDCARD_ENTITY_VERSION) { |
115 | | - return UUri.getDefaultInstance(); |
| 128 | + if (pathSegments[2].isEmpty()) { |
| 129 | + throw new IllegalArgumentException("URI must contain non-empty entity version"); |
116 | 130 | } |
117 | | - |
118 | | - // Ensure the resource id is less than the wildcard |
119 | | - if (builder.getResourceId() > UriFactory.WILDCARD_ENTITY_ID) { |
120 | | - return UUri.getDefaultInstance(); |
| 131 | + try { |
| 132 | + int versionMajor = Integer.parseUnsignedInt(pathSegments[2], 16); |
| 133 | + UriValidator.validateVersionMajor(versionMajor); |
| 134 | + builder.setUeVersionMajor(versionMajor); |
| 135 | + } catch (NumberFormatException e) { |
| 136 | + throw new IllegalArgumentException("URI must contain 8 bit hex-encoded entity version", e); |
121 | 137 | } |
122 | 138 |
|
| 139 | + // the fourth path segment can not be empty because the String.split() method excludes |
| 140 | + // trailing empty strings from the resulting array |
| 141 | + // it is therefore safe to simply parse it as an unsigned integer |
| 142 | + try { |
| 143 | + int resourceId = Integer.parseUnsignedInt(pathSegments[3], 16); |
| 144 | + UriValidator.validateResourceId(resourceId); |
| 145 | + builder.setResourceId(resourceId); |
| 146 | + } catch (NumberFormatException e) { |
| 147 | + throw new IllegalArgumentException("URI must contain 16 bit hex-encoded resource ID", e); |
| 148 | + } |
123 | 149 | return builder.build(); |
124 | 150 | } |
125 | 151 | } |
0 commit comments