/** * This file Copyright (c) 2005-2008 Aptana, Inc. This program is * dual-licensed under both the Aptana Public License and the GNU General * Public license. You may elect to use one or the other of these licenses. * * This program is distributed in the hope that it will be useful, but * AS-IS and WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, TITLE, or * NONINFRINGEMENT. Redistribution, except as permitted by whichever of * the GPL or APL you select, is prohibited. * * 1. For the GPL license (GPL), you can redistribute and/or modify this * program under the terms of the GNU General Public License, * Version 3, as published by the Free Software Foundation. You should * have received a copy of the GNU General Public License, Version 3 along * with this program; if not, write to the Free Software Foundation, Inc., 51 * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Aptana provides a special exception to allow redistribution of this file * with certain Eclipse Public Licensed code and certain additional terms * pursuant to Section 7 of the GPL. You may view the exception and these * terms on the web at http://www.aptana.com/legal/gpl/. * * 2. For the Aptana Public License (APL), this program and the * accompanying materials are made available under the terms of the APL * v1.0 which accompanies this distribution, and is available at * http://www.aptana.com/legal/apl/. * * You may view the GPL, Aptana's exception and additional terms, and the * APL in the file titled license.html at the root of the corresponding * plugin containing this source file. * * Any modifications to this file must keep this entire header intact. */ package com.aptana.ide.logging.impl; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.RandomAccessFile; import java.io.Reader; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import com.aptana.ide.core.IdeLog; import com.aptana.ide.logging.LoggingPlugin; /** * Log watcher for local file. * @author Denis Denisenko */ public class LocalLogWatcher extends AbstractLogWatcher { /** * Random access file reader. * @author Denis Denisenko */ private class RandomAccessReader extends Reader { /** * Stream. */ private Reader reader; public RandomAccessReader(final RandomAccessFile file, long startPos, Charset encoding) throws IOException { final InputStream stream = new InputStream() { @Override public int read() throws IOException { throw new UnsupportedOperationException(); } /** * {@inheritDoc} */ @Override public int read(byte[] b, int off, int len) throws IOException { return file.read(b, off, len); } /** * {@inheritDoc} */ @Override public int read(byte[] b) throws IOException { return file.read(b); } }; file.seek(startPos); this.reader = new InputStreamReader(stream, encoding); } /** * {@inheritDoc} */ @Override public void close() throws IOException { reader.close(); } /** * {@inheritDoc} */ @Override public int read(char[] cbuf, int off, int len) throws IOException { return reader.read(cbuf, off, len); } } /** * Increase coefficient */ private static final int INCREASE_K = 2; /** * Mean number of chars per string. */ private final int MEAN_CHARS_PER_STRING = 80; /** * Max buffer. */ private final int MAX_BUFFER = 1024*1024; /** * Border of linear buffer increasing. */ private final int LINEAR_BORDER = 64*1024; /** * Estimated buffer size. */ private final int estimatedBufferSize; /** * Buffer for backwards reading. */ private char[] buffer; /** * Last file end position. */ private long lastFileLength; /** * Whether whole file was read. */ private boolean wholeFileRead = false; /** * LocalFileLogWatcher constructor. * * @param resource - resource. * @param config - watcher configuration. */ public LocalLogWatcher(AbstractLogResource resource, LogWatcherConfiguration config) { super(config, resource); estimatedBufferSize = estimateSize(config.getBacklogRows(), config.getEncoding()); buffer = new char[estimatedBufferSize]; } /** * {@inheritDoc} */ protected DataChange getData() { RandomAccessFile raFile = null; try { raFile = new RandomAccessFile(getLocalResource() .getFile(), "r"); //$NON-NLS-1$ boolean wholeView = false; if (raFile.length() == lastFileLength) { // update not needed return null; } else { // if (raFile.length() < lastFileLength) // { // wholeView = true; // } // saving new file length lastFileLength = raFile.length(); } //reading data int bufLength = readBuffer(raFile); if (wholeFileRead) { return buildChange(bufLength, 0, wholeView); } int upperLineOffset; while((upperLineOffset = checkLinesNumber(bufLength)) == -1) { if (!increaseBuffer()) { return buildChange(bufLength, 0, wholeView); } bufLength = readBuffer(raFile); if (wholeFileRead) { return buildChange(bufLength, 0, wholeView); } } //data is read, now returning it return buildChange(bufLength, upperLineOffset, wholeView); } catch (IOException e) { // we were unable to access file for some reason, but still watching // it. notifyListenersResourceAvailable(false); return null; } finally { try { if (raFile != null) { raFile.close(); } } catch (IOException e) { IdeLog.logError(LoggingPlugin.getDefault(), Messages.LocalLogWatcher_ERR_Exception, e); } } } /** * Builds change using current data. * @param bufLength - current buffer length. * @param upperLineOffset - upper line offset. * @param wholeView - whether to replace whole view. * @return */ private DataChange buildChange(int bufLength, int upperLineOffset, boolean wholeView) { int dataLength = bufLength - upperLineOffset; String data = new String(buffer, upperLineOffset, dataLength); DataChange change = null; if (wholeView) { change = new DataChange(data, (int) (lastFileLength - dataLength) , Integer.MAX_VALUE); } else { change = new DataChange(data, (int) (lastFileLength - dataLength) , dataLength); } return change; } /** * Increases buffer. * @return true if buffer increased, false otherwise. */ private boolean increaseBuffer() { int currentSize = buffer.length; if (currentSize < LINEAR_BORDER) { currentSize *= INCREASE_K; } else { currentSize += LINEAR_BORDER; } if (currentSize >= MAX_BUFFER) { return false; } try { char[] newBuffer = new char[currentSize]; buffer = newBuffer; } catch(OutOfMemoryError e) { return false; } return true; } /** * Check whether number of lines read is ok. * @param length - buffer length. * @return offset of the upper line, or -1 if not enough lines * @return true if number of lines is ok, false otherwise. */ private int checkLinesNumber(int length) { int okNumberOfLines = getConfiguration().getBacklogRows(); int linesNumber = 0; List<Integer> linesInfo = new ArrayList<Integer>(); for (int i = 0; i < length; i++) { int ch = buffer[i]; switch (ch) { case '\r': //checking for following '\n' if (i < length - 1) { int nextChar = buffer[i+1]; if (nextChar == '\n') { i++; } } linesInfo.add(i); linesNumber++; break; case '\n': linesInfo.add(i); linesNumber++; break; default: break; } } if (linesNumber > okNumberOfLines) { int diff = linesNumber - okNumberOfLines; return linesInfo.get(diff) + 1; } else if (linesNumber == okNumberOfLines) { return 0; } return -1; } /** * Reads buffer. * @param raFile - file to read. * @return number of read characters. * @throws IOException */ private int readBuffer(RandomAccessFile raFile) throws IOException { long startPos = lastFileLength - buffer.length; if (startPos < 0) { startPos = 0; //TODO implement a better way of notification wholeFileRead = true; } else { wholeFileRead = false; } Reader reader = new RandomAccessReader(raFile, startPos, getResource().getEncoding()); int read = 0; int bufPos = 0; while (read != -1 && bufPos != buffer.length) { bufPos += read; read = reader.read(buffer, bufPos, buffer.length - bufPos); } return bufPos; } /** * {@inheritDoc} */ public void resetWatching() { stopWatching(); //resetting reader lastFileLength = 0; wholeFileRead = false; } /** * Gets local log resource. * @return local log resource. */ protected LocalLogResource getLocalResource() { return (LocalLogResource) getResource(); } /** * Estimates buffer size. * @param backlogRows - rows. * @param encoding - encoding. * @return estimated buffer size required to store rows. */ private int estimateSize(int backlogRows, Charset encoding) { float averageCharsPerByte = encoding.newDecoder().averageCharsPerByte(); if (averageCharsPerByte == 0) { return 0; } return (int) (((float)backlogRows) * ((float)MEAN_CHARS_PER_STRING) * (1f / averageCharsPerByte)); } }