View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.doxia.index;
20  
21  import java.util.HashMap;
22  import java.util.Map;
23  import java.util.Stack;
24  import java.util.concurrent.atomic.AtomicInteger;
25  
26  import org.apache.maven.doxia.index.IndexEntry.Type;
27  import org.apache.maven.doxia.sink.Sink;
28  import org.apache.maven.doxia.sink.SinkEventAttributes;
29  import org.apache.maven.doxia.sink.impl.BufferingSinkProxyFactory;
30  import org.apache.maven.doxia.sink.impl.BufferingSinkProxyFactory.BufferingSink;
31  import org.apache.maven.doxia.sink.impl.SinkAdapter;
32  import org.apache.maven.doxia.util.DoxiaUtils;
33  
34  /**
35   * A sink wrapper for populating an index tree for particular elements in a document.
36   * Currently this only generates {@link IndexEntry} objects for sections.
37   *
38   * @author <a href="mailto:trygvis@inamo.no">Trygve Laugst&oslash;l</a>
39   * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
40   */
41  public class IndexingSink extends org.apache.maven.doxia.sink.impl.SinkWrapper {
42  
43      /** The current type. */
44      private Type type;
45  
46      /** The stack. */
47      private final Stack<IndexEntry> stack;
48  
49      /** A map containing all used ids of index entries as key and how often they are used as value
50       * (0-based, i.e. 0 means used 1 time). {@link AtomicInteger} is only used here as it implements
51       * a mutable integer (not for its atomicity).
52       */
53      private final Map<String, AtomicInteger> usedIds;
54  
55      private final IndexEntry rootEntry;
56  
57      private boolean isComplete;
58      private boolean isTitle;
59      /**
60       * @deprecated legacy constructor, use {@link #IndexingSink(Sink)} with {@link SinkAdapter} as argument and call {@link #getRootEntry()} to retrieve the index tree afterwards.
61       */
62      @Deprecated
63      public IndexingSink(IndexEntry rootEntry) {
64          this(rootEntry, new SinkAdapter());
65      }
66  
67      public IndexingSink(Sink delegate) {
68          this(new IndexEntry("index"), delegate);
69      }
70  
71      /**
72       * Default constructor.
73       */
74      private IndexingSink(IndexEntry rootEntry, Sink delegate) {
75          super(delegate);
76          this.rootEntry = rootEntry;
77          stack = new Stack<>();
78          stack.push(rootEntry);
79          usedIds = new HashMap<>();
80          usedIds.put(rootEntry.getId(), new AtomicInteger());
81          this.type = Type.UNKNOWN;
82      }
83  
84      /**
85       * This should only be called once the sink is closed.
86       * Before that the tree might not be complete.
87       * @return the tree of entries starting from the root
88       * @throws IllegalStateException in case the sink was not closed yet
89       */
90      public IndexEntry getRootEntry() {
91          if (!isComplete) {
92              throw new IllegalStateException(
93                      "The sink has not been closed yet, i.e. the index tree is not complete yet");
94          }
95          return rootEntry;
96      }
97      /**
98       * <p>Getter for the field <code>title</code>.</p>
99       * Shortcut for {@link #getRootEntry()} followed by {@link IndexEntry#getTitle()}.
100      *
101      * @return the title
102      */
103     public String getTitle() {
104         return rootEntry.getTitle();
105     }
106 
107     // ----------------------------------------------------------------------
108     // Sink Overrides
109     // ----------------------------------------------------------------------
110 
111     @Override
112     public void title(SinkEventAttributes attributes) {
113         isTitle = true;
114         super.title(attributes);
115     }
116 
117     @Override
118     public void title_() {
119         isTitle = false;
120         super.title_();
121     }
122 
123     @Override
124     public void section(int level, SinkEventAttributes attributes) {
125         super.section(level, attributes);
126         this.type = IndexEntry.Type.fromSectionLevel(level);
127         pushNewEntry(type);
128     }
129 
130     @Override
131     public void section_(int level) {
132         pop();
133         super.section_(level);
134     }
135 
136     @Override
137     public void sectionTitle_(int level) {
138         indexEntryComplete();
139         super.sectionTitle_(level);
140     }
141 
142     @Override
143     public void text(String text, SinkEventAttributes attributes) {
144         if (isTitle) {
145             rootEntry.setTitle(text);
146             return;
147         }
148         switch (this.type) {
149             case SECTION_1:
150             case SECTION_2:
151             case SECTION_3:
152             case SECTION_4:
153             case SECTION_5:
154                 // -----------------------------------------------------------------------
155                 // Sanitize the id. The most important step is to remove any blanks
156                 // -----------------------------------------------------------------------
157 
158                 // append text to current entry
159                 IndexEntry entry = stack.lastElement();
160 
161                 String title = entry.getTitle() + text;
162                 title = title.replaceAll("[\\r\\n]+", "");
163                 entry.setTitle(title);
164 
165                 setEntryId(entry, title);
166                 break;
167                 // Dunno how to handle others yet
168             default:
169                 break;
170         }
171         super.text(text, attributes);
172     }
173 
174     @Override
175     public void anchor(String name, SinkEventAttributes attributes) {
176         parseAnchor(name);
177         super.anchor(name, attributes);
178     }
179 
180     private boolean parseAnchor(String name) {
181         switch (type) {
182             case SECTION_1:
183             case SECTION_2:
184             case SECTION_3:
185             case SECTION_4:
186             case SECTION_5:
187                 IndexEntry entry = stack.lastElement();
188                 entry.setAnchor(true);
189                 setEntryId(entry, name);
190                 break;
191             default:
192                 return false;
193         }
194         return true;
195     }
196 
197     private void setEntryId(IndexEntry entry, String id) {
198         if (entry.getId() != null) {
199             usedIds.remove(entry.getId());
200         }
201         entry.setId(getUniqueId(DoxiaUtils.encodeId(id)));
202     }
203 
204     /**
205      * Converts the given id into a unique one by potentially suffixing it with an index value.
206      *
207      * @param id
208      * @return the unique id
209      */
210     String getUniqueId(String id) {
211         final String uniqueId;
212 
213         if (usedIds.containsKey(id)) {
214             uniqueId = id + "_" + usedIds.get(id).incrementAndGet();
215         } else {
216             usedIds.put(id, new AtomicInteger());
217             uniqueId = id;
218         }
219         return uniqueId;
220     }
221 
222     void indexEntryComplete() {
223         this.type = Type.UNKNOWN;
224         // remove buffering sink from pipeline
225         BufferingSink bufferingSink = BufferingSinkProxyFactory.castAsBufferingSink(getWrappedSink());
226         setWrappedSink(bufferingSink.getBufferedSink());
227 
228         onIndexEntry(stack.peek());
229 
230         // flush the buffer afterwards
231         bufferingSink.flush();
232     }
233 
234     /**
235      * Called at the beginning of each entry (once all metadata about it is collected).
236      * The events for the metadata are buffered and only flushed after this method was called.
237      * @param entry the newly collected entry
238      */
239     protected void onIndexEntry(IndexEntry entry) {}
240 
241     /**
242      * Creates and pushes a new IndexEntry onto the top of this stack.
243      */
244     private void pushNewEntry(Type type) {
245         IndexEntry entry = new IndexEntry(peek(), "", type);
246         entry.setTitle("");
247         stack.push(entry);
248         // now buffer everything till the next index metadata is complete
249         setWrappedSink(new BufferingSinkProxyFactory().createWrapper(getWrappedSink()));
250     }
251 
252     /**
253      * Pushes an IndexEntry onto the top of this stack.
254      *
255      * @param entry to put.
256      */
257     public void push(IndexEntry entry) {
258         stack.push(entry);
259     }
260 
261     /**
262      * Removes the IndexEntry at the top of this stack.
263      */
264     public void pop() {
265         stack.pop();
266     }
267 
268     /**
269      * <p>peek.</p>
270      *
271      * @return Looks at the IndexEntry at the top of this stack.
272      */
273     public IndexEntry peek() {
274         return stack.peek();
275     }
276 
277     @Override
278     public void close() {
279         super.close();
280         isComplete = true;
281     }
282 }