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 /** 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}