001
002 /*
003 * Copyright (C) 2011 Archie L. Cobbs. All rights reserved.
004 *
005 * $Id: MapEntry.java 274 2012-02-13 21:28:59Z archie.cobbs $
006 */
007
008 package org.dellroad.stuff.jibx;
009
010 import java.util.Iterator;
011 import java.util.Map;
012
013 import org.jibx.runtime.JiBXParseException;
014
015 /**
016 * Utility class that makes it slightly easier to model {@link Map} properties in JiBX.
017 * This class can be used to represent entries in the map, each of which is modeled in XML as a separate XML element.
018 *
019 * <p>
020 * For example, suppose you have a class {@code Company} and want to add a {@code directory} property that has
021 * type {@code Map<String, Person>}:
022 * <blockquote><pre>
023 * public class Company {
024 * private Map<String, Person> directory = new HashMap<String, Person>();
025 *
026 * // Getter and setter for the "directory" property
027 * public Map<String, Person> getDirectory() {
028 * return this.directory;
029 * }
030 * public void setDirectory(Map<String, Person> directory) {
031 * this.directory = directory;
032 * }
033 * }
034 * </pre></blockquote>
035 *
036 * <p>
037 * Because the JiBX binding process modifies class files, you first need to create your own subclass of {@link MapEntry}
038 * that can be modified. In this example, we'll use an inner class of {@code Company}. In addition, you also need to add
039 * JiBX "add-method" and "iter-method" helper methods. The resulting new code might look like this:
040 * <blockquote><pre>
041 * // JiBX holder for a single entry in the Directory map
042 * public static class DirectoryEntry extends MapEntry<String, Person> {
043 * }
044 *
045 * // JiBX "add-method" that adds a new entry to the directory
046 * void addDirectoryEntry(DirectoryEntry entry) throws JiBXParseException {
047 * MapEntry.add(this.directory, entry);
048 * }
049 *
050 * // JiBX "iter-method" that iterates all entries in the directory
051 * Iterator<DirectoryEntry> iterateDirectoryEntries() {
052 * return MapEntry.iterate(this.directory, DirectoryEntry.class);
053 * }
054 * </pre></blockquote>
055 *
056 * <p>
057 * Then in your JiBX binding definition, you would do something like this:
058 * <blockquote><pre>
059 * <binding package="com.example">
060 *
061 * <!-- Include XML mapping definition for a Person object (having type-name "person") -->
062 * <include path="person.xml"/>
063 *
064 * <!-- Define the XML mapping for one entry in the "directory" map -->
065 * <mapping abstract="true" type-name="directory_entry" class="com.example.Company$DirectoryEntry">
066 * <value name="name" get-method="getKey" set-method="setKey" type="java.lang.String" style="attribute"/>
067 * <structure name="Person" get-method="getValue" set-method="setValue" map-as="person"/>
068 * </mapping>
069 *
070 * <!-- Define XML mapping for a Company object -->
071 * <mapping abstract="true" type-name="company" class="com.example.Company">
072 * <collection name="Directory" item-type="com.example.Company$DirectoryEntry"
073 * add-method="addDirectoryEntry" iter-method="iterateDirectoryEntries">
074 * <structure name="DirectoryEntry" map-as="directory_entry"/>
075 * </collection>
076 * <!-- other properties... -->
077 * </mapping>
078 * </binding>
079 * </pre></blockquote>
080 *
081 * Then the resulting XML would end up looking something like this:
082 * <blockquote><pre>
083 * <Company>
084 * <Directory>
085 * <DirectoryEntry name="George Washington">
086 * <Person>
087 * <!-- properties of George Washington... -->
088 * </Person>
089 * </DirectoryEntry>
090 * <DirectoryEntry name="Betsy Ross">
091 * <Person>
092 * <!-- properties of Betsy Ross... -->
093 * </Person>
094 * </DirectoryEntry>
095 * </Directory>
096 * <!-- other properties... -->
097 * </Company>
098 * </pre></blockquote>
099 *
100 * <p>
101 * Note that during unmarshalling, the <code>Map</code> itself is not created; it is expected to already exist
102 * and be empty. This will be the case if you provide a field initializer as in the example above.
103 *
104 * <p>
105 * The map keys are not constrained to being simple values: for complex keys, just adjust the mapping for the
106 * {@code DirectoryEntry} structure accordingly.
107 */
108 public class MapEntry<K, V> {
109
110 private K key;
111 private V value;
112
113 /**
114 * Get this map entry's key.
115 */
116 public K getKey() {
117 return this.key;
118 }
119 public void setKey(K key) {
120 this.key = key;
121 }
122
123 /**
124 * Get this map entry's value.
125 */
126 public V getValue() {
127 return this.value;
128 }
129 public void setValue(V value) {
130 this.value = value;
131 }
132
133 /**
134 * Helper method intended to be used by a custom JiBX "iter-method".
135 * This method returns an iterator that iterates over all entries in the given map.
136 *
137 * @param <K> type of map keys
138 * @param <V> type of map values
139 * @param map map to iterate
140 * @param entryClass the subclass of {@link MapEntry} used for iterated elements; must have a default constructor
141 * @return map entry iterator
142 */
143 public static <K, V, E extends MapEntry<K, V>> Iterator<E> iterate(Map<K, V> map, final Class<E> entryClass) {
144 final Iterator<Map.Entry<K, V>> entryIterator = map.entrySet().iterator();
145 return new Iterator<E>() {
146
147 @Override
148 public boolean hasNext() {
149 return entryIterator.hasNext();
150 }
151
152 @Override
153 public E next() {
154 Map.Entry<K, V> entry = entryIterator.next();
155 E mapEntry;
156 try {
157 mapEntry = entryClass.newInstance();
158 } catch (Exception e) {
159 throw new RuntimeException("unexpected exception", e);
160 }
161 mapEntry.setKey(entry.getKey());
162 mapEntry.setValue(entry.getValue());
163 return mapEntry;
164 }
165
166 @Override
167 public void remove() {
168 entryIterator.remove();
169 }
170 };
171 }
172
173 /**
174 * Helper method intended to be used by a custom JiBX "add-method".
175 * If there is an existing entry with the same key, a {@link JiBXParseException} is thrown.
176 *
177 * @param map map to which to add an new entry
178 * @param entry new entry to add
179 * @throws JiBXParseException if the map already contains an entry with the given key
180 */
181 public static <K, V> void add(Map<K, V> map, MapEntry<? extends K, ? extends V> entry) throws JiBXParseException {
182 MapEntry.add(map, entry, false);
183 }
184
185 /**
186 * Helper method intended to be used by a custom JiBX "add-method".
187 *
188 * @param map map to which to add an new entry
189 * @param entry new entry to add
190 * @param allowDuplicate {@code true} to replace any existing entry having the same key,
191 * or {@code false} to throw a {@link JiBXParseException} if there is an existing entry
192 * @throws JiBXParseException if {@code allowDuplicate} is {@code false} and an entry
193 * with the same key already exists in {@code map}
194 */
195 public static <K, V> void add(Map<K, V> map, MapEntry<? extends K, ? extends V> entry, boolean allowDuplicate)
196 throws JiBXParseException {
197 K key = entry.getKey();
198 V value = entry.getValue();
199 if (!allowDuplicate && map.containsKey(key))
200 throw new JiBXParseException("duplicate key in map", "" + key);
201 map.put(key, value);
202 }
203 }
204