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    private boolean isComplete;
058    private boolean isTitle;
059    /**
060     * @deprecated legacy constructor, use {@link #IndexingSink(Sink)} with {@link SinkAdapter} as argument and call {@link #getRootEntry()} to retrieve the index tree afterwards.
061     */
062    @Deprecated
063    public IndexingSink(IndexEntry rootEntry) {
064        this(rootEntry, new SinkAdapter());
065    }
066
067    public IndexingSink(Sink delegate) {
068        this(new IndexEntry("index"), delegate);
069    }
070
071    /**
072     * Default constructor.
073     */
074    private IndexingSink(IndexEntry rootEntry, Sink delegate) {
075        super(delegate);
076        this.rootEntry = rootEntry;
077        stack = new Stack<>();
078        stack.push(rootEntry);
079        usedIds = new HashMap<>();
080        usedIds.put(rootEntry.getId(), new AtomicInteger());
081        this.type = Type.UNKNOWN;
082    }
083
084    /**
085     * This should only be called once the sink is closed.
086     * Before that the tree might not be complete.
087     * @return the tree of entries starting from the root
088     * @throws IllegalStateException in case the sink was not closed yet
089     */
090    public IndexEntry getRootEntry() {
091        if (!isComplete) {
092            throw new IllegalStateException(
093                    "The sink has not been closed yet, i.e. the index tree is not complete yet");
094        }
095        return rootEntry;
096    }
097    /**
098     * <p>Getter for the field <code>title</code>.</p>
099     * 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}