1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.mycore.restapi.converter;
20
21 import java.io.IOException;
22 import java.io.OutputStream;
23 import java.lang.annotation.Annotation;
24 import java.lang.reflect.GenericArrayType;
25 import java.lang.reflect.ParameterizedType;
26 import java.lang.reflect.Type;
27 import java.nio.charset.Charset;
28 import java.nio.charset.StandardCharsets;
29 import java.util.Arrays;
30 import java.util.Collection;
31 import java.util.Locale;
32 import java.util.concurrent.ConcurrentHashMap;
33 import java.util.function.Predicate;
34 import java.util.function.Supplier;
35 import java.util.stream.Stream;
36
37 import jakarta.ws.rs.InternalServerErrorException;
38 import jakarta.ws.rs.Produces;
39 import jakarta.ws.rs.WebApplicationException;
40 import jakarta.ws.rs.core.MediaType;
41 import jakarta.ws.rs.core.MultivaluedMap;
42 import jakarta.ws.rs.ext.MessageBodyWriter;
43 import jakarta.ws.rs.ext.Provider;
44 import jakarta.xml.bind.JAXBContext;
45 import jakarta.xml.bind.JAXBElement;
46 import jakarta.xml.bind.JAXBException;
47 import jakarta.xml.bind.Marshaller;
48 import jakarta.xml.bind.annotation.XmlElementWrapper;
49 import jakarta.xml.bind.annotation.XmlRootElement;
50 import jakarta.xml.bind.annotation.XmlType;
51
52 @Provider
53 @Produces({ MediaType.APPLICATION_XML, MediaType.TEXT_XML })
54 public class MCRWrappedXMLWriter implements MessageBodyWriter<Object> {
55
56 private static final ConcurrentHashMap<Class, JAXBContext> CTX_MAP = new ConcurrentHashMap<>();
57
58 private static final Predicate<Class> JAXB_CHECKER = type -> type.isAnnotationPresent(XmlRootElement.class)
59 || type.isAnnotationPresent(XmlType.class);
60
61 private static boolean verifyArrayType(Class type) {
62 Class componentType = type.getComponentType();
63 return JAXB_CHECKER.test(componentType) || JAXBElement.class.isAssignableFrom(componentType);
64 }
65
66 private static boolean verifyGenericType(Type genericType) {
67 if (!(genericType instanceof ParameterizedType)) {
68 return false;
69 }
70
71 final ParameterizedType pt = (ParameterizedType) genericType;
72
73 if (pt.getActualTypeArguments().length > 1) {
74 return false;
75 }
76
77 final Type ta = pt.getActualTypeArguments()[0];
78
79 if (ta instanceof ParameterizedType) {
80 ParameterizedType lpt = (ParameterizedType) ta;
81 return (lpt.getRawType() instanceof Class)
82 && JAXBElement.class.isAssignableFrom((Class) lpt.getRawType());
83 }
84
85 if (!(pt.getActualTypeArguments()[0] instanceof Class)) {
86 return false;
87 }
88
89 final Class listClass = (Class) pt.getActualTypeArguments()[0];
90
91 return JAXB_CHECKER.test(listClass);
92 }
93
94 private static Class getElementClass(Class<?> type, Type genericType) {
95 Type ta;
96 if (genericType instanceof ParameterizedType) {
97 ta = ((ParameterizedType) genericType).getActualTypeArguments()[0];
98 } else if (genericType instanceof GenericArrayType) {
99 ta = ((GenericArrayType) genericType).getGenericComponentType();
100 } else {
101 ta = type.getComponentType();
102 }
103 if (ta instanceof ParameterizedType) {
104
105 ta = ((ParameterizedType) ta).getActualTypeArguments()[0];
106 }
107 return (Class) ta;
108 }
109
110 @Override
111 public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
112 if (Stream.of(annotations).noneMatch(a -> XmlElementWrapper.class.isAssignableFrom(a.annotationType()))) {
113 return false;
114 }
115 if (Collection.class.isAssignableFrom(type)) {
116 return verifyGenericType(genericType) && Stream.of(MediaType.APPLICATION_XML_TYPE, MediaType.TEXT_XML_TYPE)
117 .anyMatch(t -> t.isCompatible(mediaType));
118 } else {
119 return type.isArray() && verifyArrayType(type)
120 && Stream.of(MediaType.APPLICATION_XML_TYPE, MediaType.TEXT_XML_TYPE)
121 .anyMatch(t -> t.isCompatible(mediaType));
122 }
123 }
124
125 @Override
126 public void writeTo(Object t, Class<?> type, Type genericType,
127 Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders,
128 OutputStream entityStream) throws IOException, WebApplicationException {
129 Collection collection = (type.isArray()) ? Arrays.asList((Object[]) t) : (Collection) t;
130 Class elementType = getElementClass(type, genericType);
131 Supplier<Marshaller> m = () -> {
132 try {
133 JAXBContext ctx = CTX_MAP.computeIfAbsent(elementType, et -> {
134 try {
135 return JAXBContext.newInstance(et);
136 } catch (JAXBException e) {
137 throw new InternalServerErrorException(e);
138 }
139 });
140 Marshaller marshaller = ctx.createMarshaller();
141 marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true);
142 return marshaller;
143 } catch (JAXBException e) {
144 throw new InternalServerErrorException(e);
145 }
146 };
147 try {
148 XmlElementWrapper wrapper = Stream.of(annotations)
149 .filter(a -> XmlElementWrapper.class.isAssignableFrom(a.annotationType()))
150 .map(XmlElementWrapper.class::cast)
151 .findAny()
152 .get();
153 writeCollection(wrapper, collection, StandardCharsets.UTF_8, m, entityStream);
154 } catch (JAXBException ex) {
155 throw new InternalServerErrorException(ex);
156 }
157 }
158
159 public final void writeCollection(XmlElementWrapper wrapper, Collection<?> t, Charset c,
160 Supplier<Marshaller> m, OutputStream entityStream)
161 throws JAXBException, IOException {
162 final String rootElement = wrapper.name();
163
164 entityStream.write(
165 String.format(Locale.ROOT, "<?xml version=\"1.0\" encoding=\"%s\" standalone=\"yes\"?>", c.name())
166 .getBytes(c));
167 if (t.isEmpty()) {
168 entityStream.write(String.format(Locale.ROOT, "<%s />", rootElement).getBytes(c));
169 } else {
170 entityStream.write(String.format(Locale.ROOT, "<%s>", rootElement).getBytes(c));
171 Marshaller marshaller = m.get();
172 for (Object o : t) {
173 marshaller.marshal(o, entityStream);
174 }
175 entityStream.write(String.format(Locale.ROOT, "</%s>", rootElement).getBytes(c));
176 }
177 }
178 }