001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.maven.doxia.index;
020
021import java.util.HashMap;
022import java.util.Map;
023import java.util.Stack;
024import java.util.concurrent.atomic.AtomicInteger;
025
026import org.apache.maven.doxia.index.IndexEntry.Type;
027import org.apache.maven.doxia.sink.Sink;
028import org.apache.maven.doxia.sink.SinkEventAttributes;
029import org.apache.maven.doxia.sink.impl.BufferingSinkProxyFactory;
030import org.apache.maven.doxia.sink.impl.BufferingSinkProxyFactory.BufferingSink;
031import org.apache.maven.doxia.sink.impl.SinkAdapter;
032import org.apache.maven.doxia.util.DoxiaUtils;
033
034/**
035 * A sink wrapper for populating an index tree for particular elements in a document.
036 * Currently this only generates {@link IndexEntry} objects for sections.
037 *
038 * @author <a href="mailto:trygvis@inamo.no">Trygve Laugst&oslash;l</a>
039 * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
040 */
041public class IndexingSink extends org.apache.maven.doxia.sink.impl.SinkWrapper {
042
043    /** The current type. */
044    private Type type;
045
046    /** The stack. */
047    private final Stack<IndexEntry> stack;
048
049    /** A map containing all used ids of index entries as key and how often they are used as value
050     * (0-based, i.e. 0 means used 1 time). {@link AtomicInteger} is only used here as it implements
051     * a mutable integer (not for its atomicity).
052     */
053    private final Map<String, AtomicInteger> usedIds;
054
055    private final IndexEntry rootEntry;
056
057    /** Is {@code true} once the sink has been closed. */
058    private boolean isComplete;
059
060    private boolean isTitle;
061
062    /** Is {@code true} if the sink is currently populating entry data (i.e. metadata about the current entry is not completely captured yet) */
063    private boolean hasOpenEntry;
064
065    /**
066     * @deprecated legacy constructor, use {@link #IndexingSink(Sink)} with {@link SinkAdapter} as argument and call {@link #getRootEntry()} to retrieve the index tree afterwards.
067     */
068    @Deprecated
069    public IndexingSink(IndexEntry rootEntry) {
070        this(rootEntry, new SinkAdapter());
071    }
072
073    public IndexingSink(Sink delegate) {
074        this(new IndexEntry("index"), delegate);
075    }
076
077    /**
078     * Default constructor.
079     */
080    private IndexingSink(IndexEntry rootEntry, Sink delegate) {
081        super(delegate);
082        this.rootEntry = rootEntry;
083        stack = new Stack<>();
084        stack.push(rootEntry);
085        usedIds = new HashMap<>();
086        usedIds.put(rootEntry.getId(), new AtomicInteger());
087        this.type = Type.UNKNOWN;
088    }
089
090    /**
091     * This should only be called once the sink is closed.
092     * Before that the tree might not be complete.
093     * @return the tree of entries starting from the root
094     * @throws IllegalStateException in case the sink was not closed yet
095     */
096    public IndexEntry getRootEntry() {
097        if (!isComplete) {
098            throw new IllegalStateException(
099                    "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}