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&lt;String, Person&gt; directory = new HashMap&lt;String, Person&gt;();
025     *
026     *     // Getter and setter for the "directory" property
027     *     public Map&lt;String, Person&gt; getDirectory() {
028     *         return this.directory;
029     *     }
030     *     public void setDirectory(Map&lt;String, Person&gt; 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&lt;String, Person&gt; {
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&lt;DirectoryEntry&gt; 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     * &lt;binding package="com.example"&gt;
060     *
061     *     &lt;!-- Include XML mapping definition for a Person object (having type-name "person") --&gt;
062     *     &lt;include path="person.xml"/&gt;
063     *
064     *     &lt;!-- Define the XML mapping for one entry in the "directory" map --&gt;
065     *     &lt;mapping abstract="true" type-name="directory_entry" class="com.example.Company$DirectoryEntry"&gt;
066     *         &lt;value name="name" get-method="getKey" set-method="setKey" type="java.lang.String" style="attribute"/&gt;
067     *         &lt;structure name="Person" get-method="getValue" set-method="setValue" map-as="person"/&gt;
068     *     &lt;/mapping&gt;
069     *
070     *     &lt;!-- Define XML mapping for a Company object --&gt;
071     *     &lt;mapping abstract="true" type-name="company" class="com.example.Company"&gt;
072     *         &lt;collection name="Directory" item-type="com.example.Company$DirectoryEntry"
073     *           add-method="addDirectoryEntry" iter-method="iterateDirectoryEntries"&gt;
074     *             &lt;structure name="DirectoryEntry" map-as="directory_entry"/&gt;
075     *         &lt;/collection&gt;
076     *         &lt;!-- other properties... --&gt;
077     *     &lt;/mapping&gt;
078     * &lt;/binding&gt;
079     * </pre></blockquote>
080     *
081     * Then the resulting XML would end up looking something like this:
082     * <blockquote><pre>
083     * &lt;Company&gt;
084     *     &lt;Directory&gt;
085     *         &lt;DirectoryEntry name="George Washington"&gt;
086     *             &lt;Person&gt;
087     *                  &lt;!-- properties of George Washington... --&gt;
088     *             &lt;/Person&gt;
089     *         &lt;/DirectoryEntry&gt;
090     *         &lt;DirectoryEntry name="Betsy Ross"&gt;
091     *             &lt;Person&gt;
092     *                  &lt;!-- properties of Betsy Ross... --&gt;
093     *             &lt;/Person&gt;
094     *         &lt;/DirectoryEntry&gt;
095     *     &lt;/Directory&gt;
096     *     &lt;!-- other properties... --&gt;
097     * &lt;/Company&gt;
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