blob: bcf48b543a64c41608f9d8fb739a1ccd0ed1755b [file] [log] [blame]
/*
*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.royale.compiler.filespecs;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.io.IOUtils;
import org.apache.royale.utils.FilenameNormalization;
/**
* This is an {@link IFileSpecification} that include multiple source files into
* one. JFlex-generated tokenizers can skip BOM at the beginning of a file, but
* not elsewhere. This class skips BOM header in each source file so that the
* merged file won't have a BOM in the middle of a file.
*/
public class CombinedFile implements IFileSpecification
{
/**
* Create a combined file by concatenating {@code includedFilenames}
* together and append {@code sourceFilename} at the end.
*
* @param includedFilenames included files
* @param sourceFilename source file
*/
public CombinedFile(final List<String> includedFilenames, final String sourceFilename)
{
this.combinedSource = null;
this.sourceFilename = FilenameNormalization.normalize(sourceFilename);
this.fileList = includedFilenames;
this.fileList.add(sourceFilename);
}
private StringBuilder combinedSource;
private final String sourceFilename;
private final List<String> fileList;
/**
* The path of the combined file is the path of the source file.
*/
@Override
public String getPath()
{
return sourceFilename;
}
/**
* Create a {@link Reader} from the combined source text.
* <p>
* {@inheritDoc}
*/
@Override
public Reader createReader() throws FileNotFoundException
{
if (combinedSource == null)
combineFile();
return new StringReader(combinedSource.toString());
}
/**
* Get the time stamp of the most recent modified file among the source
* files.
* <p>
* {@inheritDoc}
*/
@Override
public long getLastModified()
{
long lastModified = 0;
for (final String file : fileList)
{
final long timestamp = new File(file).lastModified();
lastModified = Math.max(lastModified, timestamp);
}
return lastModified;
}
/**
* Always return false because the combined file is cached in memory.
* <p>
* {@inheritDoc}
*/
@Override
public boolean isOpenDocument()
{
return false;
}
/**
* Concatenate source files together. The main source file is always at the
* end.
*
* @throws FileNotFoundException error
*/
private void combineFile() throws FileNotFoundException
{
assert combinedSource == null : "Do not call combineFile() twice.";
combinedSource = new StringBuilder();
for (final String filename : fileList)
{
Reader reader = null;
try
{
final BufferedInputStream strm = getStreamAndSkipBOM(filename);
reader = new InputStreamReader(strm);
combinedSource.append(IOUtils.toString(reader));
combinedSource.append(IOUtils.LINE_SEPARATOR);
}
catch (FileNotFoundException e)
{
throw e;
}
catch (IOException e)
{
throw new RuntimeException(e);
}
finally
{
IOUtils.closeQuietly(reader);
}
}
}
/**
* BOM patterns.
*
* @see <a href="http://www.unicode.org/faq/utf_bom.html#BOM">Unicode BOM spec</a>
*/
public static enum BOM
{
NONE("UTF-8"),
UTF_8("UTF-8", (byte)0xEF, (byte)0xBB, (byte)0xBF),
UTF_16_LE("UTF-16LE", (byte)0xFF, (byte)0xFE),
UTF_16_BE("UTF-16BE", (byte)0xFE, (byte)0xFF),
UTF_32_LE("UTF-32LE", (byte)0xFF, (byte)0xFE, (byte)0x00, (byte)0x00),
UTF_32_BE("UTF-32BE", (byte)0x00, (byte)0x00, (byte)0xFE, (byte)0xFF);
private BOM(final String charsetName, final byte... pattern)
{
if (charsetName == null || !Charset.isSupported(charsetName))
this.charset = Charset.defaultCharset();
else
this.charset = Charset.forName(charsetName);
this.pattern = pattern;
}
/**
* BOM pattern in byte array.
*/
public final byte pattern[];
/**
* The Java {@link Charset} for this BOM header.
*/
public final Charset charset;
}
/**
* Get the {@link BufferedInputStream} of a file, skipping the BOM.
*
* @param filename The path to the file.
* @return BufferedInputStream
*/
public static BufferedInputStream getStreamAndSkipBOM(String filename) throws IOException
{
final File file = new File(filename);
BufferedInputStream strm = new BufferedInputStream(new FileInputStream(file));
final BOM bom = getBOM(strm);
strm.skip(bom.pattern.length);
return strm;
}
/**
* Get the BOM tag of a stream.
*
* @param strm BufferedInputStream to be checked.
* @return {@link BOM} type.
* @throws IOException Error.
*/
public static BOM getBOM(BufferedInputStream strm) throws IOException
{
assert (strm.markSupported()) : "getBOM call on stream which does not support mark";
// Peek the first 4 bytes.
final byte[] peek = new byte[4];
strm.mark(4);
strm.read(peek);
strm.reset();
// Try matching 4-byte BOM tags.
final byte[] quadruplet = Arrays.copyOf(peek, 4);
if (Arrays.equals(BOM.UTF_32_BE.pattern, quadruplet))
return BOM.UTF_32_BE;
else if (Arrays.equals(BOM.UTF_32_LE.pattern, quadruplet))
return BOM.UTF_32_LE;
// Try matching 3-byte BOM tags.
final byte[] triplet = Arrays.copyOf(peek, 3);
if (Arrays.equals(BOM.UTF_8.pattern, triplet))
return BOM.UTF_8;
// Try matching 2-byte BOM tags.
final byte[] twin = Arrays.copyOf(peek, 2);
if (Arrays.equals(BOM.UTF_16_BE.pattern, twin))
return BOM.UTF_16_BE;
else if (Arrays.equals(BOM.UTF_16_LE.pattern, twin))
return BOM.UTF_16_LE;
// No BOM tag.
return BOM.NONE;
}
}