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  package org.mycore.user2;
19  
20  import java.lang.reflect.Field;
21  import java.lang.reflect.InvocationTargetException;
22  import java.lang.reflect.Method;
23  import java.util.ArrayList;
24  import java.util.Arrays;
25  import java.util.HashMap;
26  import java.util.HashSet;
27  import java.util.List;
28  import java.util.Locale;
29  import java.util.Map;
30  import java.util.Set;
31  import java.util.stream.Collectors;
32  
33  import org.apache.logging.log4j.LogManager;
34  import org.apache.logging.log4j.Logger;
35  import org.jdom2.Element;
36  import org.jdom2.transform.JDOMSource;
37  import org.mycore.common.MCRUserInformation;
38  import org.mycore.user2.annotation.MCRUserAttribute;
39  import org.mycore.user2.annotation.MCRUserAttributeJavaConverter;
40  
41  import jakarta.xml.bind.JAXBContext;
42  import jakarta.xml.bind.Unmarshaller;
43  import jakarta.xml.bind.annotation.XmlAccessType;
44  import jakarta.xml.bind.annotation.XmlAccessorType;
45  import jakarta.xml.bind.annotation.XmlAttribute;
46  import jakarta.xml.bind.annotation.XmlElement;
47  import jakarta.xml.bind.annotation.XmlElementWrapper;
48  import jakarta.xml.bind.annotation.XmlRootElement;
49  import jakarta.xml.bind.annotation.XmlValue;
50  
51  /**
52   * This class is used to map attributes on {@link MCRUser} or {@link MCRUserInformation}
53   * to annotated properties or methods.
54   * <br><br>
55   * You can configure the mapping within <code>realms.xml</code> like this:
56   * <br>
57   * <pre>
58   * <code>
59   *  &lt;realms local="local"&gt;
60   *      ...
61   *      &lt;realm ...&gt;
62   *          ...
63   *          &lt;attributeMapping&gt;
64   *              &lt;attribute name="userName" mapping="eduPersonPrincipalName" /&gt;
65   *              &lt;attribute name="realName" mapping="displayName" /&gt;
66   *              &lt;attribute name="eMail" mapping="mail" /&gt;
67   *              &lt;attribute name="roles" mapping="eduPersonAffiliation" separator="," 
68   *                  converter="org.mycore.user2.utils.MCRRolesConverter"&gt;
69   *                  &lt;valueMapping name="employee"&gt;editor&lt;/valueMapping&gt;
70   *              &lt;/attribute&gt;
71   *          &lt;/attributeMapping&gt;
72   *          ...
73   *      &lt;/realm&gt;
74   *      ...
75   *  &lt;/realms&gt;
76   * </code>
77   * </pre>
78   * 
79   * @author Ren\u00E9 Adler (eagle)
80   *
81   */
82  public class MCRUserAttributeMapper {
83  
84      private static Logger LOGGER = LogManager.getLogger(MCRUserAttributeMapper.class);
85  
86      private HashMap<String, List<Attribute>> attributeMapping = new HashMap<>();
87  
88      public static MCRUserAttributeMapper instance(Element attributeMapping) {
89          try {
90              JAXBContext jaxb = JAXBContext.newInstance(Mappings.class.getPackage().getName(),
91                  Mappings.class.getClassLoader());
92  
93              Unmarshaller unmarshaller = jaxb.createUnmarshaller();
94              Mappings mappings = (Mappings) unmarshaller.unmarshal(new JDOMSource(attributeMapping));
95  
96              MCRUserAttributeMapper uam = new MCRUserAttributeMapper();
97              uam.attributeMapping.putAll(mappings.getAttributeMap());
98              return uam;
99          } catch (Exception e) {
100             return null;
101         }
102     }
103 
104     /**
105      * Maps configured attributes to {@link Object}.
106      * 
107      * @param object the {@link Object}
108      * @param attributes a collection of attributes to map
109      * @return <code>true</code> if any attribute was changed
110      */
111     @SuppressWarnings({ "rawtypes", "unchecked" })
112     public boolean mapAttributes(final Object object, final Map<String, ?> attributes) throws Exception {
113         boolean changed = false;
114 
115         for (Object annotated : getAnnotated(object)) {
116             MCRUserAttribute attrAnno = null;
117 
118             if (annotated instanceof Field) {
119                 attrAnno = ((Field) annotated).getAnnotation(MCRUserAttribute.class);
120             } else if (annotated instanceof Method) {
121                 attrAnno = ((Method) annotated).getAnnotation(MCRUserAttribute.class);
122             }
123 
124             if (attrAnno != null) {
125                 final String name = attrAnno.name().isEmpty() ? getAttriutebName(annotated) : attrAnno.name();
126                 final List<Attribute> attribs = attributeMapping.get(name);
127 
128                 if (attributes != null) {
129                     for (Attribute attribute : attribs) {
130                         if (attributes.containsKey(attribute.mapping)) {
131                             Object value = attributes.get(attribute.mapping);
132 
133                             MCRUserAttributeJavaConverter aConv = null;
134 
135                             if (annotated instanceof Field) {
136                                 aConv = ((Field) annotated).getAnnotation(MCRUserAttributeJavaConverter.class);
137                             } else if (annotated instanceof Method) {
138                                 aConv = ((Method) annotated).getAnnotation(MCRUserAttributeJavaConverter.class);
139                             }
140 
141                             Class<? extends MCRUserAttributeConverter> convCls = null;
142                             if (attribute.converter != null) {
143                                 convCls = (Class<? extends MCRUserAttributeConverter>) Class
144                                     .forName(attribute.converter);
145                             } else if (aConv != null) {
146                                 convCls = aConv.value();
147                             }
148 
149                             if (convCls != null) {
150                                 MCRUserAttributeConverter converter = convCls.getDeclaredConstructor().newInstance();
151                                 LOGGER.debug("convert value \"{}\" with \"{}\"", value, converter.getClass().getName());
152                                 value = converter.convert(value,
153                                     attribute.separator != null ? attribute.separator : attrAnno.separator(),
154                                     attribute.getValueMap());
155                             }
156 
157                             if (value != null || ((attrAnno.nullable() || attribute.nullable) && value == null)) {
158                                 Object oldValue = getValue(object, annotated);
159                                 if (oldValue != null && oldValue.equals(value)) {
160                                     continue;
161                                 }
162 
163                                 if (annotated instanceof Field) {
164                                     final Field field = (Field) annotated;
165 
166                                     LOGGER.debug("map attribute \"{}\" with value \"{}\" to field \"{}\"",
167                                         attribute.mapping, value, field.getName());
168 
169                                     field.setAccessible(true);
170                                     field.set(object, value);
171 
172                                     changed = true;
173                                 } else if (annotated instanceof Method) {
174                                     final Method method = (Method) annotated;
175 
176                                     LOGGER.debug("map attribute \"{}\" with value \"{}\" to method \"{}\"",
177                                         attribute.mapping, value, method.getName());
178 
179                                     method.setAccessible(true);
180                                     method.invoke(object, value);
181 
182                                     changed = true;
183                                 }
184                             } else {
185                                 throw new IllegalArgumentException(
186                                     "A not nullable attribute \"" + name + "\" was null.");
187                             }
188                         }
189                     }
190                 }
191             }
192         }
193 
194         return changed;
195     }
196 
197     /**
198      * Returns a collection of mapped attribute names.
199      * 
200      * @return a collection of mapped attribute names
201      */
202     public Set<String> getAttributeNames() {
203         Set<String> mAtt = new HashSet<>();
204 
205         for (final String name : attributeMapping.keySet()) {
206             attributeMapping.get(name).forEach(a -> mAtt.add(a.mapping));
207         }
208 
209         return mAtt;
210     }
211 
212     private List<Object> getAnnotated(final Object obj) {
213         List<Object> al = new ArrayList<>();
214 
215         al.addAll(getAnnotatedFields(obj.getClass()));
216         al.addAll(getAnnotatedMethods(obj.getClass()));
217 
218         if (obj.getClass().getSuperclass() != null) {
219             al.addAll(getAnnotatedFields(obj.getClass().getSuperclass()));
220             al.addAll(getAnnotatedMethods(obj.getClass().getSuperclass()));
221         }
222 
223         return al;
224     }
225 
226     private List<Object> getAnnotatedFields(final Class<?> cls) {
227         return Arrays.stream(cls.getDeclaredFields())
228             .filter(field -> field.getAnnotation(MCRUserAttribute.class) != null)
229             .collect(Collectors.toList());
230     }
231 
232     private List<Object> getAnnotatedMethods(final Class<?> cls) {
233         return Arrays.stream(cls.getDeclaredMethods())
234             .filter(method -> method.getAnnotation(MCRUserAttribute.class) != null)
235             .collect(Collectors.toList());
236     }
237 
238     private String getAttriutebName(final Object annotated) {
239         if (annotated instanceof Field) {
240             return ((Field) annotated).getName();
241         } else if (annotated instanceof Method) {
242             String name = ((Method) annotated).getName();
243             if (name.startsWith("set")) {
244                 name = name.substring(3);
245             }
246             return name.substring(0, 1).toLowerCase(Locale.ROOT) + name.substring(1);
247         }
248 
249         return null;
250     }
251 
252     private Object getValue(final Object object, final Object annotated) throws IllegalArgumentException,
253         IllegalAccessException, InvocationTargetException, NoSuchMethodException, SecurityException {
254         Object value = null;
255 
256         if (annotated instanceof Field) {
257             final Field field = (Field) annotated;
258 
259             field.setAccessible(true);
260             value = field.get(object);
261         } else if (annotated instanceof Method) {
262             Method method = null;
263             String name = ((Method) annotated).getName();
264             if (name.startsWith("get")) {
265                 name = "s" + name.substring(1);
266                 method = object.getClass().getMethod(name);
267             }
268 
269             if (method != null) {
270                 method.setAccessible(true);
271                 value = method.invoke(object);
272             }
273         }
274 
275         return value;
276     }
277 
278     @XmlRootElement(name = "realm")
279     @XmlAccessorType(XmlAccessType.FIELD)
280     private static class Mappings {
281         @XmlElementWrapper(name = "attributeMapping")
282         @XmlElement(name = "attribute")
283         List<Attribute> attributes;
284 
285         Map<String, List<Attribute>> getAttributeMap() {
286             return attributes.stream().collect(Collectors.groupingBy(attrib -> attrib.name));
287         }
288     }
289 
290     @XmlRootElement(name = "attribute")
291     @XmlAccessorType(XmlAccessType.FIELD)
292     private static class Attribute {
293         @XmlAttribute(required = true)
294         String name;
295 
296         @XmlAttribute(required = true)
297         String mapping;
298 
299         @XmlAttribute
300         String separator;
301 
302         @XmlAttribute
303         boolean nullable;
304 
305         @XmlAttribute
306         String converter;
307 
308         @XmlElement
309         List<ValueMapping> valueMapping;
310 
311         Map<String, String> getValueMap() {
312             if (valueMapping == null) {
313                 return null;
314             }
315 
316             Map<String, String> map = new HashMap<>();
317             for (ValueMapping vm : valueMapping) {
318                 map.put(vm.name, vm.mapping);
319             }
320             return map;
321         }
322     }
323 
324     @XmlRootElement(name = "valueMapping")
325     @XmlAccessorType(XmlAccessType.FIELD)
326     private static class ValueMapping {
327         @XmlAttribute(required = true)
328         String name;
329 
330         @XmlValue
331         String mapping;
332     }
333 }