View Javadoc

1   /*
2    * Copyright (C) 2003-2006 Christian Siefkes <christian@siefkes.net>.
3    * Development of this software is supported by the German Research Society,
4    * Berlin-Brandenburg Graduate School in Distributed Information Systems
5    * (DFG grant no. GRK 316).
6    *
7    * This program is free software; you can redistribute it and/or modify
8    * it under the terms of the GNU General Public License as published by
9    * the Free Software Foundation; either version 2 of the License, or
10   * (at your option) any later version.
11   *
12   * This program is distributed in the hope that it will be useful,
13   * but WITHOUT ANY WARRANTY; without even the implied warranty of
14   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15   * GNU General Public License for more details.
16   *
17   * You should have received a copy of the GNU General Public License
18   * along with this program; if not, visit
19   * http://www.gnu.org/licenses/gpl.html or write to the Free Software
20   * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
21   */
22  package de.fu_berlin.ties.xml;
23  
24  import java.util.LinkedList;
25  import java.util.List;
26  import java.util.SortedMap;
27  
28  import org.apache.commons.collections.MultiHashMap;
29  import org.apache.commons.collections.MultiMap;
30  import org.apache.commons.collections.map.LinkedMap;
31  
32  /***
33   * A container used to manage the currently open start tags during XML
34   * adjustment. Adds stack-like push and pop functionality to the superclass
35   * and allows checking specifically for
36   * {@link de.fu_berlin.ties.xml.TagVariety#TENTATIVE tentative} tag.
37   *
38   * @author Christian Siefkes
39   * @version $Revision: 1.6 $, $Date: 2006/10/21 16:04:29 $, $Author: siefkes $
40   */
41  public class OpenTags extends TagContainer {
42  
43      /***
44       * A dummy object stored as map value where no value is required.
45       */
46      protected static final Object DUMMY = new Object();
47  
48      /***
49       * Contains all tentative tags (checking the variety at insertion
50       * time), mapping from tag name Strings to Collections of TagConstituents.
51       */
52      private final MultiMap tentativeTags = new MultiHashMap();
53  
54      /***
55       * Contains all non-tentative tags (checking the variety at insertion
56       * time), mapping from tag name Strings to Collections of TagConstituents.
57       */
58      private final MultiMap nonTentativeTags = new MultiHashMap();
59  
60      /***
61       * Additionally preserves order of insertion for all TagConstituents,
62       * for push/pop/peek operations. We would prefer to use an ordered
63       * <em>set</em>, but there is no "LinkedSet" class and
64       * {@link java.util.LinkedHashSet} doesn't provide fast access to the
65       * <em>last</em> element (only to the first ones). Thus we just store
66       * a {@link #DUMMY} reference in each value, effectively using the set
67       * part of this map.
68       */
69      private final LinkedMap tagSequence = new LinkedMap();
70  
71      /***
72       * Creates a new instance.
73       */
74      public OpenTags() {
75          super();
76      }
77  
78      /***
79       * Checks whether this instance contains at least one appearance of the
80       * specified tag that is not {@link TagVariety#TENTATIVE}.
81       *
82       * @param tagname the name of the tag to check
83       * @return <code>true</code> iff the container contains one or more
84       * non-tenatative (at insertion time) appearances of the specified tag
85       */
86      public boolean containsNonTentative(final String tagname) {
87          // check tentative tags entries for tag name
88          final List appearances = (List) nonTentativeTags.get(tagname);
89          return (appearances != null) && !appearances.isEmpty();
90      }
91  
92      /***
93       * Finds a tentative appearance of a tag, if any exists.
94       * If several tentative appearances exist, the <em>last</em> of them is
95       * returned.
96       *
97       * @param tagname the name of the tag to check
98       * @return the found apearance of the tag, or <code>null</code> if no
99       * matching appearance was found
100      */
101     public TagConstituent findTentativeTag(final String tagname) {
102         // check tentative tags entries for tag name
103         final List appearances = (List) tentativeTags.get(tagname);
104         final TagConstituent result;
105 
106         if ((appearances != null) && !appearances.isEmpty()) {
107             // return last entry
108             result =
109                 (TagConstituent) appearances.get(appearances.size() - 1);
110             // Log.TIES.info("Found tentative tag: " + result);
111         } else {
112             result = null;
113         }
114         return result;
115     }
116 
117     /***
118      * Checks whether a tag is the root tag (the first tag inserted into this
119      * container).
120      *
121      * @param tag the tag to check
122      * @return <code>true</code> if the tag is the root tag
123      */
124     public boolean isRoot(final TagConstituent tag) {
125         return (tag != null) && (tag == peekFirst());
126     }
127 
128     /***
129      * Returns the first tag appearance that was initially
130      * {@link #push(TagConstituent) push}ed into this container.
131      *
132      * @return the first undeleted tag pushed into this container,
133      * or <code>null</code> if the container is empty
134      */
135     protected TagConstituent peekFirst() {
136         return (tagSequence.isEmpty())
137             ? null : (TagConstituent) tagSequence.firstKey();
138     }
139 
140     /***
141      * Returns the last tag appearance that was
142      * {@link #push(TagConstituent) push}ed into this container.
143      *
144      * @return the last undeleted tag pushed into this container,
145      * or <code>null</code> if the container is empty
146      */
147     public TagConstituent peek() {
148         return (tagSequence.isEmpty())
149             ? null : (TagConstituent) tagSequence.lastKey();
150     }
151 
152     /***
153      * Removes and returns the last tag appearance that was
154      * {@link #push(TagConstituent) push}ed into this container.
155      *
156      * @return the removed tag
157      * @throws NullPointerException if the container is empty (i.e. a
158      * call to {@link #peek()} would return <code>null</code>)
159      */
160     public TagConstituent pop() throws NullPointerException {
161         final TagConstituent tagToRemove = peek();
162         final String tagname = tagToRemove.getName();
163         final Integer seriesNo = new Integer(tagToRemove.getMarkupSeriesNo());
164         tagSequence.remove(tagToRemove);
165 
166         // remove it from (non)tentative tags, if contained there
167         tentativeTags.remove(tagname, tagToRemove);
168         nonTentativeTags.remove(tagname, tagToRemove);
169 
170         // we know the map must exist
171         final SortedMap nestedMap = (SortedMap) getTagMap().get(tagname);
172 
173         // we know the list must exist and the tag must be the last entry
174         final LinkedList tagsInSeries =
175             (LinkedList) nestedMap.get(seriesNo);
176         final TagConstituent removedTag =
177             (TagConstituent) tagsInSeries.removeLast();
178 
179         if (tagsInSeries.isEmpty()) {
180             // inner list is now empty -- remove it
181             nestedMap.remove(seriesNo);
182             if (nestedMap.isEmpty()) {
183                 // nested map is now empty -- remove it too
184                 getTagMap().remove(tagname);
185             }
186         }
187 
188         if (removedTag != tagToRemove) {
189             // should never happen
190             throw new RuntimeException("Implementation error: "
191                 + tagToRemove + " and " + removedTag
192                 + " should be the same object, but the aren't!");
193         }
194         return tagToRemove;
195     }
196 
197     /***
198      * Removes and returns the last tag appearance that was
199      * {@link #push(TagConstituent) push}ed into this container. The variety of
200      * the removed tag is set to {@link TagVariety#REGULAR} (independently of
201      * its previous value).
202      *
203      * @return <code>true</code> if the tag was irregular and thus had to be
204      * regularized; <code>false</code> if it was a regular tag
205      * @throws NullPointerException if the container is empty (i.e. a
206      * call to {@link #peek()} would return <code>null</code>)
207      */
208     public boolean popAndRegularize() throws NullPointerException {
209         final TagConstituent popped = pop();
210         final boolean result;
211         if (popped.getVariety() != TagVariety.REGULAR) {
212             // adjust variety
213             popped.setVariety(TagVariety.REGULAR);
214             result = true;
215         } else {
216             result = false;
217         }
218         return result;
219     }
220 
221     /***
222      * Inserts an appearance of a tag into the container. The name of the
223      * tag is used as key; the whole constituent is appended to the list of
224      * appearances stored for this tag name. This implementation doesn't support
225      * inserting at the begin of af the list because it would hinder the stack
226      * operations. Instances of this class accept only start tags.
227      *
228      * @param tag the tag to insert
229      * @param appendAtEnd must always be <code>true</code>
230      * @throws IllegalArgumentException if the specified tag is not a start tag
231      * @throws UnsupportedOperationException if <code>appendAtEnd</code> is
232      * <code>false</code> (prepending is not supported)
233      */
234     public void push(final TagConstituent tag, final boolean appendAtEnd)
235             throws IllegalArgumentException, UnsupportedOperationException {
236         // check arguments
237         if (tag.getType() != TagConstituent.START_TAG) {
238             throw new IllegalArgumentException(
239                 "OpenTags allows only start tags, not tags of type "
240                     + tag.getType() + " (" + tag + ")");
241         }
242         if (!appendAtEnd) {
243             throw new UnsupportedOperationException("OpenTags.push cannot "
244                 + "handle prepending -- would destroy stack operations");
245         }
246 
247         // delegate to superclass
248         super.push(tag, appendAtEnd);
249 
250         // add tag to tag sequence
251         tagSequence.put(tag, DUMMY);
252 
253         // also add to tentative resp. non-tenative tags
254         if (tag.getVariety() == TagVariety.TENTATIVE) {
255             tentativeTags.put(tag.getName(), tag);
256         } else {
257             nonTentativeTags.put(tag.getName(), tag);
258         }
259     }
260 
261     /***
262      * Removes a single appearance of a tag from the container.
263      *
264      * @param tag the tag to remove
265      * @return <code>true</code> if the specified TagConstituent was
266      * removed successfully; <code>false</code> otherwise (the specified
267      * constituent wasn't found in the container)
268      */
269     public boolean remove(final TagConstituent tag) {
270         // remove from tentative resp. non-tenative tags, if contained there
271         // (we do this always for both because the tag variety could have been
272         // changed after the tag was added)
273         tentativeTags.remove(tag.getName(), tag);
274         nonTentativeTags.remove(tag.getName(), tag);
275 
276         // delegate to superclass
277         final boolean superResult = super.remove(tag);
278 
279         // also remove from tag sequence
280         return superResult && (tagSequence.remove(tag) == DUMMY);
281     }
282 
283 }