Java tutorial
/** * Copyright (C) 2007 Sly Technologies, Inc. This library is free software; you * can redistribute it and/or modify it under the terms of the GNU Lesser * General Public License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. This * library is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ package com.slytechs.capture.file.editor; import java.io.Closeable; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.channels.FileChannel; import java.util.Iterator; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jnetstream.capture.FileMode; import org.jnetstream.capture.file.HeaderReader; import org.jnetstream.capture.file.RawIterator; import org.jnetstream.capture.file.RecordFilterTarget; import org.jnetstream.filter.Filter; import org.jnetstream.packet.ProtocolFilterTarget; import com.slytechs.capture.file.RawIteratorBuilder; import com.slytechs.utils.collection.IOIterator.IteratorAdapter; import com.slytechs.utils.io.AutoflushMonitor; import com.slytechs.utils.io.IORuntimeException; import com.slytechs.utils.memory.PartialBuffer; import com.slytechs.utils.region.FlexRegion; import com.slytechs.utils.region.RegionSegment; /** * @author Mark Bednarczyk * @author Sly Technologies, Inc. */ public class FileEditorImpl implements FileEditor, Closeable, AutoflushMonitor, Iterable<ByteBuffer> { public static final long AUTOFLUSH_AMOUNT = 1000000; public boolean autoflush = true; public ByteOrder order; public FileChannel channel; public final FlexRegion<PartialLoader> edits; protected File file; public final HeaderReader headerReader; @SuppressWarnings("unused") private final Log logger = LogFactory.getLog(FileEditorImpl.class); private final FileMode mode; public long totalChange = 0; protected final Filter<ProtocolFilterTarget> protocolFilter; private final RawIteratorBuilder rawBuilder; /** * @param file * @param mode * TODO * @param headerReader * @param order TODO * @param protocolFilter * TODO * @param rawBuilder TODO * @throws IOException */ public FileEditorImpl(final File file, final FileMode mode, final HeaderReader headerReader, ByteOrder order, Filter<ProtocolFilterTarget> protocolFilter, RawIteratorBuilder rawBuilder) throws IOException { this.file = file; this.order = order; this.protocolFilter = protocolFilter; this.rawBuilder = rawBuilder; this.channel = new RandomAccessFile(file, (mode.isContent() || mode.isAppend() ? "rw" : "r")).getChannel(); this.mode = mode; this.headerReader = headerReader; final boolean readonly = !mode.isStructure(); final boolean append = mode.isAppend(); final PartialLoader loader = new PartialFileLoader(channel, mode, headerReader, file); this.edits = new FlexRegion<PartialLoader>(readonly, append, channel.size(), loader); } /** * */ public void abortChanges() { this.edits.clear(); this.totalChange = 0; } public void add(final ByteBuffer b, final long global) throws IOException { final long length = b.limit() - b.position(); // Create a partial loader for our cache memory buffer and do the insert final PartialLoader record = new MemoryCacheLoader(b, true, headerReader); this.edits.insert(global, length, record); this.autoflushChange(length); } /* * (non-Javadoc) * * @see com.slytechs.utils.io.AutoflushMonitor#autoflushChange(long) */ public void autoflushChange(final long delta) throws IOException { this.totalChange += delta; if (this.autoflush && (this.totalChange > FileEditorImpl.AUTOFLUSH_AMOUNT)) { this.flush(); } } /* * (non-Javadoc) * * @see java.io.Closeable#close() */ public void close() throws IOException { try { this.flush(); } finally { if (this.channel.isOpen()) { this.channel.close(); } /* * Need to clear the region and run GC as memory mapped buffers may still * remain and hold the channel open. This may cause that associated file * can not be removed. Running GC seems to help, although officially Sun * says that memory mapped buffers are not unmappable and may remain in * memory until VM terminates and even beyond that. */ this.edits.clear(); System.gc(); } edits.close(); } /** * @param headerReader * @param l * @param length * @throws IOException */ private PartialBuffer fetchPartialBuffer(final HeaderReader lengthGetter, final long global, final int minLength) throws IOException { final RegionSegment<PartialLoader> segment = this.edits.getSegment(global); final PartialLoader loader = segment.getData(); final long regional = segment.mapGlobalToRegional(global); final PartialBuffer blockBuffer = loader.fetchBlock(regional, minLength); final int p = (int) (regional - blockBuffer.getStartRegional()); /* * Make sure the next record we want to fetch resides in the existing shared * buffer, otherwise we have to prefetch another buffer. */ if ((p < 0) || (blockBuffer.checkBoundsRegional(regional, minLength) == false)) { throw new IllegalStateException("Unable to prefetch buffer [" + regional + "/" + minLength + "]"); } final ByteBuffer buffer = blockBuffer.getByteBuffer(); buffer.limit(p + lengthGetter.getMinLength()); buffer.position(p); return blockBuffer; } /* * (non-Javadoc) * * @see java.lang.Object#finalize() */ @Override protected void finalize() throws Throwable { if (this.channel.isOpen()) { this.close(); } } /** * <p> * Flushes all the changes made so far to the underlying file. If no changes * have been made, nothing happens. Changes are flushed to the underlying file * and the edits hierarchy is flattened. Any previously aquired change * iterators should be discarded and new ones aquired. Any attempt to use them * will throw InvalidRegionException since all changes have been invalidated. * Forwarding to the new edits tree will forward any RegionHandles to new * RegionOverlays which now contain the flattened changes. * </p> * <p> * The algorithm in this flush method is optimized for changes that simply * append record to the end. If anyother type of changes, besides the appended * records, have been applied, the changes are flushed using a more generic * algrorithm that ensure entegrity of the entire file. * </p> * * @see java.io.Flushable#flush() */ public void flush() throws IOException { if (this.isModified() == false) { return; } if (this.edits.isModifiedByAppendOnly()) { this.flushByAppendInPlace(); } else { /* * We flatten inside inorder to release any Memory MAPPED regions before * closing the source channel */ this.flushByCopy(); } final PartialLoader loader = new PartialFileLoader(this.channel, this.mode, this.headerReader, file); loader.order(this.order); this.edits.flatten(loader); System.gc(); this.totalChange = 0; } /** * @throws IOException */ private void flushByAppendInPlace() throws IOException { /* * Position channel cursor at the end of the file, ready for append. */ this.channel.position(this.channel.size()); /* * Skip and align to the first change */ final Iterator<RegionSegment<PartialLoader>> i = this.edits.iterator(); if (i.hasNext() == false) { throw new IllegalStateException("Editor has no changes, can not flush in place"); } i.next(); // Skip over the first region, the second is the first change while (i.hasNext()) { final RegionSegment<PartialLoader> segment = i.next(); final PartialLoader loader = new ConstrainedPartialLoader(segment.getData(), segment); loader.transferTo(this.channel); } } /** * <p> * Flushes all the changes that currently exist in the "edits" buffer into a * temporary secondary file. After the copy the original file is removed and * the temporary file is renamed back to the original file which now contains * the contents of the "edits" buffer. * </p> * <p> * All the regions and their overlays are iterated over one segment at a time, * this includes the big segment consiting of the original file content, and * their reader's are asked to copy their buffers out to the temporary file's * channel in their individual smaller segments. * </p> * * @throws IOException * any IO errors with either the source file or the temporary file * operation's during the flush */ private void flushByCopy() throws IOException { /* * Create a temp file */ final File temp = File.createTempFile(this.file.getName(), null); final FileChannel tempChannel = new RandomAccessFile(temp, "rw").getChannel(); /* * Copy entire edits tree, including the root file, to temp file */ for (final RegionSegment<PartialLoader> segment : this.edits) { final PartialLoader loader = new ConstrainedPartialLoader(segment.getData(), segment); loader.transferTo(tempChannel); } this.channel.close(); tempChannel.close(); System.gc(); /* * We're done with the original file. All changes are now in the temp file * Try rename first, if it doesn't exist then do it by copy */ if (file.delete() == false) { throw new IOException("Unable to delete original file during flushByCopy()"); } if (temp.renameTo(file) == false) { throw new IOException("Unable to move temporary file during flushByCopy()"); } final String accessMode = (mode.isContent() ? "rw" : "r"); /* * Now we need to reopen the channel */ this.channel = new RandomAccessFile(file, accessMode).getChannel(); } /** * Creates a handle that keeps track of position and buffer forwards after the * editor is flushed. * * @param global * position for which to generate the handle for. * @return handle which will keep track of buffer at the specified position */ public EditorHandle generateHandle(final long global) { final EditorHandle handle = new EditorHandleImpl(this.edits.createHandle(global), headerReader); return handle; } /** * @param global * @param headerReader * @return * @throws IOException */ public ByteBuffer get(final long global, final HeaderReader blockGetter) throws IOException { final PartialBuffer min = this.fetchPartialBuffer(blockGetter, global, blockGetter.getMinLength()); final long length = blockGetter.readLength(min.getByteBuffer()); final int regional = (int) min.getStartRegional(); final PartialBuffer partial; if (min.checkBoundsRegional(regional, regional + (int) length) == false) { partial = fetchPartialBuffer(headerReader, global, (int) length); } else { partial = min; } partial.reposition(regional, (int) length); return partial.getByteBuffer(); } /* * (non-Javadoc) * * @see com.slytechs.utils.io.Autoflushable#getAutoflush() */ public boolean getAutoflush() { return this.autoflush; } public FileChannel getChannel() { return this.channel; } public File getFile() { return this.file; } /** * @return */ public long getLength() { return this.edits.getLength(); } public RawIterator getRawIterator() throws IOException { return this.getRawIterator(null); } public RawIterator getRawIterator(Filter<RecordFilterTarget> filter) throws IOException { return rawBuilder.createRawIterator(filter); } /** * @return */ public boolean isModified() { return this.totalChange != 0; } /** * @return */ public boolean isMutable() { return mode.isAppend() || mode.isContent(); } public boolean isOpen() { return channel.isOpen(); } public Iterator<ByteBuffer> iterator() { final RawIterator raw; try { raw = this.getRawIterator(); } catch (final IOException e) { throw new IORuntimeException(e); } return new IteratorAdapter<ByteBuffer>(raw); } public final ByteOrder order() { return this.order; } public final void order(final ByteOrder order) { this.order = order; /* * Change the byte order on all segments */ for (final RegionSegment<PartialLoader> segment : this.edits) { segment.getData().order(order); } /* * Notify the main editor that change happened to data and that in our case * RO buffer may have been replaced with a RW buffer */ this.edits.changeHappened(); } /* * (non-Javadoc) * * @see org.jnetstream.capture.file.RawIterator#replaceInPlace() */ public void replaceInPlace(final long global, final boolean copy) throws IOException { // existing record from buffer final ByteBuffer original = this.get(global, headerReader); // its length final int length = original.limit() - original.position(); // create new buffer by copy of the original final PartialLoader loader = new MemoryCacheLoader(original, copy, headerReader); // now the replacement by region with the new buffer this.edits.replace(global, length, length, loader); this.autoflushChange(length * (copy ? 2 : 1)); } /* * (non-Javadoc) * * @see com.slytechs.utils.io.Autoflushable#setAutoflush(boolean) */ public void setAutoflush(final boolean state) throws IOException { this.autoflush = state; this.autoflushChange(0); } /** * @return */ public HeaderReader getLengthGetter() { return headerReader; } /** * @return */ public FlexRegion<PartialLoader> getFlexRegion() { return edits; } }