package org.ops4j.pax.construct.util;
/*
* Copyright 2007 Stuart McCulloch
*
* 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.
*/
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import org.codehaus.plexus.util.IOUtil;
import org.ops4j.pax.construct.util.BndUtils.Bnd;
import org.ops4j.pax.construct.util.BndUtils.ExistingInstructionException;
/**
* Support round-trip editing of Bnd files, preserving formatting as much as possible
*/
public class RoundTripBndFile
implements Bnd
{
/**
* Underlying Bnd file
*/
private final File m_file;
/**
* Current instructions
*/
private Properties m_newInstructions;
/**
* Last saved instructions
*/
private Properties m_oldInstructions;
/**
* @param bndFile property file containing Bnd instructions
* @throws IOException
*/
public RoundTripBndFile( File bndFile )
throws IOException
{
// protect against changes in working directory
m_file = DirUtils.resolveFile( bndFile, true );
m_oldInstructions = new Properties();
m_newInstructions = new Properties();
if( m_file.exists() )
{
FileInputStream bndStream = new FileInputStream( m_file );
m_oldInstructions.load( bndStream );
IOUtil.close( bndStream );
}
m_newInstructions.putAll( m_oldInstructions );
}
/**
* {@inheritDoc}
*/
public String getInstruction( String directive )
{
return m_newInstructions.getProperty( directive );
}
/**
* {@inheritDoc}
*/
public void setInstruction( String directive, String instruction, boolean overwrite )
throws ExistingInstructionException
{
if( overwrite || !m_newInstructions.containsKey( directive ) )
{
if( null == instruction )
{
// map null instructions to the empty string
m_newInstructions.setProperty( directive, "" );
}
else
{
m_newInstructions.setProperty( directive, instruction );
}
}
else
{
throw new ExistingInstructionException( directive );
}
}
/**
* {@inheritDoc}
*/
public boolean removeInstruction( String directive )
{
return null != m_newInstructions.remove( directive );
}
/**
* {@inheritDoc}
*/
public Set getDirectives()
{
return m_newInstructions.keySet();
}
/**
* {@inheritDoc}
*/
public void overlayInstructions( Bnd bnd )
{
Set directives = bnd.getDirectives();
if( directives.contains( "Private-Package" ) || directives.contains( "Export-Package" ) )
{
// this might be an old converted wrapper project, so remove the default
// embed dependency directive (only specified in osgi-wrapper archetype)
removeInstruction( "Embed-Dependency" );
}
for( Iterator i = directives.iterator(); i.hasNext(); )
{
String directive = (String) i.next();
String instruction = bnd.getInstruction( directive );
setInstruction( directive, instruction, true );
}
}
/**
* {@inheritDoc}
*/
public File getFile()
{
return m_file;
}
/**
* {@inheritDoc}
*/
public File getBasedir()
{
return m_file.getParentFile();
}
/**
* {@inheritDoc}
*/
public void write()
throws IOException
{
if( !m_newInstructions.equals( m_oldInstructions ) || !m_file.exists() )
{
writeUpdatedInstructions();
}
m_oldInstructions.clear();
m_oldInstructions.putAll( m_newInstructions );
}
/**
* Write changes to disk, preserving formatting of unaffected lines
*
* @throws IOException
*/
private void writeUpdatedInstructions()
throws IOException
{
List lines = readLines();
List block = new ArrayList();
boolean skip = false;
boolean echo = true;
Properties instructions = new Properties();
instructions.putAll( m_newInstructions );
for( Iterator i = lines.iterator(); i.hasNext(); )
{
String line = (String) i.next();
if( isWhitespaceOrComment( line ) )
{
block.add( line );
skip = false;
}
else
{
if( !skip )
{
// check to see if we should update / remove / leave alone
echo = checkInstructionLine( block, line, instructions );
}
if( echo )
{
block.add( line );
}
// continue skipping to end of line
skip = isLineContinuation( line );
}
}
// append any new instructions...
for( Enumeration e = instructions.keys(); e.hasMoreElements(); )
{
String key = (String) e.nextElement();
String value = instructions.getProperty( key );
writeInstruction( block, key, value );
}
// finally write updated text back to the file
BufferedWriter writer = new BufferedWriter( StreamFactory.newPlatformWriter( m_file ) );
writeInstructionBlock( writer, block );
IOUtil.close( writer );
}
/**
* Write a block of comments and instructions to the Bnd file
*
* @param bndWriter writer for the Bnd file
* @param block comment block
* @throws IOException
*/
private static void writeInstructionBlock( BufferedWriter bndWriter, List block )
throws IOException
{
boolean needSpace = false;
for( Iterator i = block.iterator(); i.hasNext(); )
{
String line = (String) i.next();
boolean isSpace = isWhitespaceOrComment( line );
if( needSpace && !isSpace )
{
bndWriter.newLine();
}
needSpace = false;
if( !isSpace && !isLineContinuation( line ) )
{
needSpace = true;
}
bndWriter.write( line );
bndWriter.newLine();
}
}
/**
* This assumes most Bnd files will be relatively small
*
* @return list of all the lines in the Bnd file
* @throws IOException
*/
private List readLines()
throws IOException
{
List lines = new ArrayList();
if( m_file.exists() )
{
BufferedReader bndReader = new BufferedReader( StreamFactory.newPlatformReader( m_file ) );
while( bndReader.ready() )
{
lines.add( bndReader.readLine() );
}
IOUtil.close( bndReader );
}
return lines;
}
/**
* Check existing line against instructions and update if necessary
*
* @param block comment block
* @param line existing line
* @param instructions the instructions to write to disk
* @return true if existing line should be echoed unchanged, otherwise false
*/
private boolean checkInstructionLine( List block, String line, Properties instructions )
{
String[] keyAndValue = line.split( "[=: \t\r\n\f]", 2 );
String key = keyAndValue[0].trim();
if( instructions.containsKey( key ) )
{
String newValue = (String) instructions.remove( key );
if( newValue.equals( m_oldInstructions.getProperty( key ) ) )
{
// no change
return true;
}
// old instruction has been altered
writeInstruction( block, key, newValue );
return false;
}
// remove old instruction comment
removeInstructionComment( block );
return false;
}
/**
* Remove the comment that's directly attached to the current instruction
*
* @param block comment block
*/
private static void removeInstructionComment( List block )
{
while( !block.isEmpty() )
{
// remove lines in reverse
String line = (String) block.remove( block.size() - 1 );
// assume comment ends once we see an empty line or a non-comment
if( line.trim().length() == 0 || !isWhitespaceOrComment( line ) )
{
block.add( line );
return;
}
}
}
/**
* @param line existing line
* @return true if line only contains whitespace or comments
*/
private static boolean isWhitespaceOrComment( String line )
{
String comment = line.trim();
if( comment.length() == 0 )
{
return true;
}
char c = comment.charAt( 0 );
return '#' == c || '!' == c;
}
/**
* @param line existing line
* @return true if line ends in a continuation marker
*/
private static boolean isLineContinuation( String line )
{
boolean continueLine = false;
for( int c = line.length() - 1; c >= 0 && '\\' == line.charAt( c ); c-- )
{
continueLine = !continueLine;
}
return continueLine;
}
/**
* Mark instruction clauses with line-continuation markers
*
* @param instruction Bnd instruction
* @return marked instruction
*/
private static String markInstructionClauses( String instruction )
{
StringBuffer buf = new StringBuffer();
boolean inQuotes = false;
// add \\ markers between clauses
char[] text = instruction.toCharArray();
for( int i = 0; i < text.length; i++ )
{
char c = text[i];
buf.append( c );
switch( c )
{
case '\'':
case '\"':
inQuotes = !inQuotes;
break;
case ',':
if( !inQuotes )
{
buf.append( '\\' );
}
break;
default:
break;
}
}
return buf.toString();
}
/**
* Write instruction as a standard property, with continuation markers at every comma
*
* @param block comment block
* @param key property key
* @param value property value
*/
private static void writeInstruction( List block, String key, String value )
{
StringBuffer buf = new StringBuffer( key + ':' );
String instruction = markInstructionClauses( value );
// heuristic: only wrap long instructions
boolean multiLine = ( instruction.length() > 80 );
// output clauses on single or multiple lines
String[] clauses = instruction.split( "\\\\" );
for( int i = 0; i < clauses.length; i++ )
{
if( multiLine )
{
buf.append( '\\' );
block.add( buf.toString() );
buf.setLength( 0 );
buf.append( ' ' );
}
else if( i == 0 )
{
buf.append( ' ' );
}
buf.append( clauses[i].trim() );
}
block.add( buf.toString() );
}
}