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.ArrayList;
022import java.util.Arrays;
023import java.util.Collections;
024import java.util.List;
025import java.util.Objects;
026
027import org.apache.maven.doxia.markup.Markup;
028import org.apache.maven.doxia.sink.Sink;
029
030/**
031 * Representing the index tree within a document with the most important metadata per entry.
032 * Currently this only contains entries for sections, but in the future may be extended, therefore it
033 * is recommended to use {@link #getType()} to filter out irrelevant entries.
034 *
035 * @author <a href="mailto:trygvis@inamo.no">Trygve Laugst&oslash;l</a>
036 */
037public class IndexEntry {
038    /**
039     * The parent entry.
040     */
041    private final IndexEntry parent;
042
043    /**
044     * The id of the entry.
045     */
046    private String id;
047
048    /**
049     * true if there is already an anchor for this
050     */
051    private boolean hasAnchor;
052
053    /**
054     * The entry title.
055     */
056    private String title;
057
058    /**
059     * The child entries.
060     */
061    private List<IndexEntry> childEntries = new ArrayList<>();
062
063    public enum Type {
064        /**
065         * Used for unknown types but also for the root entry
066         */
067        UNKNOWN(),
068        SECTION_1(Sink.SECTION_LEVEL_1),
069        SECTION_2(Sink.SECTION_LEVEL_2),
070        SECTION_3(Sink.SECTION_LEVEL_3),
071        SECTION_4(Sink.SECTION_LEVEL_4),
072        SECTION_5(Sink.SECTION_LEVEL_5),
073        SECTION_6(Sink.SECTION_LEVEL_6),
074        DEFINED_TERM(),
075        FIGURE(),
076        TABLE();
077
078        private final int sectionLevel;
079
080        Type() {
081            this(-1);
082        }
083
084        Type(int sectionLevel) {
085            this.sectionLevel = sectionLevel;
086        }
087
088        static Type fromSectionLevel(int level) {
089            if (level < Sink.SECTION_LEVEL_1 || level > Sink.SECTION_LEVEL_6) {
090                throw new IllegalArgumentException("Level must be between " + Sink.SECTION_LEVEL_1 + " and "
091                        + Sink.SECTION_LEVEL_6 + " but is " + level);
092            }
093            return Arrays.stream(Type.values())
094                    .filter(t -> level == t.sectionLevel)
095                    .findAny()
096                    .orElseThrow(() -> new IllegalStateException("Could not find enum for sectionLevel " + level));
097        }
098
099        public boolean isSection() {
100            return sectionLevel >= 1;
101        }
102    }
103
104    /**
105     * The type of the entry, one of the types defined by {@link IndexingSink}
106     */
107    private final Type type;
108
109    /**
110     * Constructor for root entry.
111     *
112     * @param newId The id. May be null.
113     */
114    public IndexEntry(String newId) {
115        this(null, newId);
116    }
117
118    /**
119     * Constructor.
120     *
121     * @param newParent The parent. May be null.
122     * @param newId     The id. May be null.
123     */
124    public IndexEntry(IndexEntry newParent, String newId) {
125        this(newParent, newId, Type.UNKNOWN);
126    }
127
128    /**
129     * Constructor.
130     *
131     * @param newParent The parent. May be null.
132     * @param newId     The id. May be null.
133     * @param type      The type. Cannot be null.
134     */
135    public IndexEntry(IndexEntry newParent, String newId, Type type) {
136        this.parent = newParent;
137        this.id = newId;
138
139        if (parent != null) {
140            parent.childEntries.add(this);
141        }
142        this.type = type;
143    }
144
145    /**
146     * Returns the parent entry.
147     *
148     * @return the parent entry.
149     */
150    public IndexEntry getParent() {
151        return parent;
152    }
153
154    /**
155     * Returns the id.
156     *
157     * @return the id.
158     */
159    public String getId() {
160        return id;
161    }
162
163    /**
164     * Returns if the entry has an id.
165     * @return {@code true} if the entry has a valid id, otherwise it can be considered invalid/empty.
166     */
167    public boolean hasId() {
168        return id != null;
169    }
170
171    /**
172     * Set the id.
173     *
174     * @param id the id
175     * @since 1.1.2
176     */
177    protected void setId(String id) {
178        this.id = id;
179    }
180
181    /**
182     * Returns the type of this entry. Is one of the types defined by {@link IndexingSink}.
183     * @return the type of this entry
184     * @since 2.0.0
185     */
186    public Type getType() {
187        return type;
188    }
189
190    /** Set if the entry's id already has an anchor in the underlying document.
191     *
192     * @param hasAnchor {@code true} if the id already has an anchor.
193     * @since 2.0.0
194     */
195    public void setAnchor(boolean hasAnchor) {
196        this.hasAnchor = hasAnchor;
197    }
198
199    /**
200     * Returns if the entry's id already has an anchor in the underlying document.
201     * @return {@code true} if the id already has an anchor otherwise {@code false}.
202     *
203     * @since 2.0.0
204     */
205    public boolean hasAnchor() {
206        return hasAnchor;
207    }
208
209    /**
210     * Returns the title.
211     *
212     * @return the title (may be {@code null}).
213     */
214    public String getTitle() {
215        return title;
216    }
217
218    /**
219     * Sets the title.
220     *
221     * @param newTitle the title.
222     */
223    public void setTitle(String newTitle) {
224        this.title = newTitle;
225    }
226
227    /**
228     * Returns an unmodifiableList of the child entries.
229     *
230     * @return child entries.
231     */
232    public List<IndexEntry> getChildEntries() {
233        return Collections.unmodifiableList(childEntries);
234    }
235
236    /**
237     * Sets the child entries or creates a new ArrayList if entries == null.
238     *
239     * @param entries the entries.
240     */
241    public void setChildEntries(List<IndexEntry> entries) {
242        if (entries == null) {
243            childEntries = new ArrayList<>();
244        }
245
246        this.childEntries = entries;
247    }
248
249    // -----------------------------------------------------------------------
250    // Utils
251    // -----------------------------------------------------------------------
252
253    /**
254     * Returns the next entry.
255     *
256     * @return the next entry, or null if there is none.
257     */
258    public IndexEntry getNextEntry() {
259        if (parent == null) {
260            return null;
261        }
262
263        List<IndexEntry> entries = parent.getChildEntries();
264
265        int index = entries.indexOf(this);
266
267        if (index + 1 >= entries.size()) {
268            return null;
269        }
270
271        return entries.get(index + 1);
272    }
273
274    /**
275     * Returns the previous entry.
276     *
277     * @return the previous entry, or null if there is none.
278     */
279    public IndexEntry getPrevEntry() {
280        if (parent == null) {
281            return null;
282        }
283
284        List<IndexEntry> entries = parent.getChildEntries();
285
286        int index = entries.indexOf(this);
287
288        if (index == 0) {
289            return null;
290        }
291
292        return entries.get(index - 1);
293    }
294
295    /**
296     * Returns the first entry.
297     *
298     * @return the first entry, or null if there is none.
299     */
300    public IndexEntry getFirstEntry() {
301        List<IndexEntry> entries = getChildEntries();
302
303        if (entries.size() == 0) {
304            return null;
305        }
306
307        return entries.get(0);
308    }
309
310    /**
311     * Returns the last entry.
312     *
313     * @return the last entry, or null if there is none.
314     */
315    public IndexEntry getLastEntry() {
316        List<IndexEntry> entries = getChildEntries();
317
318        if (entries.size() == 0) {
319            return null;
320        }
321
322        return entries.get(entries.size() - 1);
323    }
324
325    /**
326     * Returns the root entry.
327     *
328     * @return the root entry, or null if there is none.
329     */
330    public IndexEntry getRootEntry() {
331        List<IndexEntry> entries = getChildEntries();
332
333        if (entries.size() == 0) {
334            return null;
335        } else if (entries.size() > 1) {
336            throw new IllegalStateException("This index has more than one root entry");
337        } else {
338            return entries.get(0);
339        }
340    }
341
342    // -----------------------------------------------------------------------
343    // Object Overrides
344    // -----------------------------------------------------------------------
345
346    /**
347     * Returns a string representation of the object.
348     *
349     * @return Returns a string representation of all objects
350     */
351    public String toString() {
352        return toString(0);
353    }
354
355    /**
356     * Returns a string representation of all objects to the given depth.
357     *
358     * @param depth The depth to descent to.
359     * @return A string.
360     */
361    public String toString(int depth) {
362        StringBuilder message = new StringBuilder();
363
364        message.append("Id: ").append(id);
365
366        if (title != null && !title.isEmpty()) {
367            message.append(", title: ").append(title);
368        }
369
370        message.append(Markup.EOL);
371
372        StringBuilder indent = new StringBuilder();
373
374        for (int i = 0; i < depth; i++) {
375            indent.append(" ");
376        }
377
378        for (IndexEntry entry : getChildEntries()) {
379            message.append(indent).append(entry.toString(depth + 1));
380        }
381
382        return message.toString();
383    }
384
385    @Override
386    public int hashCode() {
387        return Objects.hash(childEntries, hasAnchor, id, parent, title, type);
388    }
389
390    @Override
391    public boolean equals(Object obj) {
392        if (this == obj) {
393            return true;
394        }
395        if (obj == null) {
396            return false;
397        }
398        if (getClass() != obj.getClass()) {
399            return false;
400        }
401        IndexEntry other = (IndexEntry) obj;
402        return Objects.equals(childEntries, other.childEntries)
403                && hasAnchor == other.hasAnchor
404                && Objects.equals(id, other.id)
405                && Objects.equals(parent, other.parent)
406                && Objects.equals(title, other.title)
407                && type == other.type;
408    }
409}