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      /** Is {@code true} once the sink has been closed. */
58      private boolean isComplete;
59  
60      private boolean isTitle;
61  
62      /** Is {@code true} if the sink is currently populating entry data (i.e. metadata about the current entry is not completely captured yet) */
63      private boolean hasOpenEntry;
64  
65      /**
66       * @deprecated legacy constructor, use {@link #IndexingSink(Sink)} with {@link SinkAdapter} as argument and call {@link #getRootEntry()} to retrieve the index tree afterwards.
67       */
68      @Deprecated
69      public IndexingSink(IndexEntry rootEntry) {
70          this(rootEntry, new SinkAdapter());
71      }
72  
73      public IndexingSink(Sink delegate) {
74          this(new IndexEntry("index"), delegate);
75      }
76  
77      /**
78       * Default constructor.
79       */
80      private IndexingSink(IndexEntry rootEntry, Sink delegate) {
81          super(delegate);
82          this.rootEntry = rootEntry;
83          stack = new Stack<>();
84          stack.push(rootEntry);
85          usedIds = new HashMap<>();
86          usedIds.put(rootEntry.getId(), new AtomicInteger());
87          this.type = Type.UNKNOWN;
88      }
89  
90      /**
91       * This should only be called once the sink is closed.
92       * Before that the tree might not be complete.
93       * @return the tree of entries starting from the root
94       * @throws IllegalStateException in case the sink was not closed yet
95       */
96      public IndexEntry getRootEntry() {
97          if (!isComplete) {
98              throw new IllegalStateException(
99                      "The sink has not been closed yet, i.e. the index tree is not complete yet");
100         }
101         return rootEntry;
102     }
103     /**
104      * <p>Getter for the field <code>title</code>.</p>
105      * Shortcut for {@link #getRootEntry()} followed by {@link IndexEntry#getTitle()}.
106      *
107      * @return the title
108      */
109     public String getTitle() {
110         return rootEntry.getTitle();
111     }
112 
113     // ----------------------------------------------------------------------
114     // Sink Overrides
115     // ----------------------------------------------------------------------
116 
117     @Override
118     public void title(SinkEventAttributes attributes) {
119         isTitle = true;
120         super.title(attributes);
121     }
122 
123     @Override
124     public void title_() {
125         isTitle = false;
126         super.title_();
127     }
128 
129     @Override
130     public void section(int level, SinkEventAttributes attributes) {
131         super.section(level, attributes);
132         indexEntryComplete(); // make sure the previous entry is complete
133         this.type = IndexEntry.Type.fromSectionLevel(level);
134         pushNewEntry(type);
135     }
136 
137     @Override
138     public void section_(int level) {
139         indexEntryComplete(); // make sure the previous entry is complete
140         pop();
141         super.section_(level);
142     }
143 
144     @Override
145     public void sectionTitle_(int level) {
146         indexEntryComplete();
147         super.sectionTitle_(level);
148     }
149 
150     @Override
151     public void text(String text, SinkEventAttributes attributes) {
152         if (isTitle) {
153             rootEntry.setTitle(text);
154         } else {
155             switch (this.type) {
156                 case SECTION_1:
157                 case SECTION_2:
158                 case SECTION_3:
159                 case SECTION_4:
160                 case SECTION_5:
161                 case SECTION_6:
162                     // -----------------------------------------------------------------------
163                     // Sanitize the id. The most important step is to remove any blanks
164                     // -----------------------------------------------------------------------
165 
166                     // append text to current entry
167                     IndexEntry entry = stack.lastElement();
168 
169                     String title = entry.getTitle();
170                     if (title != null) {
171                         title += text;
172                     } else {
173                         title = text;
174                     }
175                     title = title.replaceAll("[\\r\\n]+", "");
176                     entry.setTitle(title);
177 
178                     setEntryId(entry, title);
179                     break;
180                     // Dunno how to handle others yet
181                 default:
182                     break;
183             }
184         }
185         super.text(text, attributes);
186     }
187 
188     @Override
189     public void anchor(String name, SinkEventAttributes attributes) {
190         parseAnchor(name);
191         super.anchor(name, attributes);
192     }
193 
194     private boolean parseAnchor(String name) {
195         switch (type) {
196             case SECTION_1:
197             case SECTION_2:
198             case SECTION_3:
199             case SECTION_4:
200             case SECTION_5:
201                 IndexEntry entry = stack.lastElement();
202                 entry.setAnchor(true);
203                 setEntryId(entry, name);
204                 break;
205             default:
206                 return false;
207         }
208         return true;
209     }
210 
211     private void setEntryId(IndexEntry entry, String id) {
212         if (entry.getId() != null) {
213             usedIds.remove(entry.getId());
214         }
215         entry.setId(getUniqueId(DoxiaUtils.encodeId(id)));
216     }
217 
218     /**
219      * Converts the given id into a unique one by potentially suffixing it with an index value.
220      *
221      * @param id
222      * @return the unique id
223      */
224     String getUniqueId(String id) {
225         final String uniqueId;
226 
227         if (usedIds.containsKey(id)) {
228             uniqueId = id + "_" + usedIds.get(id).incrementAndGet();
229         } else {
230             usedIds.put(id, new AtomicInteger());
231             uniqueId = id;
232         }
233         return uniqueId;
234     }
235 
236     void indexEntryComplete() {
237         if (!hasOpenEntry) {
238             return;
239         }
240         this.type = Type.UNKNOWN;
241         // remove buffering sink from pipeline
242         BufferingSink bufferingSink = BufferingSinkProxyFactory.castAsBufferingSink(getWrappedSink());
243         setWrappedSink(bufferingSink.getBufferedSink());
244 
245         onIndexEntry(stack.peek());
246 
247         // flush the buffer afterwards
248         bufferingSink.flush();
249         hasOpenEntry = false;
250     }
251 
252     /**
253      * Called at the beginning of each entry (once all metadata about it is collected).
254      * The events for the metadata are buffered and only flushed after this method was called.
255      * @param entry the newly collected entry
256      */
257     protected void onIndexEntry(IndexEntry entry) {}
258 
259     /**
260      * Creates and pushes a new IndexEntry onto the top of this stack.
261      */
262     private void pushNewEntry(Type type) {
263         IndexEntry entry = new IndexEntry(peek(), null, type);
264         stack.push(entry);
265         // now buffer everything till the next index metadata is complete
266         setWrappedSink(new BufferingSinkProxyFactory().createWrapper(getWrappedSink()));
267         hasOpenEntry = true;
268     }
269 
270     /**
271      * Pushes an IndexEntry onto the top of this stack.
272      *
273      * @param entry to put.
274      */
275     public void push(IndexEntry entry) {
276         stack.push(entry);
277     }
278 
279     /**
280      * Removes the IndexEntry at the top of this stack.
281      */
282     public void pop() {
283         stack.pop();
284     }
285 
286     /**
287      * <p>peek.</p>
288      *
289      * @return Looks at the IndexEntry at the top of this stack.
290      */
291     public IndexEntry peek() {
292         return stack.peek();
293     }
294 
295     @Override
296     public void close() {
297         super.close();
298         isComplete = true;
299     }
300 }