Introduce alternative Buffer API (#11347)
Motivation:
In Netty 5 we wish to have a simpler, safe, future proof, and more consistent buffer API.
We developed such an API in the incubating buffer repository, and taking it through multiple rounds of review and adjustments.
This PR/commit bring the results of that work into the Netty 5 branch of the main Netty repository.
Modifications:
* `Buffer` is an interface, and all implementations are hidden behind it.
There is no longer an inheritance hierarchy of abstract classes and implementations.
* Reference counting is gone.
After a buffer has been allocated, calling `close` on it will deallocate it.
It is then up to users and integrators to ensure that the life-times of buffers are managed correctly.
This is usually not a problem as buffers tend to flow through the pipeline to be released after a terminal IO operation.
* Slice and duplicate methods are replaced with `split`.
By removing slices, duplicate, and reference counting, there is no longer a possibility that a buffer and/or its memory can be shared and accessible through multiple routes.
This solves the problem of data being accessed from multiple places in an uncoordinated way, and the problem of buffer memory being closed while being in use by some unsuspecting piece of code.
Some adjustments will have to be made to other APIs, idioms, and usages, since `split` is not always a replacement for `slice` in some use cases.
* The `split` has been added which allows memory to be shared among multiple buffers, but in non-overlapping regions.
When the memory regions don't overlap, it will not be possible for the different buffers to interfere with each other.
An internal, and completely transparent, reference counting system ensures that the backing memory is released once the last buffer view is closed.
* A Send API has been introduced that can be used to enforce (in the type system) the transfer of buffer ownership.
This is not expected to be used in the pipeline flow itself, but rather for other objects that wrap buffers and wish to avoid becoming "shared views" — the absence of "shared views" of memory is important for avoiding bugs in the absence of reference counting.
* A new BufferAllocator API, where the choice of implementation determines factors like on-/off-heap, pooling or not.
How access to the different allocators will be exposed to integrators will be decided later.
Perhaps they'll be directly accessible on the `ChannelHandlerContext`.
* The `PooledBufferAllocator` has been copied and modified to match the new allocator API.
This includes unifying its implementation that was previously split across on-heap and off-heap.
* The `PooledBufferAllocator` implementation has also been adjusted to allocate 4 MiB chunks by default, and a few changes have been made to the implementation to make a newly created, empty allocator use significantly less heap memory.
* A `Resource` interface has been added, which defines the life-cycle methods and the `send` method.
The `Buffer` interface extends this.
* Analogues for `ByteBufHolder` has been added in the `BufferHolder` and `BufferRef` classes.
* `ByteCursor` is added as a new way to iterate the data in buffers.
The byte cursor API is designed to be more JIT friendly than an iterator, or the existing `ByteProcessor` interface.
* `CompositeBuffer` no longer permit the same level of access to its internal components.
The composite buffer enforces its ownership of its components via the `Send` API, and the components can only be individually accessed with the `forEachReadable` and `forEachWritable` methods.
This keeps the API and behavioral differences between composite and non-composite buffers to a minimum.
* Two implementations of the `Buffer` interface are provided with the API: One based on `ByteBuffer`, and one based on `sun.misc.Unsafe`.
The `ByteBuffer` implementation is used by default.
More implementations can be loaded from the classpath via service loading.
The `MemorySegment` based implementation is left behind in the incubator repository.
* An extensive and highly parameterised test suite has been added, to ensure that all implementations have consistent and correct behaviour, regardless of their configuration or composition.
Result:
We have a new buffer API that is simpler, better tested, more consistent in behaviour, and safer by design, than the existing `ByteBuf` API.
The next legs of this journey will be about integrating this new API into Netty proper, and deprecate (and eventually remove) the `ByteBuf` API.
This fixes #11024, #8601, #8543, #8542, #8534, #3358, and #3306.
2021-06-28 12:06:44 +02:00
|
|
|
/*
|
|
|
|
* Copyright 2021 The Netty Project
|
|
|
|
*
|
|
|
|
* The Netty Project 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:
|
|
|
|
*
|
|
|
|
* https://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.
|
|
|
|
*/
|
2021-09-17 16:28:14 +02:00
|
|
|
package io.net5.buffer.api;
|
Introduce alternative Buffer API (#11347)
Motivation:
In Netty 5 we wish to have a simpler, safe, future proof, and more consistent buffer API.
We developed such an API in the incubating buffer repository, and taking it through multiple rounds of review and adjustments.
This PR/commit bring the results of that work into the Netty 5 branch of the main Netty repository.
Modifications:
* `Buffer` is an interface, and all implementations are hidden behind it.
There is no longer an inheritance hierarchy of abstract classes and implementations.
* Reference counting is gone.
After a buffer has been allocated, calling `close` on it will deallocate it.
It is then up to users and integrators to ensure that the life-times of buffers are managed correctly.
This is usually not a problem as buffers tend to flow through the pipeline to be released after a terminal IO operation.
* Slice and duplicate methods are replaced with `split`.
By removing slices, duplicate, and reference counting, there is no longer a possibility that a buffer and/or its memory can be shared and accessible through multiple routes.
This solves the problem of data being accessed from multiple places in an uncoordinated way, and the problem of buffer memory being closed while being in use by some unsuspecting piece of code.
Some adjustments will have to be made to other APIs, idioms, and usages, since `split` is not always a replacement for `slice` in some use cases.
* The `split` has been added which allows memory to be shared among multiple buffers, but in non-overlapping regions.
When the memory regions don't overlap, it will not be possible for the different buffers to interfere with each other.
An internal, and completely transparent, reference counting system ensures that the backing memory is released once the last buffer view is closed.
* A Send API has been introduced that can be used to enforce (in the type system) the transfer of buffer ownership.
This is not expected to be used in the pipeline flow itself, but rather for other objects that wrap buffers and wish to avoid becoming "shared views" — the absence of "shared views" of memory is important for avoiding bugs in the absence of reference counting.
* A new BufferAllocator API, where the choice of implementation determines factors like on-/off-heap, pooling or not.
How access to the different allocators will be exposed to integrators will be decided later.
Perhaps they'll be directly accessible on the `ChannelHandlerContext`.
* The `PooledBufferAllocator` has been copied and modified to match the new allocator API.
This includes unifying its implementation that was previously split across on-heap and off-heap.
* The `PooledBufferAllocator` implementation has also been adjusted to allocate 4 MiB chunks by default, and a few changes have been made to the implementation to make a newly created, empty allocator use significantly less heap memory.
* A `Resource` interface has been added, which defines the life-cycle methods and the `send` method.
The `Buffer` interface extends this.
* Analogues for `ByteBufHolder` has been added in the `BufferHolder` and `BufferRef` classes.
* `ByteCursor` is added as a new way to iterate the data in buffers.
The byte cursor API is designed to be more JIT friendly than an iterator, or the existing `ByteProcessor` interface.
* `CompositeBuffer` no longer permit the same level of access to its internal components.
The composite buffer enforces its ownership of its components via the `Send` API, and the components can only be individually accessed with the `forEachReadable` and `forEachWritable` methods.
This keeps the API and behavioral differences between composite and non-composite buffers to a minimum.
* Two implementations of the `Buffer` interface are provided with the API: One based on `ByteBuffer`, and one based on `sun.misc.Unsafe`.
The `ByteBuffer` implementation is used by default.
More implementations can be loaded from the classpath via service loading.
The `MemorySegment` based implementation is left behind in the incubator repository.
* An extensive and highly parameterised test suite has been added, to ensure that all implementations have consistent and correct behaviour, regardless of their configuration or composition.
Result:
We have a new buffer API that is simpler, better tested, more consistent in behaviour, and safer by design, than the existing `ByteBuf` API.
The next legs of this journey will be about integrating this new API into Netty proper, and deprecate (and eventually remove) the `ByteBuf` API.
This fixes #11024, #8601, #8543, #8542, #8534, #3358, and #3306.
2021-06-28 12:06:44 +02:00
|
|
|
|
|
|
|
import java.nio.ByteBuffer;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* A stub of a {@link Buffer} implementation that implements all buffer methods by delegating them to a wrapped buffer
|
|
|
|
* instance.
|
|
|
|
* <p>
|
|
|
|
* This can be used when writing automated tests for code that integrates with {@link Buffer}, but should not be used in
|
|
|
|
* production code.
|
|
|
|
*/
|
|
|
|
public class BufferStub implements Buffer {
|
|
|
|
protected final Buffer delegate;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a new buffer stub that delegates all calls to the given instance.
|
|
|
|
*
|
|
|
|
* @param delegate The buffer instance to delegate all method calls to.
|
|
|
|
*/
|
|
|
|
public BufferStub(Buffer delegate) {
|
|
|
|
this.delegate = delegate;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public int capacity() {
|
|
|
|
return delegate.capacity();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public int readerOffset() {
|
|
|
|
return delegate.readerOffset();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer readerOffset(int offset) {
|
|
|
|
return delegate.readerOffset(offset);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public int writerOffset() {
|
|
|
|
return delegate.writerOffset();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer writerOffset(int offset) {
|
|
|
|
return delegate.writerOffset(offset);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public int readableBytes() {
|
|
|
|
return delegate.readableBytes();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public int writableBytes() {
|
|
|
|
return delegate.writableBytes();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer fill(byte value) {
|
|
|
|
return delegate.fill(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer makeReadOnly() {
|
|
|
|
return delegate.makeReadOnly();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean readOnly() {
|
|
|
|
return delegate.readOnly();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void copyInto(int srcPos, byte[] dest, int destPos, int length) {
|
|
|
|
delegate.copyInto(srcPos, dest, destPos, length);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void copyInto(int srcPos, ByteBuffer dest, int destPos, int length) {
|
|
|
|
delegate.copyInto(srcPos, dest, destPos, length);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void copyInto(int srcPos, Buffer dest, int destPos, int length) {
|
|
|
|
delegate.copyInto(srcPos, dest, destPos, length);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer writeBytes(Buffer source) {
|
|
|
|
return delegate.writeBytes(source);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer writeBytes(byte[] source) {
|
|
|
|
return delegate.writeBytes(source);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer resetOffsets() {
|
|
|
|
return delegate.resetOffsets();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public ByteCursor openCursor() {
|
|
|
|
return delegate.openCursor();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public ByteCursor openCursor(int fromOffset, int length) {
|
|
|
|
return delegate.openCursor(fromOffset, length);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public ByteCursor openReverseCursor() {
|
|
|
|
return delegate.openReverseCursor();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public ByteCursor openReverseCursor(int fromOffset, int length) {
|
|
|
|
return delegate.openReverseCursor(fromOffset, length);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer ensureWritable(int size) {
|
|
|
|
return delegate.ensureWritable(size);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer ensureWritable(int size, int minimumGrowth, boolean allowCompaction) {
|
|
|
|
return delegate.ensureWritable(size, minimumGrowth, allowCompaction);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer copy() {
|
|
|
|
return delegate.copy();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer copy(int offset, int length) {
|
|
|
|
return delegate.copy(offset, length);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer split() {
|
|
|
|
return delegate.split();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer split(int splitOffset) {
|
|
|
|
return delegate.split(splitOffset);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer compact() {
|
|
|
|
return delegate.compact();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public int countComponents() {
|
|
|
|
return delegate.countComponents();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public int countReadableComponents() {
|
|
|
|
return delegate.countReadableComponents();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public int countWritableComponents() {
|
|
|
|
return delegate.countWritableComponents();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public <E extends Exception> int forEachReadable(int initialIndex,
|
|
|
|
ReadableComponentProcessor<E> processor) throws E {
|
|
|
|
return delegate.forEachReadable(initialIndex, processor);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public <E extends Exception> int forEachWritable(int initialIndex,
|
|
|
|
WritableComponentProcessor<E> processor) throws E {
|
|
|
|
return delegate.forEachWritable(initialIndex, processor);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public byte readByte() {
|
|
|
|
return delegate.readByte();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public byte getByte(int roff) {
|
|
|
|
return delegate.getByte(roff);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public int readUnsignedByte() {
|
|
|
|
return delegate.readUnsignedByte();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public int getUnsignedByte(int roff) {
|
|
|
|
return delegate.getUnsignedByte(roff);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer writeByte(byte value) {
|
|
|
|
return delegate.writeByte(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer setByte(int woff, byte value) {
|
|
|
|
return delegate.setByte(woff, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer writeUnsignedByte(int value) {
|
|
|
|
return delegate.writeUnsignedByte(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer setUnsignedByte(int woff, int value) {
|
|
|
|
return delegate.setUnsignedByte(woff, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public char readChar() {
|
|
|
|
return delegate.readChar();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public char getChar(int roff) {
|
|
|
|
return delegate.getChar(roff);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer writeChar(char value) {
|
|
|
|
return delegate.writeChar(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer setChar(int woff, char value) {
|
|
|
|
return delegate.setChar(woff, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public short readShort() {
|
|
|
|
return delegate.readShort();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public short getShort(int roff) {
|
|
|
|
return delegate.getShort(roff);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public int readUnsignedShort() {
|
|
|
|
return delegate.readUnsignedShort();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public int getUnsignedShort(int roff) {
|
|
|
|
return delegate.getUnsignedShort(roff);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer writeShort(short value) {
|
|
|
|
return delegate.writeShort(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer setShort(int woff, short value) {
|
|
|
|
return delegate.setShort(woff, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer writeUnsignedShort(int value) {
|
|
|
|
return delegate.writeUnsignedShort(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer setUnsignedShort(int woff, int value) {
|
|
|
|
return delegate.setUnsignedShort(woff, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public int readMedium() {
|
|
|
|
return delegate.readMedium();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public int getMedium(int roff) {
|
|
|
|
return delegate.getMedium(roff);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public int readUnsignedMedium() {
|
|
|
|
return delegate.readUnsignedMedium();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public int getUnsignedMedium(int roff) {
|
|
|
|
return delegate.getUnsignedMedium(roff);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer writeMedium(int value) {
|
|
|
|
return delegate.writeMedium(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer setMedium(int woff, int value) {
|
|
|
|
return delegate.setMedium(woff, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer writeUnsignedMedium(int value) {
|
|
|
|
return delegate.writeUnsignedMedium(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer setUnsignedMedium(int woff, int value) {
|
|
|
|
return delegate.setUnsignedMedium(woff, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public int readInt() {
|
|
|
|
return delegate.readInt();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public int getInt(int roff) {
|
|
|
|
return delegate.getInt(roff);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public long readUnsignedInt() {
|
|
|
|
return delegate.readUnsignedInt();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public long getUnsignedInt(int roff) {
|
|
|
|
return delegate.getUnsignedInt(roff);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer writeInt(int value) {
|
|
|
|
return delegate.writeInt(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer setInt(int woff, int value) {
|
|
|
|
return delegate.setInt(woff, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer writeUnsignedInt(long value) {
|
|
|
|
return delegate.writeUnsignedInt(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer setUnsignedInt(int woff, long value) {
|
|
|
|
return delegate.setUnsignedInt(woff, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public float readFloat() {
|
|
|
|
return delegate.readFloat();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public float getFloat(int roff) {
|
|
|
|
return delegate.getFloat(roff);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer writeFloat(float value) {
|
|
|
|
return delegate.writeFloat(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer setFloat(int woff, float value) {
|
|
|
|
return delegate.setFloat(woff, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public long readLong() {
|
|
|
|
return delegate.readLong();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public long getLong(int roff) {
|
|
|
|
return delegate.getLong(roff);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer writeLong(long value) {
|
|
|
|
return delegate.writeLong(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer setLong(int woff, long value) {
|
|
|
|
return delegate.setLong(woff, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public double readDouble() {
|
|
|
|
return delegate.readDouble();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public double getDouble(int roff) {
|
|
|
|
return delegate.getDouble(roff);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer writeDouble(double value) {
|
|
|
|
return delegate.writeDouble(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Buffer setDouble(int woff, double value) {
|
|
|
|
return delegate.setDouble(woff, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public Send<Buffer> send() {
|
|
|
|
return delegate.send();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public void close() {
|
|
|
|
delegate.close();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Override
|
|
|
|
public boolean isAccessible() {
|
|
|
|
return delegate.isAccessible();
|
|
|
|
}
|
|
|
|
}
|