Copyright Microsoft Corporation
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.
/**
* Copyright Microsoft Corporation
*
* 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.
*/
package com.microsoft.azure.storage.file;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import com.microsoft.azure.storage.AccessCondition;
import com.microsoft.azure.storage.Constants;
import com.microsoft.azure.storage.DoesServiceRequest;
import com.microsoft.azure.storage.OperationContext;
import com.microsoft.azure.storage.StorageErrorCode;
import com.microsoft.azure.storage.StorageErrorCodeStrings;
import com.microsoft.azure.storage.StorageException;
import com.microsoft.azure.storage.core.Base64;
import com.microsoft.azure.storage.core.SR;
import com.microsoft.azure.storage.core.Utility;
Provides an input stream to read a given file resource.
/**
* Provides an input stream to read a given file resource.
*/
public class FileInputStream extends InputStream {
Holds the reference to the file this stream is associated with.
/**
* Holds the reference to the file this stream is associated with.
*/
private final CloudFile parentFileRef;
Holds the reference to the MD5 digest for the file.
/**
* Holds the reference to the MD5 digest for the file.
*/
private MessageDigest md5Digest;
A flag to determine if the stream is faulted, if so the last error will be thrown on next operation.
/**
* A flag to determine if the stream is faulted, if so the last error will be thrown on next operation.
*/
private volatile boolean streamFaulted;
Holds the last exception this stream encountered.
/**
* Holds the last exception this stream encountered.
*/
private IOException lastError;
Holds the OperationContext for the current stream.
/**
* Holds the OperationContext for the current stream.
*/
private final OperationContext opContext;
Holds the options for the current stream
/**
* Holds the options for the current stream
*/
private final FileRequestOptions options;
Holds the stream length.
/**
* Holds the stream length.
*/
private long streamLength = -1;
Holds the stream read size.
/**
* Holds the stream read size.
*/
private final int readSize;
A flag indicating if the File MD5 should be validated.
/**
* A flag indicating if the File MD5 should be validated.
*/
private boolean validateFileMd5;
Holds the File MD5.
/**
* Holds the File MD5.
*/
private final String retrievedContentMD5Value;
Holds the reference to the current buffered data.
/**
* Holds the reference to the current buffered data.
*/
private ByteArrayInputStream currentBuffer;
Holds an absolute byte position for the mark feature.
/**
* Holds an absolute byte position for the mark feature.
*/
private long markedPosition;
Holds the mark delta for which the mark position is expired.
/**
* Holds the mark delta for which the mark position is expired.
*/
private int markExpiry;
Holds an absolute byte position of the current read position.
/**
* Holds an absolute byte position of the current read position.
*/
private long currentAbsoluteReadPosition;
Holds the absolute byte position of the start of the current buffer.
/**
* Holds the absolute byte position of the start of the current buffer.
*/
private long bufferStartOffset;
Holds the length of the current buffer in bytes.
/**
* Holds the length of the current buffer in bytes.
*/
private int bufferSize;
Holds the AccessCondition
object that represents the access conditions for the file. /**
* Holds the {@link AccessCondition} object that represents the access conditions for the file.
*/
private AccessCondition accessCondition = null;
Initializes a new instance of the FileInputStream class.
Params: - parentFile – A
CloudFile
object which represents the file that this stream is associated with. - accessCondition – An
AccessCondition
object which represents the access conditions for the file. - options – A
FileRequestOptions
object which represents that specifies any additional options for the request. - opContext – An
OperationContext
object which is used to track the execution of the operation.
Throws: - StorageException –
An exception representing any error which occurred during the operation.
/**
* Initializes a new instance of the FileInputStream class.
*
* @param parentFile
* A {@link CloudFile} object which represents the file that this stream is associated with.
* @param accessCondition
* An {@link AccessCondition} object which represents the access conditions for the file.
* @param options
* A {@link FileRequestOptions} object which represents that specifies any additional options for the
* request.
* @param opContext
* An {@link OperationContext} object which is used to track the execution of the operation.
*
* @throws StorageException
* An exception representing any error which occurred during the operation.
*/
@DoesServiceRequest
protected FileInputStream(final CloudFile parentFile, final AccessCondition accessCondition,
final FileRequestOptions options, final OperationContext opContext) throws StorageException {
this.parentFileRef = parentFile;
this.options = new FileRequestOptions(options);
this.opContext = opContext;
this.streamFaulted = false;
this.currentAbsoluteReadPosition = 0;
this.readSize = parentFile.getStreamMinimumReadSizeInBytes();
if (options.getUseTransactionalContentMD5() && this.readSize > 4 * Constants.MB) {
throw new IllegalArgumentException(SR.INVALID_RANGE_CONTENT_MD5_HEADER);
}
parentFile.downloadAttributes(accessCondition, this.options, this.opContext);
this.retrievedContentMD5Value = parentFile.getProperties().getContentMD5();
// Will validate it if it was returned
this.validateFileMd5 = !options.getDisableContentMD5Validation()
&& !Utility.isNullOrEmpty(this.retrievedContentMD5Value);
String previousLeaseId = null;
if (accessCondition != null) {
previousLeaseId = accessCondition.getLeaseID();
}
this.accessCondition = AccessCondition.generateIfMatchCondition(this.parentFileRef.getProperties().getEtag());
this.accessCondition.setLeaseID(previousLeaseId);
this.streamLength = parentFile.getProperties().getLength();
if (this.validateFileMd5) {
try {
this.md5Digest = MessageDigest.getInstance("MD5");
}
catch (final NoSuchAlgorithmException e) {
// This wont happen, throw fatal.
throw Utility.generateNewUnexpectedStorageException(e);
}
}
this.reposition(0);
}
Returns an estimate of the number of bytes that can be read (or skipped over) from this input stream without
blocking by the next invocation of a method for this input stream. The next invocation might be the same thread
or another thread. A single read or skip of this many bytes will not block, but may read or skip fewer bytes.
Throws: - IOException –
If an I/O error occurs.
Returns: An int
which represents an estimate of the number of bytes that can be read (or skipped
over)
from this input stream without blocking, or 0 when it reaches the end of the input stream.
/**
* Returns an estimate of the number of bytes that can be read (or skipped over) from this input stream without
* blocking by the next invocation of a method for this input stream. The next invocation might be the same thread
* or another thread. A single read or skip of this many bytes will not block, but may read or skip fewer bytes.
*
* @return An <code>int</code> which represents an estimate of the number of bytes that can be read (or skipped
* over)
* from this input stream without blocking, or 0 when it reaches the end of the input stream.
*
* @throws IOException
* If an I/O error occurs.
*/
@Override
public synchronized int available() throws IOException {
return this.bufferSize - (int) (this.currentAbsoluteReadPosition - this.bufferStartOffset);
}
Helper function to check if the stream is faulted, if it is it surfaces the exception.
Throws: - IOException –
If an I/O error occurs. In particular, an IOException may be thrown if the output stream has been
closed.
/**
* Helper function to check if the stream is faulted, if it is it surfaces the exception.
*
* @throws IOException
* If an I/O error occurs. In particular, an IOException may be thrown if the output stream has been
* closed.
*/
private synchronized void checkStreamState() throws IOException {
if (this.streamFaulted) {
throw this.lastError;
}
}
Closes this input stream and releases any system resources associated with the stream.
Throws: - IOException –
If an I/O error occurs.
/**
* Closes this input stream and releases any system resources associated with the stream.
*
* @throws IOException
* If an I/O error occurs.
*/
@Override
public synchronized void close() throws IOException {
this.currentBuffer = null;
this.streamFaulted = true;
this.lastError = new IOException(SR.STREAM_CLOSED);
}
Dispatches a read operation of N bytes.
Params: - readLength –
An
int
which represents the number of bytes to read.
Throws: - IOException –
If an I/O error occurs.
/**
* Dispatches a read operation of N bytes.
*
* @param readLength
* An <code>int</code> which represents the number of bytes to read.
*
* @throws IOException
* If an I/O error occurs.
*/
@DoesServiceRequest
private synchronized void dispatchRead(final int readLength) throws IOException {
try {
final byte[] byteBuffer = new byte[readLength];
this.parentFileRef.downloadRangeInternal(this.currentAbsoluteReadPosition, (long) readLength, byteBuffer,
0, null /* this.accessCondition */, this.options, this.opContext);
// Check Etag manually for now -- use access condition once conditional headers supported.
if (this.accessCondition != null) {
if (!this.accessCondition.getIfMatch().equals(this.parentFileRef.getProperties().getEtag())) {
throw new StorageException(StorageErrorCode.CONDITION_FAILED.toString(),
SR.INVALID_CONDITIONAL_HEADERS, HttpURLConnection.HTTP_PRECON_FAILED, null, null);
}
}
this.currentBuffer = new ByteArrayInputStream(byteBuffer);
this.bufferSize = readLength;
this.bufferStartOffset = this.currentAbsoluteReadPosition;
}
catch (final StorageException e) {
this.streamFaulted = true;
this.lastError = Utility.initIOException(e);
throw this.lastError;
}
}
Marks the current position in this input stream. A subsequent call to the reset method repositions this stream at
the last marked position so that subsequent reads re-read the same bytes.
Params: - readlimit –
An
int
which represents the maximum limit of bytes that can be read before the mark
position becomes invalid.
/**
* Marks the current position in this input stream. A subsequent call to the reset method repositions this stream at
* the last marked position so that subsequent reads re-read the same bytes.
*
* @param readlimit
* An <code>int</code> which represents the maximum limit of bytes that can be read before the mark
* position becomes invalid.
*/
@Override
public synchronized void mark(final int readlimit) {
this.markedPosition = this.currentAbsoluteReadPosition;
this.markExpiry = readlimit;
}
Tests if this input stream supports the mark and reset methods. Whether or not mark and reset are supported is an invariant property of a particular input stream instance. The markSupported method of InputStream
returns false. Returns: True
if this stream instance supports the mark and reset methods; False
otherwise.
/**
* Tests if this input stream supports the mark and reset methods. Whether or not mark and reset are supported is an
* invariant property of a particular input stream instance. The markSupported method of {@link InputStream} returns
* false.
*
* @return <Code>True</Code> if this stream instance supports the mark and reset methods; <Code>False</Code>
* otherwise.
*/
@Override
public boolean markSupported() {
return true;
}
Reads the next byte of data from the input stream. The value byte is returned as an int in the range 0 to 255. If
no byte is available because the end of the stream has been reached, the value -1 is returned. This method blocks
until input data is available, the end of the stream is detected, or an exception is thrown.
Throws: - IOException –
If an I/O error occurs.
Returns: An int
which represents the total number of bytes read into the buffer, or -1 if
there is no more data because the end of the stream has been reached.
/**
* Reads the next byte of data from the input stream. The value byte is returned as an int in the range 0 to 255. If
* no byte is available because the end of the stream has been reached, the value -1 is returned. This method blocks
* until input data is available, the end of the stream is detected, or an exception is thrown.
*
* @return An <code>int</code> which represents the total number of bytes read into the buffer, or -1 if
* there is no more data because the end of the stream has been reached.
*
* @throws IOException
* If an I/O error occurs.
*/
@Override
@DoesServiceRequest
public int read() throws IOException {
final byte[] tBuff = new byte[1];
final int numberOfBytesRead = this.read(tBuff, 0, 1);
if (numberOfBytesRead > 0) {
return tBuff[0] & 0xFF;
}
else if (numberOfBytesRead == 0) {
throw new IOException(SR.UNEXPECTED_STREAM_READ_ERROR);
}
else {
return -1;
}
}
Reads some number of bytes from the input stream and stores them into the buffer array b
. The number
of bytes actually read is returned as an integer. This method blocks until input data is available, end of file
is detected, or an exception is thrown. If the length of b
is zero, then no bytes are read and 0 is
returned; otherwise, there is an attempt to read at least one byte. If no byte is available because the stream is
at the end of the file, the value -1 is returned; otherwise, at least one byte is read and stored into
b
.
The first byte read is stored into element b[0]
, the next one into b[1]
, and so on. The
number of bytes read is, at most, equal to the length of b
. Let k
be the number of
bytes actually read; these bytes will be stored in elements b[0]
through b[k-1]
,
leaving elements b[k]
through b[b.length-1]
unaffected.
The read(b)
method for class InputStream
has the same effect as: read(b, 0, b.length)
Params: - b –
A
byte
array which represents the buffer into which the data is read.
Throws: - IOException –
If the first byte cannot be read for any reason other than the end of the file, if the input stream
has been closed, or if some other I/O error occurs.
- NullPointerException –
If the
byte
array b
is null.
/**
* Reads some number of bytes from the input stream and stores them into the buffer array <code>b</code>. The number
* of bytes actually read is returned as an integer. This method blocks until input data is available, end of file
* is detected, or an exception is thrown. If the length of <code>b</code> is zero, then no bytes are read and 0 is
* returned; otherwise, there is an attempt to read at least one byte. If no byte is available because the stream is
* at the end of the file, the value -1 is returned; otherwise, at least one byte is read and stored into
* <code>b</code>.
*
* The first byte read is stored into element <code>b[0]</code>, the next one into <code>b[1]</code>, and so on. The
* number of bytes read is, at most, equal to the length of <code>b</code>. Let <code>k</code> be the number of
* bytes actually read; these bytes will be stored in elements <code>b[0]</code> through <code>b[k-1]</code>,
* leaving elements <code>b[k]</code> through <code>b[b.length-1]</code> unaffected.
*
* The <code>read(b)</code> method for class {@link InputStream} has the same effect as:
*
* <code>read(b, 0, b.length)</code>
*
* @param b
* A <code>byte</code> array which represents the buffer into which the data is read.
*
* @throws IOException
* If the first byte cannot be read for any reason other than the end of the file, if the input stream
* has been closed, or if some other I/O error occurs.
* @throws NullPointerException
* If the <code>byte</code> array <code>b</code> is null.
*/
@Override
@DoesServiceRequest
public int read(final byte[] b) throws IOException {
return this.read(b, 0, b.length);
}
Reads up to len
bytes of data from the input stream into an array of bytes. An attempt is made to
read as many as len
bytes, but a smaller number may be read. The number of bytes actually read is
returned as an integer. This method blocks until input data is available, end of file is detected, or an
exception is thrown.
If len
is zero, then no bytes are read and 0 is returned; otherwise, there is an attempt to read at
least one byte. If no byte is available because the stream is at end of file, the value -1 is returned;
otherwise, at least one byte is read and stored into b
.
The first byte read is stored into element b[off]
, the next one into b[off+1]
, and so
on. The number of bytes read is, at most, equal to len
. Let k
be the number of bytes
actually read; these bytes will be stored in elements b[off]
through b[off+k-1]
,
leaving elements b[off+k]
through b[off+len-1]
unaffected.
In every case, elements b[0]
through b[off]
and elements b[off+len]
through b[b.length-1]
are unaffected.
The read(b, off, len)
method for class InputStream
simply calls the method read()
repeatedly. If the first such call results in an IOException
, that exception is
returned from the call to the read(b, off, len)
method. If any subsequent call to
read()
results in a IOException
, the exception is caught and treated
as if it were end of file; the bytes read up to that point are stored into b
and the number of bytes
read before the exception occurred is returned. The default implementation of this method blocks until the
requested amount of input data len
has been read, end of file is detected, or an exception is
thrown. Subclasses are encouraged to provide a more efficient implementation of this method.
Params: - b –
A
byte
array which represents the buffer into which the data is read. - off –
An
int
which represents the start offset in the byte
array at which the data
is written. - len –
An
int
which represents the maximum number of bytes to read.
Throws: - IOException –
If the first byte cannot be read for any reason other than end of file, or if the input stream has
been closed, or if some other I/O error occurs.
- NullPointerException –
If the
byte
array b
is null. - IndexOutOfBoundsException –
If
off
is negative, len
is negative, or len
is greater than
b.length - off
.
Returns: An int
which represents the total number of bytes read into the buffer, or -1 if
there is no more data because the end of the stream has been reached.
/**
* Reads up to <code>len</code> bytes of data from the input stream into an array of bytes. An attempt is made to
* read as many as <code>len</code> bytes, but a smaller number may be read. The number of bytes actually read is
* returned as an integer. This method blocks until input data is available, end of file is detected, or an
* exception is thrown.
*
* If <code>len</code> is zero, then no bytes are read and 0 is returned; otherwise, there is an attempt to read at
* least one byte. If no byte is available because the stream is at end of file, the value -1 is returned;
* otherwise, at least one byte is read and stored into <code>b</code>.
*
* The first byte read is stored into element <code>b[off]</code>, the next one into <code>b[off+1]</code>, and so
* on. The number of bytes read is, at most, equal to <code>len</code>. Let <code>k</code> be the number of bytes
* actually read; these bytes will be stored in elements <code>b[off]</code> through <code>b[off+k-1]</code>,
* leaving elements <code>b[off+k]</code> through <code>b[off+len-1]</code> unaffected.
*
* In every case, elements <code>b[0]</code> through <code>b[off]</code> and elements <code>b[off+len]</code>
* through <code>b[b.length-1]</code> are unaffected.
*
* The <code>read(b, off, len)</code> method for class {@link InputStream} simply calls the method
* <code>read()</code> repeatedly. If the first such call results in an <code>IOException</code>, that exception is
* returned from the call to the <code>read(b, off, len)</code> method. If any subsequent call to
* <code>read()</code> results in a <code>IOException</code>, the exception is caught and treated
* as if it were end of file; the bytes read up to that point are stored into <code>b</code> and the number of bytes
* read before the exception occurred is returned. The default implementation of this method blocks until the
* requested amount of input data <code>len</code> has been read, end of file is detected, or an exception is
* thrown. Subclasses are encouraged to provide a more efficient implementation of this method.
*
* @param b
* A <code>byte</code> array which represents the buffer into which the data is read.
* @param off
* An <code>int</code> which represents the start offset in the <code>byte</code> array at which the data
* is written.
* @param len
* An <code>int</code> which represents the maximum number of bytes to read.
*
* @return An <code>int</code> which represents the total number of bytes read into the buffer, or -1 if
* there is no more data because the end of the stream has been reached.
*
* @throws IOException
* If the first byte cannot be read for any reason other than end of file, or if the input stream has
* been closed, or if some other I/O error occurs.
* @throws NullPointerException
* If the <code>byte</code> array <code>b</code> is null.
* @throws IndexOutOfBoundsException
* If <code>off</code> is negative, <code>len</code> is negative, or <code>len</code> is greater than
* <code>b.length - off</code>.
*/
@Override
@DoesServiceRequest
public int read(final byte[] b, final int off, final int len) throws IOException {
if (off < 0 || len < 0 || len > b.length - off) {
throw new IndexOutOfBoundsException();
}
return this.readInternal(b, off, len);
}
Performs internal read to the given byte buffer.
Params: - b –
A
byte
array which represents the buffer into which the data is read. - off –
An
int
which represents the start offset in the byte
array b
at
which the data is written. - len –
An
int
which represents the maximum number of bytes to read.
Throws: - IOException –
If the first byte cannot be read for any reason other than end of file, or if the input stream has
been closed, or if some other I/O error occurs.
Returns: An int
which represents the total number of bytes read into the buffer, or -1 if
there is no more data because the end of the stream has been reached.
/**
* Performs internal read to the given byte buffer.
*
* @param b
* A <code>byte</code> array which represents the buffer into which the data is read.
* @param off
* An <code>int</code> which represents the start offset in the <code>byte</code> array <code>b</code> at
* which the data is written.
* @param len
* An <code>int</code> which represents the maximum number of bytes to read.
*
* @return An <code>int</code> which represents the total number of bytes read into the buffer, or -1 if
* there is no more data because the end of the stream has been reached.
*
* @throws IOException
* If the first byte cannot be read for any reason other than end of file, or if the input stream has
* been closed, or if some other I/O error occurs.
*/
@DoesServiceRequest
private synchronized int readInternal(final byte[] b, final int off, int len) throws IOException {
this.checkStreamState();
// if buffer is empty do next get operation
if ((this.currentBuffer == null || this.currentBuffer.available() == 0)
&& this.currentAbsoluteReadPosition < this.streamLength) {
this.dispatchRead((int) Math.min(this.readSize, this.streamLength - this.currentAbsoluteReadPosition));
}
len = Math.min(len, this.readSize);
// do read from buffer
final int numberOfBytesRead = this.currentBuffer.read(b, off, len);
if (numberOfBytesRead > 0) {
this.currentAbsoluteReadPosition += numberOfBytesRead;
if (this.validateFileMd5) {
this.md5Digest.update(b, off, numberOfBytesRead);
if (this.currentAbsoluteReadPosition == this.streamLength) {
// Reached end of stream, validate md5.
final String calculatedMd5 = Base64.encode(this.md5Digest.digest());
if (!calculatedMd5.equals(this.retrievedContentMD5Value)) {
this.lastError = Utility
.initIOException(new StorageException(
StorageErrorCodeStrings.INVALID_MD5,
String.format(
"File data corrupted (integrity check failed), Expected value is %s, retrieved %s",
this.retrievedContentMD5Value, calculatedMd5),
Constants.HeaderConstants.HTTP_UNUSED_306, null, null));
this.streamFaulted = true;
throw this.lastError;
}
}
}
}
// update markers
if (this.markExpiry > 0 && this.markedPosition + this.markExpiry < this.currentAbsoluteReadPosition) {
this.markedPosition = 0;
this.markExpiry = 0;
}
return numberOfBytesRead;
}
Repositions the stream to the given absolute byte offset.
Params: - absolutePosition –
A
long
which represents the absolute byte offset withitn the stream reposition.
/**
* Repositions the stream to the given absolute byte offset.
*
* @param absolutePosition
* A <code>long</code> which represents the absolute byte offset withitn the stream reposition.
*/
private synchronized void reposition(final long absolutePosition) {
this.currentAbsoluteReadPosition = absolutePosition;
this.currentBuffer = new ByteArrayInputStream(new byte[0]);
}
Repositions this stream to the position at the time the mark method was last called on this input stream. Note
repositioning the file read stream will disable file MD5 checking.
Throws: - IOException –
If this stream has not been marked or if the mark has been invalidated.
/**
* Repositions this stream to the position at the time the mark method was last called on this input stream. Note
* repositioning the file read stream will disable file MD5 checking.
*
* @throws IOException
* If this stream has not been marked or if the mark has been invalidated.
*/
@Override
public synchronized void reset() throws IOException {
if (this.markedPosition + this.markExpiry < this.currentAbsoluteReadPosition) {
throw new IOException(SR.MARK_EXPIRED);
}
this.validateFileMd5 = false;
this.md5Digest = null;
this.reposition(this.markedPosition);
}
Skips over and discards n bytes of data from this input stream. The skip method may, for a variety of reasons,
end up skipping over some smaller number of bytes, possibly 0. This may result from any of a number of
conditions; reaching end of file before n bytes have been skipped is only one possibility. The actual number of
bytes skipped is returned. If n is negative, no bytes are skipped.
Note repositioning the file read stream will disable file MD5 checking.
Params: - n –
A
long
which represents the number of bytes to skip.
/**
* Skips over and discards n bytes of data from this input stream. The skip method may, for a variety of reasons,
* end up skipping over some smaller number of bytes, possibly 0. This may result from any of a number of
* conditions; reaching end of file before n bytes have been skipped is only one possibility. The actual number of
* bytes skipped is returned. If n is negative, no bytes are skipped.
*
* Note repositioning the file read stream will disable file MD5 checking.
*
* @param n
* A <code>long</code> which represents the number of bytes to skip.
*/
@Override
public synchronized long skip(final long n) throws IOException {
if (n == 0) {
return 0;
}
if (n < 0 || this.currentAbsoluteReadPosition + n > this.streamLength) {
throw new IndexOutOfBoundsException();
}
this.validateFileMd5 = false;
this.md5Digest = null;
this.reposition(this.currentAbsoluteReadPosition + n);
return n;
}
}