/*! ****************************************************************************** * * Pentaho Data Integration * * Copyright (C) 2002-2013 by Pentaho : http://www.pentaho.com * ******************************************************************************* * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * ******************************************************************************/ package org.pentaho.di.core.util; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.util.Collections; import java.util.Vector; import org.pentaho.di.core.logging.LogChannelInterface; public class SortedFileOutputStream extends FileOutputStream { /** Internal buffer to catch output. Before really writing output, the properties get sorted. */ private StringBuilder sb = null; /** Logger, for the few errors that may occur. */ private LogChannelInterface log = null; /** * CT * * @param file * @throws FileNotFoundException */ public SortedFileOutputStream( File file ) throws FileNotFoundException { super( file ); } /** * Setter * * @param log */ public void setLogger( LogChannelInterface log ) { this.log = log; } /** * Appending to internal StringBuilder, instead of immediately writing to the file */ @Override public void write( byte[] b, int off, int len ) throws IOException { if ( sb == null ) { sb = new StringBuilder(); } sb.append( new String( b, off, len ) ); } /** * Appending to internal StringBuilder, instead of immediately writing to the file */ @Override public void write( byte[] b ) throws IOException { if ( sb == null ) { sb = new StringBuilder(); } sb.append( new String( b ) ); } /** * Appending to internal StringBuilder, instead of immediately writing to the file */ @Override public void write( int b ) throws IOException { if ( sb == null ) { sb = new StringBuilder(); } sb.append( b ); } /** * Catch <code>flush</code> method, don't do nothing */ @Override public void flush() throws IOException { } /** * If internally stored content is available, sorting keys of content, then sending content to file. Then calling * {@link FileOutputStream#close()} method. */ @Override public void close() throws IOException { if ( sb == null || sb.length() == 0 ) { super.flush(); super.close(); } int[] iPos = new int[1]; iPos[0] = 0; String sLine = nextLine( iPos ); Vector<String> lines = new Vector<String>(); while ( sLine != null ) { // Length 0 -> do nothing if ( sLine.length() == 0 ) { sLine = nextLine( iPos ); continue; } // Character at first position is a '#' -> this is a comment if ( sLine.charAt( 0 ) == '#' ) { super.write( sLine.getBytes() ); sLine = nextLine( iPos ); continue; } // Get first occurrence of '=' character, that is not a position 0 and not // escaped by a '\\' int idx = sLine.indexOf( '=' ); if ( idx <= 0 ) { // '=' either does not exist or is at first position (that should never happen!). // Write line immediately log .logError( this.getClass().getName(), "Unexpected: '=' character not found or found at first position." ); super.write( sLine.getBytes() ); } else { while ( idx != -1 && sLine.charAt( idx - 1 ) == '\\' ) { idx = sLine.indexOf( '=', idx + 1 ); } if ( idx == -1 ) { log.logError( this.getClass().getName(), "Unexpected: No '=' character found that is not escaped by a '\\'." ); super.write( sLine.getBytes() ); } else { lines.add( sLine ); } } sLine = nextLine( iPos ); } Collections.sort( lines ); for ( String line : lines ) { super.write( line.getBytes() ); } super.flush(); super.close(); } /** * Get next line. The line end is marked at the first occurrence of an unescaped '\n' or '\r' character. All following * '\n' or '\r' characters after the first unescaped '\n' or '\r' character are included in the line. * * @param iPos * The position from where to start at. This is passed as array of size one to <i>pass back</i> the parsing * position (kind of C++ reference pass) * @return */ private String nextLine( int[] iPos ) { // End of StringBuilder reached? if ( iPos[0] >= sb.length() ) { return null; } // Remember start int iStart = iPos[0]; char c = sb.charAt( iPos[0] ); // Read until end of stream reached or first '\n' or '\r' character found while ( iPos[0] < sb.length() && c != '\n' && c != '\r' ) { c = sb.charAt( iPos[0]++ ); // If now we have '\r' or '\n' and they are escaped, we just read the next // character. For this at least two characters must have been read. if ( iPos[0] >= 2 ) { // Is it an escaped '\r' or '\n'? if ( ( c == '\n' || c == '\r' ) && ( iPos[0] - 2 == '\\' ) ) { // Yes! Just read next character, if not end of stream reached if ( iPos[0] < sb.length() ) { c = sb.charAt( iPos[0]++ ); } } } } // Either we've found a '\r' or '\n' character or we are at the end of the stream. // In either case return. if ( iPos[0] == sb.length() ) { // Return complete remainder return sb.substring( iStart ); } else { // Consume characters as long as '\r' or '\n' is found. while ( iPos[0] < sb.length() && ( c == '\n' || c == '\r' ) ) { c = sb.charAt( iPos[0]++ ); } // Return complete remainder or part of stream if ( iPos[0] == sb.length() ) { return sb.substring( iStart ); } else { iPos[0]--; return sb.substring( iStart, iPos[0] ); } } } }