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
100    /**
101     * The type of the entry, one of the types defined by {@link IndexingSink}
102     */
103    private final Type type;
104
105    /**
106     * Constructor for root entry.
107     *
108     * @param newId The id. May be null.
109     */
110    public IndexEntry(String newId) {
111        this(null, newId);
112    }
113
114    /**
115     * Constructor.
116     *
117     * @param newParent The parent. May be null.
118     * @param newId     The id. May be null.
119     */
120    public IndexEntry(IndexEntry newParent, String newId) {
121        this(newParent, newId, Type.UNKNOWN);
122    }
123
124    /**
125     * Constructor.
126     *
127     * @param newParent The parent. May be null.
128     * @param newId     The id. May be null.
129     * @param type      The type. Cannot be null.
130     */
131    public IndexEntry(IndexEntry newParent, String newId, Type type) {
132        this.parent = newParent;
133        this.id = newId;
134
135        if (parent != null) {
136            parent.childEntries.add(this);
137        }
138        this.type = type;
139    }
140
141    /**
142     * Returns the parent entry.
143     *
144     * @return the parent entry.
145     */
146    public IndexEntry getParent() {
147        return parent;
148    }
149
150    /**
151     * Returns the id.
152     *
153     * @return the id.
154     */
155    public String getId() {
156        return id;
157    }
158
159    /**
160     * Set the id.
161     *
162     * @param id the id
163     * @since 1.1.2
164     */
165    protected void setId(String id) {
166        this.id = id;
167    }
168
169    /**
170     * Returns the type of this entry. Is one of the types defined by {@link IndexingSink}.
171     * @return the type of this entry
172     * @since 2.0.0
173     */
174    public Type getType() {
175        return type;
176    }
177
178    /** Set if the entry's id already has an anchor in the underlying document.
179     *
180     * @param hasAnchor {@code true} if the id already has an anchor.
181     * @since 2.0.0
182     */
183    public void setAnchor(boolean hasAnchor) {
184        this.hasAnchor = hasAnchor;
185    }
186
187    /**
188     * Returns if the entry's id already has an anchor in the underlying document.
189     * @return {@code true} if the id already has an anchor otherwise {@code false}.
190     *
191     * @since 2.0.0
192     */
193    public boolean hasAnchor() {
194        return hasAnchor;
195    }
196
197    /**
198     * Returns the title.
199     *
200     * @return the title.
201     */
202    public String getTitle() {
203        return title;
204    }
205
206    /**
207     * Sets the title.
208     *
209     * @param newTitle the title.
210     */
211    public void setTitle(String newTitle) {
212        this.title = newTitle;
213    }
214
215    /**
216     * Returns an unmodifiableList of the child entries.
217     *
218     * @return child entries.
219     */
220    public List<IndexEntry> getChildEntries() {
221        return Collections.unmodifiableList(childEntries);
222    }
223
224    /**
225     * Sets the child entries or creates a new ArrayList if entries == null.
226     *
227     * @param entries the entries.
228     */
229    public void setChildEntries(List<IndexEntry> entries) {
230        if (entries == null) {
231            childEntries = new ArrayList<>();
232        }
233
234        this.childEntries = entries;
235    }
236
237    // -----------------------------------------------------------------------
238    // Utils
239    // -----------------------------------------------------------------------
240
241    /**
242     * Returns the next entry.
243     *
244     * @return the next entry, or null if there is none.
245     */
246    public IndexEntry getNextEntry() {
247        if (parent == null) {
248            return null;
249        }
250
251        List<IndexEntry> entries = parent.getChildEntries();
252
253        int index = entries.indexOf(this);
254
255        if (index + 1 >= entries.size()) {
256            return null;
257        }
258
259        return entries.get(index + 1);
260    }
261
262    /**
263     * Returns the previous entry.
264     *
265     * @return the previous entry, or null if there is none.
266     */
267    public IndexEntry getPrevEntry() {
268        if (parent == null) {
269            return null;
270        }
271
272        List<IndexEntry> entries = parent.getChildEntries();
273
274        int index = entries.indexOf(this);
275
276        if (index == 0) {
277            return null;
278        }
279
280        return entries.get(index - 1);
281    }
282
283    /**
284     * Returns the first entry.
285     *
286     * @return the first entry, or null if there is none.
287     */
288    public IndexEntry getFirstEntry() {
289        List<IndexEntry> entries = getChildEntries();
290
291        if (entries.size() == 0) {
292            return null;
293        }
294
295        return entries.get(0);
296    }
297
298    /**
299     * Returns the last entry.
300     *
301     * @return the last entry, or null if there is none.
302     */
303    public IndexEntry getLastEntry() {
304        List<IndexEntry> entries = getChildEntries();
305
306        if (entries.size() == 0) {
307            return null;
308        }
309
310        return entries.get(entries.size() - 1);
311    }
312
313    /**
314     * Returns the root entry.
315     *
316     * @return the root entry, or null if there is none.
317     */
318    public IndexEntry getRootEntry() {
319        List<IndexEntry> entries = getChildEntries();
320
321        if (entries.size() == 0) {
322            return null;
323        } else if (entries.size() > 1) {
324            throw new IllegalStateException("This index has more than one root entry");
325        } else {
326            return entries.get(0);
327        }
328    }
329
330    // -----------------------------------------------------------------------
331    // Object Overrides
332    // -----------------------------------------------------------------------
333
334    /**
335     * Returns a string representation of the object.
336     *
337     * @return Returns a string representation of all objects
338     */
339    public String toString() {
340        return toString(0);
341    }
342
343    /**
344     * Returns a string representation of all objects to the given depth.
345     *
346     * @param depth The depth to descent to.
347     * @return A string.
348     */
349    public String toString(int depth) {
350        StringBuilder message = new StringBuilder();
351
352        message.append("Id: ").append(id);
353
354        if (title != null && !title.isEmpty()) {
355            message.append(", title: ").append(title);
356        }
357
358        message.append(Markup.EOL);
359
360        StringBuilder indent = new StringBuilder();
361
362        for (int i = 0; i < depth; i++) {
363            indent.append(" ");
364        }
365
366        for (IndexEntry entry : getChildEntries()) {
367            message.append(indent).append(entry.toString(depth + 1));
368        }
369
370        return message.toString();
371    }
372
373    @Override
374    public int hashCode() {
375        return Objects.hash(childEntries, hasAnchor, id, parent, title, type);
376    }
377
378    @Override
379    public boolean equals(Object obj) {
380        if (this == obj) {
381            return true;
382        }
383        if (obj == null) {
384            return false;
385        }
386        if (getClass() != obj.getClass()) {
387            return false;
388        }
389        IndexEntry other = (IndexEntry) obj;
390        return Objects.equals(childEntries, other.childEntries)
391                && hasAnchor == other.hasAnchor
392                && Objects.equals(id, other.id)
393                && Objects.equals(parent, other.parent)
394                && Objects.equals(title, other.title)
395                && type == other.type;
396    }
397}