]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.api.acr/src/org/argeo/api/acr/CrAttributeType.java
Introduce UUID identified and openForEdit/freeze cycle
[lgpl/argeo-commons.git] / org.argeo.api.acr / src / org / argeo / api / acr / CrAttributeType.java
1 package org.argeo.api.acr;
2
3 import static javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI;
4
5 import java.net.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;
14
15 import javax.xml.namespace.NamespaceContext;
16 import javax.xml.namespace.QName;
17
18 /**
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>.
22 */
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()), //
35 ;
36
37 private final Class<?> clss;
38 private final AttributeFormatter<?> formatter;
39
40 private final ContentName qName;
41
42 private <T> CrAttributeType(Class<T> clss, String namespaceUri, String localName, AttributeFormatter<T> formatter) {
43 this.clss = clss;
44 this.formatter = formatter;
45
46 qName = new ContentName(namespaceUri, localName, RuntimeNamespaceContext.getNamespaceContext());
47 }
48
49 public QName getQName() {
50 return qName;
51 }
52
53 public Class<?> getClss() {
54 return clss;
55 }
56
57 public AttributeFormatter<?> getFormatter() {
58 return formatter;
59 }
60
61 // @Override
62 // public String getDefaultPrefix() {
63 // if (equals(UUID))
64 // return CrName.CR_DEFAULT_PREFIX;
65 // else
66 // return "xs";
67 // }
68 //
69 // @Override
70 // public String getNamespaceURI() {
71 // if (equals(UUID))
72 // return CrName.CR_NAMESPACE_URI;
73 // else
74 // return XMLConstants.W3C_XML_SCHEMA_NS_URI;
75 // }
76
77 /** Default parsing procedure from a String to an object. */
78 public static Object parse(String str) {
79 return parse(RuntimeNamespaceContext.getNamespaceContext(), str);
80 }
81
82 /** Default parsing procedure from a String to an object. */
83 public static Object parse(NamespaceContext namespaceContext, String str) {
84 if (str == null)
85 throw new IllegalArgumentException("String cannot be null");
86 // order IS important
87 try {
88 if (str.length() == 4 || str.length() == 5)
89 return BOOLEAN.getFormatter().parse(namespaceContext, str);
90 } catch (IllegalArgumentException e) {
91 // silent
92 }
93 try {
94 return INTEGER.getFormatter().parse(namespaceContext, str);
95 } catch (IllegalArgumentException e) {
96 // silent
97 }
98 try {
99 return LONG.getFormatter().parse(namespaceContext, str);
100 } catch (IllegalArgumentException e) {
101 // silent
102 }
103 try {
104 return DOUBLE.getFormatter().parse(namespaceContext, str);
105 } catch (IllegalArgumentException e) {
106 // silent
107 }
108 try {
109 return DATE_TIME.getFormatter().parse(namespaceContext, str);
110 } catch (IllegalArgumentException e) {
111 // silent
112 }
113 try {
114 if (str.length() == 36)
115 return UUID.getFormatter().parse(namespaceContext, str);
116 } catch (IllegalArgumentException e) {
117 // silent
118 }
119
120 // CURIE
121 if (str.startsWith("[") && str.endsWith("]")) {
122 try {
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());
126 }
127 } catch (IllegalArgumentException e) {
128 // silent
129 }
130 }
131
132 try {
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
137 return qName;
138 }
139 } catch (IllegalArgumentException e) {
140 // silent
141 }
142 try {
143 java.net.URI uri = (java.net.URI) ANY_URI.getFormatter().parse(namespaceContext, str);
144 if (uri.getScheme() != null)
145 return uri;
146 String path = uri.getPath();
147 if (path.indexOf('/') >= 0)
148 return uri;
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) {
154 // silent
155 }
156
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
159
160 // default
161 return STRING.getFormatter().parse(namespaceContext, str);
162 }
163
164 /**
165 * Cast well know java types based on {@link Object#toString()} of the provided
166 * object.
167 *
168 */
169 public static <T> Optional<T> cast(Class<T> clss, Object value) {
170 return cast(RuntimeNamespaceContext.getNamespaceContext(), clss, value);
171 }
172
173 /**
174 * Cast well know java types based on {@link Object#toString()} of the provided
175 * object.
176 *
177 */
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
181 if (value == null)
182 return Optional.empty();
183
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()));
188 }
189
190 // if value can be cast directly, let's do it
191 if (value.getClass().isAssignableFrom(clss)) {
192 return Optional.of(((T) value));
193 }
194
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());
203 }
204
205 // let's now try with the string representation
206 String strValue = value instanceof String ? (String) value : value.toString();
207
208 if (String.class.isAssignableFrom(clss)) {
209 return Optional.of((T) strValue);
210 }
211 if (QName.class.isAssignableFrom(clss)) {
212 return Optional.of((T) NamespaceUtils.parsePrefixedName(namespaceContext, strValue));
213 }
214 // Numbers
215 else if (Long.class.isAssignableFrom(clss)) {
216 if (value instanceof Long)
217 return Optional.of((T) value);
218 return Optional.of((T) Long.valueOf(strValue));
219 } else if (Integer.class.isAssignableFrom(clss)) {
220 if (value instanceof Integer)
221 return Optional.of((T) value);
222 return Optional.of((T) Integer.valueOf(strValue));
223 } else if (Double.class.isAssignableFrom(clss)) {
224 if (value instanceof Double)
225 return Optional.of((T) value);
226 return Optional.of((T) Double.valueOf(strValue));
227 }
228
229 // let's now try to parse the string representation to a well-known type
230 Object parsedValue = parse(strValue);
231 if (parsedValue.getClass().isAssignableFrom(clss)) {
232 return Optional.of(((T) value));
233 }
234 throw new ClassCastException("Cannot convert " + value.getClass() + " to " + clss);
235 }
236
237 /** Utility to convert a data: URI to bytes. */
238 public static byte[] bytesFromDataURI(URI uri) {
239 if (!"data".equals(uri.getScheme()))
240 throw new IllegalArgumentException("URI must have 'data' as a scheme");
241 String schemeSpecificPart = uri.getSchemeSpecificPart();
242 int commaIndex = schemeSpecificPart.indexOf(',');
243 String prefix = schemeSpecificPart.substring(0, commaIndex);
244 List<String> info = Arrays.asList(prefix.split(";"));
245 if (!info.contains("base64"))
246 throw new IllegalArgumentException("URI must specify base64");
247
248 String base64Str = schemeSpecificPart.substring(commaIndex + 1);
249 return Base64.getDecoder().decode(base64Str);
250
251 }
252
253 /** Utility to convert bytes to a data: URI. */
254 public static URI bytesToDataURI(byte[] arr) {
255 String base64Str = Base64.getEncoder().encodeToString(arr);
256 try {
257 final String PREFIX = "data:application/octet-stream;base64,";
258 return new URI(PREFIX + base64Str);
259 } catch (URISyntaxException e) {
260 throw new IllegalStateException("Cannot serialize bytes a Base64 data URI", e);
261 }
262
263 }
264
265 static class BooleanFormatter implements AttributeFormatter<Boolean> {
266
267 /**
268 * @param str must be exactly equals to either 'true' or 'false' (different
269 * contract than {@link Boolean#parseBoolean(String)}.
270 */
271 @Override
272 public Boolean parse(NamespaceContext namespaceContext, String str) throws IllegalArgumentException {
273 if ("true".equals(str))
274 return Boolean.TRUE;
275 if ("false".equals(str))
276 return Boolean.FALSE;
277 throw new IllegalArgumentException("Argument is neither 'true' or 'false' : " + str);
278 }
279 }
280
281 static class IntegerFormatter implements AttributeFormatter<Integer> {
282 @Override
283 public Integer parse(NamespaceContext namespaceContext, String str) throws NumberFormatException {
284 return Integer.parseInt(str);
285 }
286 }
287
288 static class LongFormatter implements AttributeFormatter<Long> {
289 @Override
290 public Long parse(NamespaceContext namespaceContext, String str) throws NumberFormatException {
291 return Long.parseLong(str);
292 }
293 }
294
295 static class DoubleFormatter implements AttributeFormatter<Double> {
296
297 @Override
298 public Double parse(NamespaceContext namespaceContext, String str) throws NumberFormatException {
299 return Double.parseDouble(str);
300 }
301 }
302
303 static class InstantFormatter implements AttributeFormatter<Instant> {
304
305 @Override
306 public Instant parse(NamespaceContext namespaceContext, String str) throws IllegalArgumentException {
307 try {
308 return Instant.parse(str);
309 } catch (DateTimeParseException e) {
310 throw new IllegalArgumentException("Cannot parse '" + str + "' as an instant", e);
311 }
312 }
313 }
314
315 static class UuidFormatter implements AttributeFormatter<UUID> {
316
317 @Override
318 public UUID parse(NamespaceContext namespaceContext, String str) throws IllegalArgumentException {
319 return java.util.UUID.fromString(str);
320 }
321 }
322
323 static class UriFormatter implements AttributeFormatter<URI> {
324
325 @Override
326 public URI parse(NamespaceContext namespaceContext, String str) throws IllegalArgumentException {
327 try {
328 return new URI(str);
329 } catch (URISyntaxException e) {
330 throw new IllegalArgumentException("Cannot parse " + str + " as an URI.", e);
331 }
332 }
333
334 }
335
336 static class QNameFormatter implements AttributeFormatter<QName> {
337
338 @Override
339 public QName parse(NamespaceContext namespaceContext, String str) throws IllegalArgumentException {
340 return NamespaceUtils.parsePrefixedName(namespaceContext, str);
341 }
342
343 }
344
345 static class StringFormatter implements AttributeFormatter<String> {
346
347 @Override
348 public String parse(NamespaceContext namespaceContext, String str) {
349 return str;
350 }
351
352 @Override
353 public String format(String obj) {
354 return obj;
355 }
356
357 }
358
359 }