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