View Javadoc
1   /*
2    * Portions of this software was developed by employees of the National Institute
3    * of Standards and Technology (NIST), an agency of the Federal Government and is
4    * being made available as a public service. Pursuant to title 17 United States
5    * Code Section 105, works of NIST employees are not subject to copyright
6    * protection in the United States. This software may be subject to foreign
7    * copyright. Permission in the United States and in foreign countries, to the
8    * extent that NIST may hold copyright, to use, copy, modify, create derivative
9    * works, and distribute this software and its documentation without fee is hereby
10   * granted on a non-exclusive basis, provided that this notice and disclaimer
11   * of warranty appears in all copies.
12   *
13   * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
14   * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
15   * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
16   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
17   * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
18   * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
19   * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
20   * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
21   * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
22   * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
23   * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
24   * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
25   */
26  
27  package gov.nist.secauto.metaschema.databind.model.info;
28  
29  import com.fasterxml.jackson.core.JsonParser;
30  import com.fasterxml.jackson.core.JsonToken;
31  
32  import gov.nist.secauto.metaschema.core.model.util.JsonUtil;
33  import gov.nist.secauto.metaschema.core.model.util.XmlEventUtil;
34  import gov.nist.secauto.metaschema.core.util.ObjectUtils;
35  import gov.nist.secauto.metaschema.databind.io.BindingException;
36  import gov.nist.secauto.metaschema.databind.io.json.IJsonParsingContext;
37  import gov.nist.secauto.metaschema.databind.io.json.IJsonWritingContext;
38  import gov.nist.secauto.metaschema.databind.io.xml.IXmlParsingContext;
39  import gov.nist.secauto.metaschema.databind.io.xml.IXmlWritingContext;
40  import gov.nist.secauto.metaschema.databind.model.IBoundFlagInstance;
41  import gov.nist.secauto.metaschema.databind.model.IBoundNamedModelInstance;
42  import gov.nist.secauto.metaschema.databind.model.IClassBinding;
43  
44  import org.codehaus.stax2.XMLEventReader2;
45  
46  import java.io.IOException;
47  import java.lang.reflect.ParameterizedType;
48  import java.util.Collection;
49  import java.util.LinkedHashMap;
50  import java.util.List;
51  import java.util.Map;
52  
53  import javax.xml.namespace.QName;
54  import javax.xml.stream.XMLStreamException;
55  import javax.xml.stream.events.StartElement;
56  import javax.xml.stream.events.XMLEvent;
57  
58  import edu.umd.cs.findbugs.annotations.NonNull;
59  import edu.umd.cs.findbugs.annotations.Nullable;
60  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
61  
62  class MapPropertyInfo
63      extends AbstractModelPropertyInfo {
64  
65    @SuppressWarnings("null")
66    @Override
67    public Collection<?> getItemsFromValue(Object value) {
68      return value == null ? List.of() : ((Map<?, ?>) value).values();
69    }
70  
71    @Override
72    public int getItemCount(Object value) {
73      return value == null ? 0 : ((Map<?, ?>) value).size();
74    }
75  
76    public MapPropertyInfo(
77        @NonNull IBoundNamedModelInstance property) {
78      super(property);
79    }
80  
81    @Override
82    public boolean isJsonKeyRequired() {
83      return true;
84    }
85  
86    @SuppressWarnings("null")
87    @NonNull
88    public Class<?> getKeyType() {
89      ParameterizedType actualType = (ParameterizedType) getProperty().getType();
90      // this is a Map so the first generic type is the key
91      return (Class<?>) actualType.getActualTypeArguments()[0];
92    }
93  
94    @Override
95    public Class<?> getItemType() {
96      return getValueType();
97    }
98  
99    @SuppressWarnings("null")
100   @NonNull
101   public Class<?> getValueType() {
102     ParameterizedType actualType = (ParameterizedType) getProperty().getType();
103     // this is a Map so the second generic type is the value
104     return (Class<?>) actualType.getActualTypeArguments()[1];
105   }
106 
107   @Override
108   public IPropertyCollector newPropertyCollector() {
109     return new MapPropertyCollector();
110   }
111 
112   @Override
113   public void readValues(IPropertyCollector collector, Object parentInstance, IJsonParsingContext context)
114       throws IOException {
115     @SuppressWarnings("resource") // not owned
116     JsonParser jsonParser = context.getReader(); // NOPMD - intentional
117 
118     // A map value is always wrapped in a START_OBJECT, since fields are used for
119     // the keys
120     JsonUtil.assertAndAdvance(jsonParser, JsonToken.START_OBJECT);
121 
122     // process all map items
123     while (!JsonToken.END_OBJECT.equals(jsonParser.currentToken())) {
124 
125       // a map item will always start with a FIELD_NAME, since this represents the key
126       JsonUtil.assertCurrent(jsonParser, JsonToken.FIELD_NAME);
127 
128       Object value = getProperty().getDataTypeHandler().readItem(parentInstance, context);
129       collector.add(value);
130 
131       // the next item will be a FIELD_NAME, or we will encounter an END_OBJECT if all
132       // items have been
133       // read
134       JsonUtil.assertCurrent(jsonParser, JsonToken.FIELD_NAME, JsonToken.END_OBJECT);
135     }
136 
137     // A map value will always end with an end object, which needs to be consumed
138     JsonUtil.assertAndAdvance(jsonParser, JsonToken.END_OBJECT);
139   }
140 
141   @Override
142   public boolean readValues(IPropertyCollector collector, Object parentInstance, StartElement start,
143       IXmlParsingContext context) throws IOException, XMLStreamException {
144     QName qname = getProperty().getXmlQName();
145     XMLEventReader2 eventReader = context.getReader();
146 
147     // consume extra whitespace between elements
148     XmlEventUtil.skipWhitespace(eventReader);
149 
150     boolean handled = false;
151     XMLEvent event;
152     while ((event = eventReader.peek()).isStartElement() && qname.equals(event.asStartElement().getName())) {
153 
154       // Consume the start element
155       Object value = context.readModelInstanceValue(getProperty(), parentInstance, start);
156       if (value != null) {
157         collector.add(value);
158         handled = true;
159       }
160 
161       // consume extra whitespace between elements
162       XmlEventUtil.skipWhitespace(eventReader);
163     }
164 
165     return handled;
166   }
167 
168   @Override
169   public void writeValues(Object value, QName parentName, IXmlWritingContext context)
170       throws XMLStreamException, IOException {
171     IBoundNamedModelInstance property = getProperty();
172     @SuppressWarnings("unchecked") Map<String, ? extends Object> items = (Map<String, ? extends Object>) value;
173     for (Object item : items.values()) {
174       context.writeInstanceValue(property, ObjectUtils.notNull(item), parentName);
175     }
176   }
177 
178   @Override
179   public void writeValues(Object parentInstance, IJsonWritingContext context) throws IOException {
180     for (Object targetObject : getItemsFromParentInstance(parentInstance)) {
181       assert targetObject != null;
182       getProperty().getDataTypeHandler().writeItem(targetObject, context);
183     }
184   }
185 
186   @Override
187   public boolean isValueSet(Object parentInstance) throws IOException {
188     Collection<? extends Object> items = getItemsFromParentInstance(parentInstance);
189     return !items.isEmpty();
190   }
191 
192   @Override
193   public void copy(@NonNull Object fromInstance, @NonNull Object toInstance, @NonNull IPropertyCollector collector)
194       throws BindingException {
195     IBoundNamedModelInstance property = getProperty();
196 
197     for (Object item : getItemsFromParentInstance(fromInstance)) {
198       collector.add(property.copyItem(ObjectUtils.requireNonNull(item), toInstance));
199     }
200   }
201 
202   public class MapPropertyCollector implements IPropertyCollector {
203     @NonNull
204     private final Map<String, Object> map = new LinkedHashMap<>(); // NOPMD - single threaded
205     @Nullable
206     private final IBoundFlagInstance jsonKey;
207 
208     protected MapPropertyCollector() {
209       IClassBinding classBinding = getProperty().getDataTypeHandler().getClassBinding();
210       this.jsonKey = classBinding == null ? null : classBinding.getJsonKeyFlagInstance();
211       if (this.jsonKey == null) {
212         throw new IllegalStateException("No JSON key found");
213       }
214     }
215 
216     protected IBoundFlagInstance getJsonKey() {
217       return jsonKey;
218     }
219 
220     @Override
221     public void add(Object item) {
222       assert item != null;
223 
224       // lookup the key
225       String key = getJsonKey().getValue(item).toString();
226       map.put(key, item);
227     }
228 
229     @Override
230     public void addAll(Collection<?> items) {
231       for (Object item : items) {
232         add(ObjectUtils.requireNonNull(item));
233       }
234     }
235 
236     @NonNull
237     @Override
238     @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "this is a data holder")
239     public Map<String, Object> getValue() {
240       return map;
241     }
242   }
243 }