001/* 002 * Copyright (C) Christian Schulte, 2005-206 003 * All rights reserved. 004 * 005 * Redistribution and use in source and binary forms, with or without 006 * modification, are permitted provided that the following conditions 007 * are met: 008 * 009 * o Redistributions of source code must retain the above copyright 010 * notice, this list of conditions and the following disclaimer. 011 * 012 * o Redistributions in binary form must reproduce the above copyright 013 * notice, this list of conditions and the following disclaimer in 014 * the documentation and/or other materials provided with the 015 * distribution. 016 * 017 * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 018 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY 019 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 020 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, 021 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 022 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 023 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 024 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 025 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 026 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 027 * 028 * $JOMC: SectionEditor.java 4613 2012-09-22 10:07:08Z schulte $ 029 * 030 */ 031package org.jomc.util; 032 033import java.io.IOException; 034import java.text.MessageFormat; 035import java.util.HashMap; 036import java.util.Map; 037import java.util.ResourceBundle; 038import java.util.Stack; 039 040/** 041 * Interface to section based editing. 042 * <p>Section based editing is a two phase process of parsing the editor's input into a corresponding hierarchy of 043 * {@code Section} instances, followed by rendering the parsed sections to produce the output of the editor. Method 044 * {@code editLine} returns {@code null} during parsing and the output of the editor on end of input, rendered by 045 * calling method {@code getOutput}. Parsing is backed by methods {@code getSection} and {@code isSectionFinished}.</p> 046 * 047 * @author <a href="mailto:cs@schulte.it">Christian Schulte</a> 048 * @version $JOMC: SectionEditor.java 4613 2012-09-22 10:07:08Z schulte $ 049 * 050 * @see #edit(java.lang.String) 051 */ 052public class SectionEditor extends LineEditor 053{ 054 055 /** Marker indicating the start of a section. */ 056 private static final String DEFAULT_SECTION_START = "SECTION-START["; 057 058 /** Marker indicating the end of a section. */ 059 private static final String DEFAULT_SECTION_END = "SECTION-END"; 060 061 /** Stack of sections. */ 062 private Stack<Section> stack; 063 064 /** Mapping of section names to flags indicating presence of the section. */ 065 private final Map<String, Boolean> presenceFlags = new HashMap<String, Boolean>(); 066 067 /** Creates a new {@code SectionEditor} instance. */ 068 public SectionEditor() 069 { 070 this( null, null ); 071 } 072 073 /** 074 * Creates a new {@code SectionEditor} instance taking a string to use for separating lines. 075 * 076 * @param lineSeparator String to use for separating lines. 077 */ 078 public SectionEditor( final String lineSeparator ) 079 { 080 this( null, lineSeparator ); 081 } 082 083 /** 084 * Creates a new {@code SectionEditor} instance taking an editor to chain. 085 * 086 * @param editor The editor to chain. 087 */ 088 public SectionEditor( final LineEditor editor ) 089 { 090 this( editor, null ); 091 } 092 093 /** 094 * Creates a new {@code SectionEditor} instance taking an editor to chain and a string to use for separating lines. 095 * 096 * @param editor The editor to chain. 097 * @param lineSeparator String to use for separating lines. 098 */ 099 public SectionEditor( final LineEditor editor, final String lineSeparator ) 100 { 101 super( editor, lineSeparator ); 102 } 103 104 @Override 105 protected final String editLine( final String line ) throws IOException 106 { 107 if ( this.stack == null ) 108 { 109 final Section root = new Section(); 110 root.setMode( Section.MODE_HEAD ); 111 this.stack = new Stack<Section>(); 112 this.stack.push( root ); 113 } 114 115 Section current = this.stack.peek(); 116 String replacement = null; 117 118 if ( line != null ) 119 { 120 final Section child = this.getSection( line ); 121 122 if ( child != null ) 123 { 124 child.setStartingLine( line ); 125 child.setMode( Section.MODE_HEAD ); 126 127 if ( current.getMode() == Section.MODE_TAIL && current.getTailContent().length() > 0 ) 128 { 129 final Section s = new Section(); 130 s.getHeadContent().append( current.getTailContent() ); 131 current.getTailContent().setLength( 0 ); 132 current.getSections().add( s ); 133 current = s; 134 this.stack.push( current ); 135 } 136 137 current.getSections().add( child ); 138 current.setMode( Section.MODE_TAIL ); 139 this.stack.push( child ); 140 } 141 else if ( this.isSectionFinished( line ) ) 142 { 143 final Section s = this.stack.pop(); 144 s.setEndingLine( line ); 145 146 if ( this.stack.isEmpty() ) 147 { 148 this.stack = null; 149 throw new IOException( getMessage( "unexpectedEndOfSection", this.getLineNumber() ) ); 150 } 151 152 if ( this.stack.peek().getName() == null && this.stack.size() > 1 ) 153 { 154 this.stack.pop(); 155 } 156 } 157 else 158 { 159 switch ( current.getMode() ) 160 { 161 case Section.MODE_HEAD: 162 current.getHeadContent().append( line ).append( this.getLineSeparator() ); 163 break; 164 165 case Section.MODE_TAIL: 166 current.getTailContent().append( line ).append( this.getLineSeparator() ); 167 break; 168 169 default: 170 throw new AssertionError( current.getMode() ); 171 172 } 173 } 174 } 175 else 176 { 177 final Section root = this.stack.pop(); 178 179 if ( !this.stack.isEmpty() ) 180 { 181 this.stack = null; 182 throw new IOException( getMessage( "unexpectedEndOfFile", this.getLineNumber(), root.getName() ) ); 183 } 184 185 replacement = this.getOutput( root ); 186 this.stack = null; 187 } 188 189 return replacement; 190 } 191 192 /** 193 * Parses the given line to mark the start of a new section. 194 * 195 * @param line The line to parse or {@code null}. 196 * 197 * @return The section starting at {@code line} or {@code null}, if {@code line} does not mark the start of a 198 * section. 199 * 200 * @throws IOException if parsing fails. 201 */ 202 protected Section getSection( final String line ) throws IOException 203 { 204 Section s = null; 205 206 if ( line != null ) 207 { 208 final int markerIndex = line.indexOf( DEFAULT_SECTION_START ); 209 210 if ( markerIndex != -1 ) 211 { 212 final int startIndex = markerIndex + DEFAULT_SECTION_START.length(); 213 final int endIndex = line.indexOf( ']', startIndex ); 214 215 if ( endIndex == -1 ) 216 { 217 throw new IOException( getMessage( "sectionMarkerParseFailure", line, this.getLineNumber() ) ); 218 } 219 220 s = new Section(); 221 s.setName( line.substring( startIndex, endIndex ) ); 222 } 223 } 224 225 return s; 226 } 227 228 /** 229 * Parses the given line to mark the end of a section. 230 * 231 * @param line The line to parse or {@code null}. 232 * 233 * @return {@code true}, if {@code line} marks the end of a section; {@code false}, if {@code line} does not mark 234 * the end of a section. 235 * 236 * @throws IOException if parsing fails. 237 */ 238 protected boolean isSectionFinished( final String line ) throws IOException 239 { 240 return line != null && line.indexOf( DEFAULT_SECTION_END ) != -1; 241 } 242 243 /** 244 * Edits a section. 245 * <p>This method does not change any content by default. Overriding classes may use this method for editing 246 * sections prior to rendering.</p> 247 * 248 * @param section The section to edit. 249 * 250 * @throws NullPointerException if {@code section} is {@code null}. 251 * @throws IOException if editing fails. 252 */ 253 protected void editSection( final Section section ) throws IOException 254 { 255 if ( section == null ) 256 { 257 throw new NullPointerException( "section" ); 258 } 259 260 if ( section.getName() != null ) 261 { 262 this.presenceFlags.put( section.getName(), Boolean.TRUE ); 263 } 264 } 265 266 /** 267 * Edits a section recursively. 268 * 269 * @param section The section to edit recursively. 270 * 271 * @throws NullPointerException if {@code section} is {@code null}. 272 * @throws IOException if editing fails. 273 */ 274 private void editSections( final Section section ) throws IOException 275 { 276 if ( section == null ) 277 { 278 throw new NullPointerException( "section" ); 279 } 280 281 this.editSection( section ); 282 for ( int i = 0, s0 = section.getSections().size(); i < s0; i++ ) 283 { 284 this.editSections( section.getSections().get( i ) ); 285 } 286 } 287 288 /** 289 * Gets the output of the editor. 290 * <p>This method calls method {@code editSection()} for each section of the editor prior to rendering the sections 291 * to produce the output of the editor.</p> 292 * 293 * @param section The section to start rendering the editor's output with. 294 * 295 * @return The output of the editor. 296 * 297 * @throws NullPointerException if {@code section} is {@code null}. 298 * @throws IOException if editing or rendering fails. 299 */ 300 protected String getOutput( final Section section ) throws IOException 301 { 302 if ( section == null ) 303 { 304 throw new NullPointerException( "section" ); 305 } 306 307 this.presenceFlags.clear(); 308 this.editSections( section ); 309 return this.renderSections( section, new StringBuilder( 512 ) ).toString(); 310 } 311 312 /** 313 * Gets a flag indicating that the input of the editor contained a named section. 314 * 315 * @param sectionName The name of the section to test or {@code null}. 316 * 317 * @return {@code true}, if the input of the editor contained a section with name {@code sectionName}; 318 * {@code false}, if the input of the editor did not contain a section with name {@code sectionName}. 319 */ 320 public boolean isSectionPresent( final String sectionName ) 321 { 322 return sectionName != null && this.presenceFlags.get( sectionName ) != null 323 && this.presenceFlags.get( sectionName ).booleanValue(); 324 325 } 326 327 /** 328 * Appends the content of a given section to a given buffer. 329 * 330 * @param section The section to render. 331 * @param buffer The buffer to append the content of {@code section} to. 332 * 333 * @return {@code buffer} with content of {@code section} appended. 334 */ 335 private StringBuilder renderSections( final Section section, final StringBuilder buffer ) 336 { 337 if ( section.getStartingLine() != null ) 338 { 339 buffer.append( section.getStartingLine() ).append( this.getLineSeparator() ); 340 } 341 342 buffer.append( section.getHeadContent() ); 343 344 for ( int i = 0, s0 = section.getSections().size(); i < s0; i++ ) 345 { 346 this.renderSections( section.getSections().get( i ), buffer ); 347 } 348 349 buffer.append( section.getTailContent() ); 350 351 if ( section.getEndingLine() != null ) 352 { 353 buffer.append( section.getEndingLine() ).append( this.getLineSeparator() ); 354 } 355 356 return buffer; 357 } 358 359 private static String getMessage( final String key, final Object... arguments ) 360 { 361 return MessageFormat.format( ResourceBundle.getBundle( SectionEditor.class.getName(). 362 replace( '.', '/' ) ).getString( key ), arguments ); 363 364 } 365 366}