View Javadoc
1   /*
2    * This file is part of ***  M y C o R e  ***
3    * See http://www.mycore.de/ for details.
4    *
5    * MyCoRe is free software: you can redistribute it and/or modify
6    * it under the terms of the GNU General Public License as published by
7    * the Free Software Foundation, either version 3 of the License, or
8    * (at your option) any later version.
9    *
10   * MyCoRe is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with MyCoRe.  If not, see <http://www.gnu.org/licenses/>.
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             //JAXBElement
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 }