1 package org
.argeo
.api
.acr
;
3 import static javax
.xml
.XMLConstants
.W3C_XML_SCHEMA_NS_URI
;
6 import java
.net
.URISyntaxException
;
7 import java
.time
.Instant
;
8 import java
.time
.format
.DateTimeParseException
;
9 import java
.util
.Arrays
;
10 import java
.util
.Base64
;
11 import java
.util
.List
;
12 import java
.util
.Optional
;
13 import java
.util
.UUID
;
15 import javax
.xml
.namespace
.NamespaceContext
;
16 import javax
.xml
.namespace
.QName
;
19 * Minimal standard attribute types that MUST be supported. All related classes
20 * belong to java.base and can be implicitly derived form a given
21 * <code>String</code>.
23 public enum CrAttributeType
{
24 BOOLEAN(Boolean
.class, W3C_XML_SCHEMA_NS_URI
, "boolean", new BooleanFormatter()), //
25 INTEGER(Integer
.class, W3C_XML_SCHEMA_NS_URI
, "integer", new IntegerFormatter()), //
26 LONG(Long
.class, W3C_XML_SCHEMA_NS_URI
, "long", new LongFormatter()), //
27 DOUBLE(Double
.class, W3C_XML_SCHEMA_NS_URI
, "double", new DoubleFormatter()), //
28 // we do not support short and float, like recent additions to Java
29 // (e.g. optional primitives)
30 DATE_TIME(Instant
.class, W3C_XML_SCHEMA_NS_URI
, "dateTime", new InstantFormatter()), //
31 UUID(UUID
.class, ArgeoNamespace
.CR_NAMESPACE_URI
, "uuid", new UuidFormatter()), //
32 QNAME(QName
.class, W3C_XML_SCHEMA_NS_URI
, "QName", new QNameFormatter()), //
33 ANY_URI(URI
.class, W3C_XML_SCHEMA_NS_URI
, "anyUri", new UriFormatter()), //
34 STRING(String
.class, W3C_XML_SCHEMA_NS_URI
, "string", new StringFormatter()), //
37 private final Class
<?
> clss
;
38 private final AttributeFormatter
<?
> formatter
;
40 private final ContentName qName
;
42 private <T
> CrAttributeType(Class
<T
> clss
, String namespaceUri
, String localName
, AttributeFormatter
<T
> formatter
) {
44 this.formatter
= formatter
;
46 qName
= new ContentName(namespaceUri
, localName
, RuntimeNamespaceContext
.getNamespaceContext());
49 public QName
getQName() {
53 public Class
<?
> getClss() {
57 public AttributeFormatter
<?
> getFormatter() {
62 // public String getDefaultPrefix() {
64 // return CrName.CR_DEFAULT_PREFIX;
70 // public String getNamespaceURI() {
72 // return CrName.CR_NAMESPACE_URI;
74 // return XMLConstants.W3C_XML_SCHEMA_NS_URI;
77 /** Default parsing procedure from a String to an object. */
78 public static Object
parse(String str
) {
79 return parse(RuntimeNamespaceContext
.getNamespaceContext(), str
);
82 /** Default parsing procedure from a String to an object. */
83 public static Object
parse(NamespaceContext namespaceContext
, String str
) {
85 throw new IllegalArgumentException("String cannot be null");
88 if (str
.length() == 4 || str
.length() == 5)
89 return BOOLEAN
.getFormatter().parse(namespaceContext
, str
);
90 } catch (IllegalArgumentException e
) {
94 return INTEGER
.getFormatter().parse(namespaceContext
, str
);
95 } catch (IllegalArgumentException e
) {
99 return LONG
.getFormatter().parse(namespaceContext
, str
);
100 } catch (IllegalArgumentException e
) {
104 return DOUBLE
.getFormatter().parse(namespaceContext
, str
);
105 } catch (IllegalArgumentException e
) {
109 return DATE_TIME
.getFormatter().parse(namespaceContext
, str
);
110 } catch (IllegalArgumentException e
) {
114 if (str
.length() == 36)
115 return UUID
.getFormatter().parse(namespaceContext
, str
);
116 } catch (IllegalArgumentException e
) {
121 if (str
.startsWith("[") && str
.endsWith("]")) {
123 if (str
.indexOf(":") >= 0) {
124 QName qName
= (QName
) QNAME
.getFormatter().parse(namespaceContext
, str
);
125 return (java
.net
.URI
) ANY_URI
.getFormatter().parse(qName
.getNamespaceURI() + qName
.getLocalPart());
127 } catch (IllegalArgumentException e
) {
133 if (str
.indexOf(":") >= 0) {
134 QName qName
= (QName
) QNAME
.getFormatter().parse(namespaceContext
, str
);
135 // note: this QName may not be valid
136 // note: CURIE should be explicitly defined with surrounding brackets
139 } catch (IllegalArgumentException e
) {
143 java
.net
.URI uri
= (java
.net
.URI
) ANY_URI
.getFormatter().parse(namespaceContext
, str
);
144 if (uri
.getScheme() != null)
146 String path
= uri
.getPath();
147 if (path
.indexOf('/') >= 0)
149 // if it is not clearly a path, we will consider it as a string
150 // because their is no way to distinguish between 'any_string'
151 // and 'any_file_name'.
152 // Note that providing ./any_file_name would result in an equivalent URI
153 } catch (IllegalArgumentException e
) {
157 // TODO support QName as a type? It would require a NamespaceContext
158 // see https://www.oreilly.com/library/view/xml-schema/0596002521/re91.html
161 return STRING
.getFormatter().parse(namespaceContext
, str
);
165 * Cast well know java types based on {@link Object#toString()} of the provided
169 public static <T
> Optional
<T
> cast(Class
<T
> clss
, Object value
) {
170 return cast(RuntimeNamespaceContext
.getNamespaceContext(), clss
, value
);
174 * Cast well know java types based on {@link Object#toString()} of the provided
178 @SuppressWarnings("unchecked")
179 public static <T
> Optional
<T
> cast(NamespaceContext namespaceContext
, Class
<T
> clss
, Object value
) {
180 // if value is null, optional is empty
182 return Optional
.empty();
184 // if a default has been explicitly requested by passing Object.class
185 // we parse the related String
186 if (clss
.isAssignableFrom(Object
.class)) {
187 return Optional
.of((T
) parse(value
.toString()));
190 // if value can be cast directly, let's do it
191 if (value
.getClass().isAssignableFrom(clss
)) {
192 return Optional
.of(((T
) value
));
195 // let's cast between various numbers (possibly losing precision)
196 if (value
instanceof Number number
) {
197 if (Long
.class.isAssignableFrom(clss
))
198 return Optional
.of((T
) (Long
) number
.longValue());
199 else if (Integer
.class.isAssignableFrom(clss
))
200 return Optional
.of((T
) (Integer
) number
.intValue());
201 else if (Double
.class.isAssignableFrom(clss
))
202 return Optional
.of((T
) (Double
) number
.doubleValue());
205 // let's now try with the string representation
206 String strValue
= value
instanceof String ?
(String
) value
: value
.toString();
208 if (String
.class.isAssignableFrom(clss
)) {
209 return Optional
.of((T
) strValue
);
211 if (java
.util
.UUID
.class.isAssignableFrom(clss
)) {
212 return Optional
.of((T
) java
.util
.UUID
.fromString(strValue
));
214 if (QName
.class.isAssignableFrom(clss
)) {
215 return Optional
.of((T
) NamespaceUtils
.parsePrefixedName(namespaceContext
, strValue
));
218 else if (Long
.class.isAssignableFrom(clss
)) {
219 if (value
instanceof Long
)
220 return Optional
.of((T
) value
);
221 return Optional
.of((T
) Long
.valueOf(strValue
));
222 } else if (Integer
.class.isAssignableFrom(clss
)) {
223 if (value
instanceof Integer
)
224 return Optional
.of((T
) value
);
225 return Optional
.of((T
) Integer
.valueOf(strValue
));
226 } else if (Double
.class.isAssignableFrom(clss
)) {
227 if (value
instanceof Double
)
228 return Optional
.of((T
) value
);
229 return Optional
.of((T
) Double
.valueOf(strValue
));
232 // let's now try to parse the string representation to a well-known type
233 Object parsedValue
= parse(strValue
);
234 if (parsedValue
.getClass().isAssignableFrom(clss
)) {
235 return Optional
.of(((T
) value
));
237 throw new ClassCastException("Cannot convert " + value
.getClass() + " to " + clss
);
240 /** Utility to convert a data: URI to bytes. */
241 public static byte[] bytesFromDataURI(URI uri
) {
242 if (!"data".equals(uri
.getScheme()))
243 throw new IllegalArgumentException("URI must have 'data' as a scheme");
244 String schemeSpecificPart
= uri
.getSchemeSpecificPart();
245 int commaIndex
= schemeSpecificPart
.indexOf(',');
246 String prefix
= schemeSpecificPart
.substring(0, commaIndex
);
247 List
<String
> info
= Arrays
.asList(prefix
.split(";"));
248 if (!info
.contains("base64"))
249 throw new IllegalArgumentException("URI must specify base64");
251 String base64Str
= schemeSpecificPart
.substring(commaIndex
+ 1);
252 return Base64
.getDecoder().decode(base64Str
);
256 /** Utility to convert bytes to a data: URI. */
257 public static URI
bytesToDataURI(byte[] arr
) {
258 String base64Str
= Base64
.getEncoder().encodeToString(arr
);
260 final String PREFIX
= "data:application/octet-stream;base64,";
261 return new URI(PREFIX
+ base64Str
);
262 } catch (URISyntaxException e
) {
263 throw new IllegalStateException("Cannot serialize bytes a Base64 data URI", e
);
268 static class BooleanFormatter
implements AttributeFormatter
<Boolean
> {
271 * @param str must be exactly equals to either 'true' or 'false' (different
272 * contract than {@link Boolean#parseBoolean(String)}.
275 public Boolean
parse(NamespaceContext namespaceContext
, String str
) throws IllegalArgumentException
{
276 if ("true".equals(str
))
278 if ("false".equals(str
))
279 return Boolean
.FALSE
;
280 throw new IllegalArgumentException("Argument is neither 'true' or 'false' : " + str
);
284 static class IntegerFormatter
implements AttributeFormatter
<Integer
> {
286 public Integer
parse(NamespaceContext namespaceContext
, String str
) throws NumberFormatException
{
287 return Integer
.parseInt(str
);
291 static class LongFormatter
implements AttributeFormatter
<Long
> {
293 public Long
parse(NamespaceContext namespaceContext
, String str
) throws NumberFormatException
{
294 return Long
.parseLong(str
);
298 static class DoubleFormatter
implements AttributeFormatter
<Double
> {
301 public Double
parse(NamespaceContext namespaceContext
, String str
) throws NumberFormatException
{
302 return Double
.parseDouble(str
);
306 static class InstantFormatter
implements AttributeFormatter
<Instant
> {
309 public Instant
parse(NamespaceContext namespaceContext
, String str
) throws IllegalArgumentException
{
311 return Instant
.parse(str
);
312 } catch (DateTimeParseException e
) {
313 throw new IllegalArgumentException("Cannot parse '" + str
+ "' as an instant", e
);
318 static class UuidFormatter
implements AttributeFormatter
<UUID
> {
321 public UUID
parse(NamespaceContext namespaceContext
, String str
) throws IllegalArgumentException
{
322 return java
.util
.UUID
.fromString(str
);
326 static class UriFormatter
implements AttributeFormatter
<URI
> {
329 public URI
parse(NamespaceContext namespaceContext
, String str
) throws IllegalArgumentException
{
332 } catch (URISyntaxException e
) {
333 throw new IllegalArgumentException("Cannot parse " + str
+ " as an URI.", e
);
339 static class QNameFormatter
implements AttributeFormatter
<QName
> {
342 public QName
parse(NamespaceContext namespaceContext
, String str
) throws IllegalArgumentException
{
343 return NamespaceUtils
.parsePrefixedName(namespaceContext
, str
);
348 static class StringFormatter
implements AttributeFormatter
<String
> {
351 public String
parse(NamespaceContext namespaceContext
, String str
) {
356 public String
format(String obj
) {