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ø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 }