
org.apache.daffodil.io.DirectOrBufferedDataOutputStream.scala Maven / Gradle / Ivy
/*
* 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.daffodil.io
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardOpenOption
import passera.unsigned.ULong
import org.apache.daffodil.equality._
import org.apache.daffodil.exceptions.Assert
import org.apache.daffodil.exceptions.ThinThrowable
import org.apache.daffodil.schema.annotation.props.gen.BitOrder
import org.apache.daffodil.util.Bits
import org.apache.daffodil.util.LogLevel
import org.apache.daffodil.util.Maybe
import org.apache.daffodil.util.Maybe._
import org.apache.daffodil.util.MaybeULong
import org.apache.daffodil.util.Misc
import org.apache.daffodil.util.Logging
import java.io.File
/**
* This simple extension just gives us a public method for access to the underlying byte array.
* That way we don't have to make a copy just to access the bytes.
*/
private[io] class ByteArrayOutputStreamWithGetBuf() extends java.io.ByteArrayOutputStream {
def getBuf = buf
}
private[io] class ByteArrayOrFileOutputStream(
maxBufferSizeInBytes: Long,
tempDirPath: File,
maybeExistingFile: Maybe[Path])
extends java.io.OutputStream with Logging {
var isFile: Boolean = maybeExistingFile.isDefined
// This will be the file object that can represent either a generated
// temporary file or an existing blob file
var maybeFile: Maybe[File] = {
if (maybeExistingFile.isDefined)
Maybe(maybeExistingFile.get.toFile)
else
Maybe.Nope
}
// Keep track of whether or not this is a temporary file. If it is an existing
// file, i.e. a blob file, we do not want to delete it as that is handled
// elsewhere.
lazy val isTempFile = (!maybeExistingFile.isDefined && isFile)
// This is only used to determine if we need to switch to a file based output
// stream
var nBytes: Long = 0
var stream: java.io.OutputStream = new ByteArrayOutputStreamWithGetBuf()
/**
* Check to see if there is enough room in the ByteArrayOutputStream for the
* specified length. If there is not, switch to FileOutputStream.
*/
@inline
private def checkBuffer(lengthInBytes: Long): Unit = {
if (!isFile && (nBytes + lengthInBytes > maxBufferSizeInBytes)) {
log(LogLevel.Info, "Switching to file based output stream. If this is performance critical, you may want to consider re-organizing your schema to avoid this if possible.")
maybeFile = try {
val file = File.createTempFile("daffodil-", ".tmp", tempDirPath)
file.deleteOnExit()
Maybe(file)
} catch {
case e: Exception =>
throw new FileIOException("Unable to create temporary file in %s: %s".format(tempDirPath.getPath, e.getMessage()))
}
val newStream: java.io.OutputStream = new java.io.FileOutputStream(maybeFile.get)
stream.flush
stream.asInstanceOf[ByteArrayOutputStreamWithGetBuf].writeTo(newStream)
stream = newStream
isFile = true
} else
nBytes += lengthInBytes
}
override def write(b: Array[Byte]) = {
checkBuffer(b.length)
stream.write(b)
}
override def write(b: Array[Byte], off: Int, len: Int) = {
checkBuffer(len)
stream.write(b, off, len)
}
override def write(b: Int) = {
checkBuffer(1)
stream.write(b)
}
override def close() = stream.close()
def getBuf() = {
Assert.usage(!isFile, "Attempted to call getBuf on FileOutputStream")
stream.asInstanceOf[ByteArrayOutputStreamWithGetBuf].getBuf
}
def getFile: File = maybeFile.get
def hexDump: String = {
if (isFile)
maybeFile.get.toString
else
(0 to (nBytes.toInt - 1)).map { i => "%2x".format(getBuf()(i).toInt & 0xFF) }.mkString(".")
}
override def toString = hexDump
}
/**
* To support dfdl:outputValueCalc, we must suspend output. This is done by
* taking the current "direct" output, and splitting it into a still direct part, and
* a following buffered output.
*
* The direct part waits for the OVC calculation to complete, when that is written,
* it is finished and collapses into the following, which was buffered, but becomes direct
* as a result of this collapsing.
*
* Hence, most output will be to direct data output streams, with some, while an OVC
* is pending, will be buffered, but this is eliminated as soon as possible.
*
* A Buffered DOS can be finished or not. Not finished means that it might still be
* appended to. Not concurrently, but by other code invoked from this thread of
* control (which might traverse different co-routine "stack" threads, but it's still
* one thread of control).
*
* Finished means that the Buffered DOS can never be appended to again.
*
* Has two modes of operation, buffering or direct. When buffering, all output goes into a
* buffer. When direct, all output goes into a "real" DataOutputStream.
*
* The isLayer parameter defines whether or not this instance originated from a
* layer or not. This is important to specify because this class is reponsible
* for closing the associated Java OutputStream, ultimately being written to
* the underlying underlying DataOutputStream. However, if the DataOutputStream
* is not related to a layer, that means the associated Java OutputStream came
* from the user and it is the users responsibility to close it. The isLayer
* provides the flag to know which streams should be closed or not.
*
* chunkSizeInBytes is used when the buffered output stream is using a file as
* its buffer. This is the size of chunks that will be read into memory before
* being written to the direct output stream.
*
* maxBufferSizeInByte is the size that the ByteArrayOutputStream will grow to
* before switching over to a FileOutputStream
*
* tempDirPath is the path where temporary files will be created when switching
* to a file based buffer
*
* maybeExistingFile is used in the case of blob files, where we already have an
* existing file containing the data. This is the path to said file.
*
*/
class DirectOrBufferedDataOutputStream private[io] (
var splitFrom: DirectOrBufferedDataOutputStream,
val isLayer: Boolean = false,
val chunkSizeInBytes: Int,
val maxBufferSizeInBytes: Long,
val tempDirPath: File,
val maybeExistingFile: Maybe[Path])
extends DataOutputStreamImplMixin {
type ThisType = DirectOrBufferedDataOutputStream
override def putULong(unsignedLong: ULong, bitLengthFrom1To64: Int, finfo: FormatInfo): Boolean = {
val res = putLongChecked(unsignedLong.longValue, bitLengthFrom1To64, finfo)
res
}
override def putLong(signedLong: Long, bitLengthFrom1To64: Int, finfo: FormatInfo) = {
val res = putLongChecked(signedLong.longValue, bitLengthFrom1To64, finfo)
res
}
private val layerID: Int = synchronized {
if (splitFrom ne null) splitFrom.layerID
else {
val lid = DirectOrBufferedDataOutputStream.nextLayerID
DirectOrBufferedDataOutputStream.nextLayerID += 1
lid
}
}
/**
* Must be val, as split-from will get reset to null as streams
* are morphed into direct streams.
*/
private val splitID: Int = if (splitFrom == null) 0 else splitFrom.splitID + 1
/**
* id will be a N.M type identifier where N is the layer number,
* and M is the splitID.
*/
lazy val id: String = layerID.toString + "." + splitID.toString
/**
* Two of these are equal if they are eq.
* This matters because we compare them to see if we are making forward progress
*/
override def equals(other: Any) = AnyRef.equals(other)
override def hashCode() = AnyRef.hashCode()
override def toString = {
lazy val buf = bufferingJOS.getBuf()
lazy val max16ByteArray = buf.slice(0, 16)
lazy val upTo16BytesInHex = Misc.bytes2Hex(max16ByteArray)
val toDisplay = "DOS(id=" + id + ", " + dosState +
(if (isBuffering) ", Buffered" else ", Direct") +
(if (maybeAbsBitPos0b.isDefined) {
val srt = if (isDirect) 0 else maybeAbsStartingBitPos0b.get
val end = maybeAbsBitPos0b.get
val len = ULong(end - srt).longValue
" Absolute from %d to %d (length %d)".format(srt, end, len)
} else {
if (splitFrom ne null)
" at rel bit pos %d".format(relBitPos0b.longValue)
else
" at rel bit pos %d".format(relBitPos0b.longValue)
}) +
(if (maybeAbsBitLimit0b.isDefined) {
" limit %d.".format(maybeAbsBitLimit0b.get)
} else if (maybeRelBitLimit0b.isDefined) {
" length limit %d.".format(maybeRelBitLimit0b.get)
} else "") +
(if (isBuffering) ", data=" + upTo16BytesInHex else "") +
(if (_following.isEmpty) " no following" else "") +
")"
toDisplay
}
/**
* This is for debugging. It works backward through the chain of DOS' until
* it finds one that is holding things up (preventing collapsing)
* by not having any absolute position information, or being still active.
*/
def findFirstBlocking: DirectOrBufferedDataOutputStream = {
if (maybeAbsBitPos0b.isEmpty || !isFinished) this
else {
Assert.invariant(this.maybeAbsBitPos0b.isEmpty)
Assert.invariant(this.splitFrom ne null)
splitFrom.findFirstBlocking
}
}
/**
* When in buffering mode, this is the buffering device.
*
* If reused, this must be reset.
*/
protected val bufferingJOS = new ByteArrayOrFileOutputStream(maxBufferSizeInBytes, tempDirPath, maybeExistingFile)
/**
* Switched to point a either the buffering or direct java output stream in order
* to change modes from buffering to direct (and back if these objects get reused.)
*/
protected var _javaOutputStream: java.io.OutputStream = bufferingJOS
final def isBuffering: Boolean = {
val res = getJavaOutputStream() _eq_ bufferingJOS
res
}
override def setJavaOutputStream(newOutputStream: java.io.OutputStream) {
Assert.usage(newOutputStream ne null)
_javaOutputStream = newOutputStream
Assert.usage(newOutputStream ne bufferingJOS) // these are born buffering, and evolve into direct.
}
override def getJavaOutputStream() = {
Assert.usage(_javaOutputStream ne null)
_javaOutputStream
}
/**
* Refers to the next DOS the contents of which will follow the contents of this DOS in the output.
*
* Note that an alignment region may be inserted first if the next DOS has an alignment requirement.
*/
private var _following: Maybe[DirectOrBufferedDataOutputStream] = Nope
override def maybeNextInChain: Maybe[DataOutputStream] = _following
def lastInChain: DirectOrBufferedDataOutputStream =
if (_following.isEmpty) this
else _following.get.lastInChain
/**
* Provides a new buffered data output stream. Note that this must
* be completely configured (byteOrder, encoding, bitOrder, etc.)
*/
def addBuffered(): DirectOrBufferedDataOutputStream = {
val buffered = new DirectOrBufferedDataOutputStream(
this,
isLayer,
chunkSizeInBytes,
maxBufferSizeInBytes,
tempDirPath,
Maybe.Nope)
addBufferedDOS(buffered)
buffered
}
def addBufferedBlob(
path: Path,
lengthInBits: Long,
blobChunkSizeInBytes: Int,
finfo: FormatInfo): DirectOrBufferedDataOutputStream = {
// create a special buffered blob data outputstream and split the current
// DOS to it. When this normal DOS is finished, the blob DOS will handle
// delivering the blob data to it without loading the whole blob into
// memory all at once.
val bufferedBlob = new DirectOrBufferedDataOutputStream(
this,
isLayer,
blobChunkSizeInBytes,
0,
tempDirPath,
Maybe(path))
addBufferedDOS(bufferedBlob)
// we know the length of the blob as passed in, so adjust the bit position
// and mark it as finished.
bufferedBlob.setRelBitPos0b(bufferedBlob.relBitPos0b + ULong(lengthInBits))
if (lengthInBits > 0)
bufferedBlob.setNonZeroLength()
bufferedBlob.setFinished(finfo)
// now split the blob DOS to a normal buffered DOS, we'll return this and
// expect the caller to update the UState accordingly so that all future
// unparsed data is written to this DOS. This DOS will eventually be
// delivered to the blob DOS once it becomes direct
val buffered = bufferedBlob.addBuffered()
buffered
}
private def addBufferedDOS(newBufStr: DirectOrBufferedDataOutputStream): Unit = {
Assert.usage(_following.isEmpty)
_following = One(newBufStr)
//
// TODO: PERFORMANCE: This is very pessimistic. It's making a complete clone of the state
// just in case after an outputValueCalc element we go off for a long time and lots of things
// change about these format settings.
//
// Really the expected case is that an OVC element and an IVC element form pairs. Often they'll
// be adjacent elements even, and it's very unlikely that any of the format properties vary as we
// go from the OVC element to the most distant element the OVC expression references
//
// So algorithmically, we'd like to share the DataOutputStream state, and UState, and split so they
// can differ only if we need to.
//
// Seems we need one more indirection to the state, so that we can share it, but on any write operation, we
// can split it by copying, and then change our indirection pointer to the copy, and then modify that.
//
newBufStr.assignFrom(this)
newBufStr.resetAllBitPos()
if (maybeRelBitLimit0b.isDefined) {
newBufStr.setMaybeRelBitLimit0b(MaybeULong(maybeRelBitLimit0b.get - relBitPos0b.toLong))
}
}
/**
* A buffering stream, when preceded by a direct stream, can become a
* direct stream when the preceding direct stream is finished.
*/
private def convertToDirect(oldDirectDOS: ThisType) {
Assert.usage(isBuffering)
Assert.usage(oldDirectDOS.isDirect)
setJavaOutputStream(oldDirectDOS.getJavaOutputStream)
Assert.invariant(isDirect)
this.setAbsStartingBitPos0b(ULong(0))
Assert.invariant(oldDirectDOS.maybeAbsStartingBitPos0b.isDefined)
// Preserve the bit limit
val mabl = oldDirectDOS.maybeAbsBitLimit0b
val absLargerLimit =
math.max(
if (mabl.isDefined) mabl.get else 0L,
if (maybeAbsBitLimit0b.isDefined) maybeAbsBitLimit0b.get else 0L)
if (mabl.isDefined || maybeAbsBitLimit0b.isDefined) {
val newRelLimit = absLargerLimit - this.maybeAbsStartingBitPos0b.get
this.setMaybeRelBitLimit0b(MaybeULong(newRelLimit))
}
// after the old bufferedDOS has been completely written to the
// oldDirectDOS, there may have been a fragment byte left over. We must
// copy that fragment byte to the new directDOS
this.setFragmentLastByte(oldDirectDOS.fragmentLastByte, oldDirectDOS.fragmentLastByteLimit)
// lastly, as the direct stream, we no longer have a splitFrom that we look back at.
this.splitFrom = null
Assert.invariant(isDirect)
}
override def setFinished(finfo: FormatInfo) {
Assert.usage(!isFinished)
// if we are direct, and there's a buffer following this one
//
// we know it isn't finished (because of flush() above)
//
// It must take over being the direct one.
//
if (isDirect) {
var directStream = this
var keepMerging = true
while (directStream._following.isDefined && keepMerging) {
val first = directStream._following.get
keepMerging = first.isFinished // continue until AFTER we merge forward into the first non-finished successor
Assert.invariant(first.isBuffering)
log(LogLevel.Debug, "merging direct DOS %s into DOS %s", directStream, first)
val dabp = directStream.maybeAbsBitPos0b.getULong
if (first.maybeAbsStartingBitPos0b.isEmpty) {
first.setAbsStartingBitPos0b(dabp)
}
DirectOrBufferedDataOutputStream.deliverBufferContent(directStream, first, finfo) // from first, into direct stream's buffers
// so now the first one is an EMPTY not necessarily a finished buffered DOS
//
first.convertToDirect(directStream) // first is now the direct stream
directStream.setDOSState(Uninitialized) // old direct stream is now dead
directStream = first // long live the new direct stream!
log(LogLevel.Debug, "New direct DOS %s", directStream)
}
if (directStream._following.isDefined) {
Assert.invariant(!keepMerging) // we stopped because we merged forward into an active stream.
// that active stream isn't finished
Assert.invariant(directStream.isActive)
// we still have a following stream, but it might be finished or might still be active.
Assert.invariant(directStream._following.get.isActive ||
directStream._following.get.isFinished)
} else {
// nothing following, so we're setting finished at the very end of everything.
// However, the last thing we merged forward into may or may not be finished.
// So you can setFinished() on a stream, that stream becomes dead (state uninitialized),
// and the stream it merges forward into remains active. Funny, but no stream ends up in state "finished".
if (keepMerging) {
// the last stream we merged into was finished. So we're completely done.
// flush the final frag byte if there is one.
if (directStream.cst.fragmentLastByteLimit > 0) {
// must not omit the fragment byte on the end.
directStream.getJavaOutputStream().write(directStream.cst.fragmentLastByte)
// zero out so we don't end up thinking it is still there
directStream.cst.setFragmentLastByte(0, 0)
}
// Now flush the whole data output stream. Note that we only want to
// close the java output stream if it was one we created for
// layering. If it was not from a layer, then it is the underlying
// OutputStream from a user and they are responsible for closing it.
directStream.getJavaOutputStream().flush()
if (directStream.isLayer) {
directStream.getJavaOutputStream().close()
}
directStream.setDOSState(Uninitialized) // not just finished. We're dead now.
} else {
// the last stream we merged forward into was not finished.
Assert.invariant(directStream.isActive)
}
}
// that ends everything for a direct stream being set finished.
} else {
Assert.invariant(isBuffering)
//
// setFinished() on a unfinished buffered DOS
// we want to become read-only. So that after the
// setFinished, any bugs if someone still tries to
// operate on this, are caught.
//
// However, we don't merge forward, because that involves copying the bytes
// and we want to do that exactly once, which is when the direct DOS "catches up"
// and merges itself forward into all the buffered streams.
//
// But, we do need to propagate information about the absolute position
// of buffers.
//
setDOSState(Finished)
this.getJavaOutputStream().close()
if (_following.isDefined) {
val f = _following.get
f.maybeAbsBitPos0b // requesting this pulls the absolute position info forward.
}
}
}
/**
* This override implements a critical behavior, which is that when we ask for
* an absolute bit position, if we have it great. if we don't, we look at the
* prior DOS to see if it is finished and has an absolute bit position. If so
* that bit position becomes this DOS abs starting bit position, and then our
* absolute bit position is known.
*
* Without this behavior, it's possible for the unparse to hang, with every
* DOS chained together, but they all get finished in just the wrong order,
* and so the content or value length of something late in the data can't be
* determined that is needed to determine something early in the schema.
* Unless this absolute position information is propagated forward, everything
* can hang.
*
* Recursively this reaches backward until it finds a non-finished DOS or one
* that doesn't have absolute positioning information.
*
* I guess worst case this is a bad algorithm in that this could recurse
* deeply, going all the way back to the very start, over and over again.
* A better algorithm would depend on forward push of the absolute positioning
* information when setFinished occurs, which is, after all, the time when we
* can push such info forward.
*
* However, see setFinished comment. Where we setFinished and there is a following
* DOS we reach forward and ask that for its maybeAbsBitPos0b, which pulls the information
* forward by one DOS in the chain. So this chain should never be very long.
*/
override def maybeAbsBitPos0b: MaybeULong = {
val mSuper = super.maybeAbsBitPos0b
if (mSuper.isDefined)
mSuper
else if (splitFrom eq null) MaybeULong.Nope
else {
val prior = this.splitFrom
Assert.invariant(prior ne null)
Assert.invariant(prior._following.isDefined)
Assert.invariant(prior._following.get eq this)
if (prior.isFinished) {
// The prior is a finished DOS. If it (recursively) has a maybeAbsBitPos0b,
// then since it is finished, we can compute ours and save it.
val pmabp = prior.maybeAbsBitPos0b
if (pmabp.isDefined) {
val pabp = pmabp.getULong
this.setAbsStartingBitPos0b(pabp)
log(LogLevel.Debug, "for %s propagated absolute starting bit pos %s\n", this, pabp.toString)
super.maybeAbsBitPos0b // will get the right value this time.
} else {
// prior doesn't have an abs bit pos.
MaybeULong.Nope
}
} else {
// prior is not finished, so we don't know where we start yet
// and so can't compute an absolute bit pos yet.
MaybeULong.Nope
}
}
}
/**
* Always writes out at least 1 bit.
*/
final override protected def putLong_BE_MSBFirst(signedLong: Long, bitLengthFrom1To64: Int): Boolean = {
// Note: we don't have to check for bit limit. That check was already done.
//
// steps are
// add bits to the fragmentByte (if there is one)
// if the fragmentByte is full, write it.
// so now there is no fragment byte
// if we have more bits still to write, then
// do we have a multiple of 8 bits left (all whole bytes) or are we going to have a final fragment byte?
// shift long until MSB is first bit to be output
// for all whole bytes, take most-significant byte of the long, and write it out. shift << 8 bits
// set the fragment byte to the remaining most significant byte.
var nBitsRemaining = bitLengthFrom1To64
val mask = if (bitLengthFrom1To64 == 64) -1.toLong else (1.toLong << bitLengthFrom1To64) - 1
var bits = signedLong & mask
if (fragmentLastByteLimit > 0) {
//
// there is a frag byte, to which we are writing first.
// We will write at least 1 bit to the frag.
//
val nFragBitsAvailableToWrite = 8 - fragmentLastByteLimit
val nBitsOfFragToBeFilled =
if (bitLengthFrom1To64 >= nFragBitsAvailableToWrite) nFragBitsAvailableToWrite
else bitLengthFrom1To64
val nFragBitsAfter = fragmentLastByteLimit + nBitsOfFragToBeFilled // this can be 8 if we're going to fill all of the frag.
val bitsToGoIntoFrag = bits >> (bitLengthFrom1To64 - nBitsOfFragToBeFilled)
val bitsToGoIntoFragInPosition = bitsToGoIntoFrag << (8 - nFragBitsAfter)
val newFragByte = Bits.asUnsignedByte(fragmentLastByte | bitsToGoIntoFragInPosition)
Assert.invariant(newFragByte <= 255 && newFragByte >= 0)
val shift1 = 64 - (bitLengthFrom1To64 - nBitsOfFragToBeFilled)
bits = (bits << shift1) >>> shift1
nBitsRemaining = bitLengthFrom1To64 - nBitsOfFragToBeFilled
if (nFragBitsAfter == 8) {
// we filled the entire frag byte. Write it out, then zero it
realStream.write(newFragByte.toByte)
setFragmentLastByte(0, 0)
} else {
// we did not fill up the frag byte. We added bits to it (at least 1), but
// it's not filled up yet.
setFragmentLastByte(newFragByte.toInt, nFragBitsAfter)
}
}
// at this point we have bits and nBitsRemaining
Assert.invariant(nBitsRemaining >= 0)
if (nBitsRemaining == 0)
true // we are done
else {
// we have more bits to write. Could be as many as 64 still.
Assert.invariant(fragmentLastByteLimit == 0) // there is no frag byte.
val nWholeBytes = nBitsRemaining / 8
val nFragBits = nBitsRemaining % 8
// we want to shift the bits so that the 1st byte is in 0xFF00000000000000 position.
val shift = 64 - nBitsRemaining
var shiftedBits = bits << shift
var i = 0
while (i < nWholeBytes) {
val byt = shiftedBits >>> 56
Assert.invariant(byt <= 255)
realStream.write(byt.toByte)
shiftedBits = shiftedBits << 8
i += 1
}
if (nFragBits > 0) {
val newFragByte = shiftedBits >>> 56
setFragmentLastByte(newFragByte.toInt, nFragBits)
}
true
}
}
final override protected def putLong_LE_MSBFirst(signedLong: Long, bitLengthFrom1To64: Int): Boolean = {
// Note: we don't have to check for bit limit. That check was already done.
//
// LE_MSBF is most complicated of all.
// Frag byte contents must be shifted to MSB position
// But we take MSBs of the least-significant byte of the signedLong to put into that FragByte.
var bits = signedLong
//
// The long we're writing has a last byte (from byteOrder LittleEndian perspective).
// If this last byte is partial, we have to shift left to put the bits in the MSBs of
// that byte, since we're storing data MSBF.
//
val nWholeBytesAtStart = bitLengthFrom1To64 / 8
val nUsedBitsLastByte = (bitLengthFrom1To64 % 8)
val nUnusedBitsLastByte = if (nUsedBitsLastByte == 0) 0 else 8 - nUsedBitsLastByte
val indexOfLastByteLE = nWholeBytesAtStart - (if (nUnusedBitsLastByte > 0) 0 else 1)
unionLongBuffer.put(0, bits)
Bits.reverseBytes(unionByteBuffer)
// bytes are now in unionByteBuffer in LE order
val lastByte = unionByteBuffer.get(indexOfLastByteLE) // last byte is the most significant byte
val newLastByte = ((lastByte << nUnusedBitsLastByte) & 0xFF).toByte
unionByteBuffer.put(indexOfLastByteLE, newLastByte)
//
// bytes of the number are now in LE order, but with bits MSBF
//
var nBitsOfFragToBeFilled = 0
if (fragmentLastByteLimit > 0) {
//
// there is a frag byte, to which we are writing first.
// We will write at least 1 bit to the frag.
//
val nFragBitsAvailableToWrite = 8 - fragmentLastByteLimit
// the bits we're writing might not fill the frag, so the number
// we will fill is the lesser of the size of available space in the frag, and the bitLength argument.
nBitsOfFragToBeFilled =
if (bitLengthFrom1To64 >= nFragBitsAvailableToWrite) nFragBitsAvailableToWrite
else bitLengthFrom1To64
val nFragBitsAfter = fragmentLastByteLimit + nBitsOfFragToBeFilled // this can be 8 if we're going to fill all of the frag.
// Now get the bits that will go into the frag, from the least significant (first) byte.
val newFragBitsMask = (0x80.toByte >> (nBitsOfFragToBeFilled - 1)) & 0xFF
val LSByte = unionByteBuffer.get(0)
val bitsToGoIntoFragInPosition = (((LSByte & newFragBitsMask) & 0xFF) >>> fragmentLastByteLimit).toInt
val newFragByte = Bits.asUnsignedByte((fragmentLastByte | bitsToGoIntoFragInPosition).toByte)
Assert.invariant(newFragByte <= 255 && newFragByte >= 0)
if (nFragBitsAfter == 8) {
// we filled the entire frag byte. Write it out, then zero it
realStream.write(newFragByte.toByte)
setFragmentLastByte(0, 0)
} else {
// we did not fill up the frag byte. We added bits to it (at least 1), but
// it's not filled up yet.
setFragmentLastByte(newFragByte, nFragBitsAfter)
}
//
// Now we have to remove the bits that went into the
// current frag byte
//
// This is a strange operation. Were creating a long from the littleEndian bytes.
// The value of this will be very strange, but shifting left moves bits from more significant
// bytes into less significant bytes,
bits = unionLongBuffer.get(0)
bits = bits << nBitsOfFragToBeFilled
unionLongBuffer.put(0, bits)
}
//
// now we have the unionByteBuffer containing the correct LE bytes, in LE order.
//
val bitLengthRemaining = bitLengthFrom1To64 - nBitsOfFragToBeFilled
Assert.invariant(bitLengthRemaining >= 0)
if (bitLengthRemaining > 0) {
val nWholeBytesNow = bitLengthRemaining / 8
val nBitsInFinalFrag = bitLengthRemaining % 8
val indexOfFinalFragByte = nWholeBytesNow
var i = 0
while (i < nWholeBytesNow) {
realStream.write(unionByteBuffer.get(i))
i += 1
}
if (nBitsInFinalFrag > 0) {
val finalFragByte = Bits.asUnsignedByte(unionByteBuffer.get(indexOfFinalFragByte))
setFragmentLastByte(finalFragByte, nBitsInFinalFrag)
}
}
true
}
final override protected def putLong_LE_LSBFirst(signedLong: Long, bitLengthFrom1To64: Int): Boolean = {
// Note: we don't have to check for bit limit. That check was already done.
//
// Interestingly, LE_LSBF is slightly simpler than BE_MSBF as we don't have to shift bytes to get the
// bits into MSBF position.
//
// steps are
// add bits to the fragmentByte (if there is one)
// if the fragmentByte is full, write it.
// so now there is no fragment byte
// if we have more bits still to write, then
// do we have a multiple of 8 bits left (all whole bytes) or are we going to have a final fragment byte?
// for all whole bytes, take least-significant byte of the long, and write it out. shift >> 8 bits
// set the fragment byte to the remaining most significant byte.
var nBitsRemaining = bitLengthFrom1To64
var bits = signedLong
if (fragmentLastByteLimit > 0) {
//
// there is a frag byte, to which we are writing first.
// We will write at least 1 bit to the frag.
//
val nFragBitsAvailableToWrite = 8 - fragmentLastByteLimit
val nBitsOfFragToBeFilled =
if (bitLengthFrom1To64 >= nFragBitsAvailableToWrite) nFragBitsAvailableToWrite
else bitLengthFrom1To64
val nFragBitsAfter = fragmentLastByteLimit + nBitsOfFragToBeFilled // this can be 8 if we're going to fill all of the frag.
val fragLastByteMask = 0xFF >> (8 - nFragBitsAfter)
val bitsToGoIntoFragInPosition = ((bits << fragmentLastByteLimit) & fragLastByteMask).toInt
val newFragByte = fragmentLastByte | bitsToGoIntoFragInPosition
Assert.invariant(newFragByte <= 255 && newFragByte >= 0)
bits = bits >>> nBitsOfFragToBeFilled
nBitsRemaining = bitLengthFrom1To64 - nBitsOfFragToBeFilled
if (nFragBitsAfter == 8) {
// we filled the entire frag byte. Write it out, then zero it
realStream.write(newFragByte.toByte)
setFragmentLastByte(0, 0)
} else {
// we did not fill up the frag byte. We added bits to it (at least 1), but
// it's not filled up yet.
setFragmentLastByte(newFragByte, nFragBitsAfter)
}
}
// at this point we have bits and nBitsRemaining
Assert.invariant(nBitsRemaining >= 0)
if (nBitsRemaining == 0)
true // we are done
else {
// we have more bits to write. Could be as many as 64 still.
Assert.invariant(fragmentLastByteLimit == 0) // there is no frag byte.
val nWholeBytes = nBitsRemaining / 8
val nFragBits = nBitsRemaining % 8
val fragUsedBitsMask = ((1 << nFragBits) - 1)
var shiftedBits = bits
var i = 0
while (i < nWholeBytes) {
val byt = shiftedBits & 0xFF
realStream.write(byt.toByte)
shiftedBits = shiftedBits >>> 8
i += 1
}
if (nFragBits > 0) {
val newFragByte = Bits.asUnsignedByte((shiftedBits & fragUsedBitsMask).toByte)
setFragmentLastByte(newFragByte, nFragBits)
}
true
}
}
/**
* Convenience methods that temporarily set and (reliably) restore the bitLimit.
* The argument gives the limit length. Note this is a length, not a bit position.
*
* This is added to the current bit position to get the limiting bit position
* which is then set as the bitLimit when
* the body is evaluated. On return the bit limit is restored to its
* prior value.
*
* The return value is false if the new bit limit is beyond the existing bit limit range.
* Otherwise the return value is true.
*
* The prior value is restored even if an Error/Exception is thrown. (ie., via a try-finally)
*
* These are intended for use implementing specified-length types (simple or complex).
*
* Note that length limits in lengthUnits Characters are not implemented
* this way. See fillCharBuffer(cb) method.
*/
// private def withBitLengthLimit(lengthLimitInBits: Long)(body: => Unit): Boolean = macro IOMacros.withBitLengthLimitMacroForOutput
protected def deliverContent(directDOS: DirectOrBufferedDataOutputStream, finfo: FormatInfo) = {
val bufferNBits = this.relBitPos0b // don't have to subtract a starting offset. It's always zero in buffered case.
val bufOS = this.bufferingJOS
if (directDOS.isEndOnByteBoundary && this.isEndOnByteBoundary) {
// no fragment bytes anywhere - just take the bytes
val nBytes = (bufferNBits / 8).toInt
val nBytesPut = {
if (bufOS.isFile) {
bufOS.close
val nBitsPut = try {
directDOS.putFile(bufOS.getFile.toPath, bufferNBits.toLong, chunkSizeInBytes, finfo)
} finally {
// Make sure we delete the file after it was put in the directDOS or
// if we encountered an error
if (bufOS.isTempFile)
bufOS.getFile.delete()
}
nBitsPut / 8
} else
directDOS.putBytes(bufOS.getBuf, 0, nBytes, finfo)
}
Assert.invariant(nBytesPut == nBytes)
} else {
// fragment byte on directDOS, fragment byte on bufDOS, or both.
val nFragBits = this.fragmentLastByteLimit
val byteCount = bufferNBits / 8
val wholeBytesWritten = {
if (bufOS.isFile) {
bufOS.close
val nBitsPut = try {
directDOS.putFile(bufOS.getFile.toPath, bufferNBits.toLong, chunkSizeInBytes, finfo)
} finally {
// Make sure we delete the file after it was put in the directDOS or
// if we encountered an error
if (bufOS.isTempFile)
bufOS.getFile.delete()
}
nBitsPut / 8
} else
directDOS.putBytes(bufOS.getBuf, 0, byteCount.toInt, finfo)
}
Assert.invariant(byteCount == wholeBytesWritten)
if (nFragBits > 0) {
if (directDOS.isEndOnByteBoundary) {
// We cannot use putLong like below because it's possible that
// the fragment byte has a different bitOrder than the finfo
// passed in, since that came from a suspension. However, if
// the directDOS ended on a byte boundary, that means that its
// new fragment byte should be exactly the same as the buffered
// DOS fragment byte. So in this case, just copy the frag byte
// information from buffered to direct.
directDOS.setFragmentLastByte(this.fragmentLastByte, this.fragmentLastByteLimit)
} else {
// If the direct DOS wasn't byte aligned, then we need logic to
// write the buffered DOS fragment after the direct DOS
// fragment. Fortunately, putLong has all of this logic. Like
// above, the call to putLong potentially uses the wrong finfo
// since it may have come from a suspension. However, all that
// putLong really uses from the finfo is the bitOrder. And
// because the directDOS isn't byte aligned we know it must
// have the same bitOrder as the buffered DOS. So even though
// it could be the wrong format info, it's safe to use in this
// case.
val origfrag = this.fragmentLastByte
val fragNum =
if (finfo.bitOrder eq BitOrder.MostSignificantBitFirst)
origfrag >> (8 - nFragBits)
else
origfrag
Assert.invariant(directDOS.putLongUnchecked(
fragNum, nFragBits, finfo,
ignoreByteOrder=this.bufferingJOS.isFile))
}
}
}
bufOS.close()
}
/**
* Clean up any temporary files that were generated
*/
def cleanUp(): Unit = {
if (isBuffering && bufferingJOS.isTempFile)
bufferingJOS.getFile.delete()
while (_following.isDefined) {
_following.get.cleanUp()
}
}
}
/**
* Throw to indicate that bitOrder changed, but not on a byte boundary.
*
* Must be caught at higher level and turned into a RuntimeSDE where we have
* the context to do so.
*
* All calls to setFinished should, somewhere, be surrounded by a catch of this.
*/
class BitOrderChangeException(directDOS: DirectOrBufferedDataOutputStream, finfo: FormatInfo) extends Exception {
override def getMessage() = {
"Data output stream %s with bitOrder '%s' which is not on a byte boundary (%s bits past last byte boundary), cannot be populated with bitOrder '%s'.".format(
directDOS,
directDOS.priorBitOrder,
directDOS.fragmentLastByteLimit,
finfo.bitOrder)
}
}
class FileIOException(message: String) extends Exception(message)
object DirectOrBufferedDataOutputStream {
var nextLayerID = 0
/**
* This is over here to be sure it isn't operating on other members
* of the object. This operates on the arguments only.
*
* Delivers the bits of bufDOS into directDOS's output stream. Deals with the possibility that
* the directDOS ends with a fragment byte, or the bufDOS does, or both.
*/
private def deliverBufferContent(
directDOS: DirectOrBufferedDataOutputStream,
bufDOS: DirectOrBufferedDataOutputStream,
finfo: FormatInfo) {
Assert.invariant(bufDOS.isBuffering)
Assert.invariant(!directDOS.isBuffering)
val finfoBitOrder = finfo.bitOrder // bit order we are supposed to write with
val priorBitOrder = directDOS.cst.priorBitOrder // bit order that the directDOS had at last successful unparse. (prior is set after each unparser)
if (finfoBitOrder ne priorBitOrder) {
if ((bufDOS.relBitPos0b > ULong.Zero) &&
!directDOS.isEndOnByteBoundary) {
//
// If the bit order changes, it has to be on a byte boundary
// It's simply not meaningful for it to change otherwise.
//
throw new BitOrderChangeException(directDOS, finfo)
}
}
val newLengthLimit = bufDOS.relBitPos0b.toLong
val savedLengthLimit = directDOS.maybeRelBitLimit0b
if (directDOS.setMaybeRelBitLimit0b(MaybeULong(directDOS.relBitPos0b + newLengthLimit))) {
try {
bufDOS.deliverContent(directDOS, finfo)
//
// the buffered contents have now been output into directDOS
// but we don't need to change it or set it up for
// reuse as a buffered DOS, because whether it is in finished state
// or active state, we're about to morph it into being the direct DOS
//
} finally {
directDOS.resetMaybeRelBitLimit0b(savedLengthLimit)
}
}
}
/**
* Factory for creating new ones/
* Passing creator as null indicates no other stream created this one.
*/
def apply(
jos: java.io.OutputStream,
creator: DirectOrBufferedDataOutputStream,
isLayer: Boolean = false,
chunkSizeInBytes: Int,
maxBufferSizeInBytes: Long,
tempDirPath: File,
maybeExistingFile: Maybe[Path] = Maybe.Nope) = {
val dbdos = new DirectOrBufferedDataOutputStream(
creator,
isLayer,
chunkSizeInBytes,
maxBufferSizeInBytes,
tempDirPath,
maybeExistingFile)
dbdos.setJavaOutputStream(jos)
if (creator eq null) {
dbdos.setAbsStartingBitPos0b(ULong(0))
dbdos.setAbsStartingBitPos0b(ULong(0)) // yes. We do want to call this twice.
Assert.invariant(dbdos.isDirect)
}
dbdos
}
}