// --------------------------------------------------------- Public Methods
/** *Numberofkeep-alivesockets. * *@returnAlwaysreturns-1.
*/ publicint getKeepAliveCount() { // For this connector, only the overall connection count is relevant return -1;
}
// ----------------------------------------------- Public Lifecycle Methods
@Override protectedvoid startAcceptorThread() { // Instead of starting a real acceptor thread, this will instead call // an asynchronous accept operation if (acceptor == null) {
acceptor = new Nio2Acceptor(this);
acceptor.setThreadName(getName() + "-Acceptor");
}
acceptor.state = AcceptorState.RUNNING;
getExecutor().execute(acceptor);
}
/** *Stoptheendpoint.Thiswillcauseallprocessingthreadstostop.
*/
@Override publicvoid stopInternal() { if (!paused) {
pause();
} if (running) {
running = false;
acceptor.stop(10); // Use the executor to avoid binding the main thread if something bad // occurs and unbind will also wait for a bit for it to complete
getExecutor().execute(() -> { // Then close all active connections if any remain try { for (SocketWrapperBase<Nio2Channel> wrapper : getConnections()) {
wrapper.close();
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
} finally {
allClosed = true;
}
}); if (nioChannels != null) {
Nio2Channel socket; while ((socket = nioChannels.pop()) != null) {
socket.free();
}
nioChannels = null;
} if (processorCache != null) {
processorCache.clear();
processorCache = null;
}
}
}
/** *DeallocateNIOmemorypools,andcloseserversocket.
*/
@Override publicvoid unbind() throws Exception { if (running) {
stop();
}
doCloseServerSocket();
destroySsl(); super.unbind(); // Unlike other connectors, the thread pool is tied to the server socket
shutdownExecutor(); if (getHandler() != null) {
getHandler().recycle();
}
}
@Override protectedvoid doCloseServerSocket() throws IOException { // Close server socket if (serverSock != null) {
serverSock.close();
serverSock = null;
}
}
/** *Processthespecifiedconnection. *@paramsocketThesocketchannel *@return<code>true</code>ifthesocketwascorrectlyconfigured *andprocessingmaycontinue,<code>false</code>ifthesocketneedstobe *closeimmediately
*/
@Override protectedboolean setSocketOptions(AsynchronousSocketChannel socket) {
Nio2SocketWrapper socketWrapper = null; try { // Allocate channel and wrapper
Nio2Channel channel = null; if (nioChannels != null) {
channel = nioChannels.pop();
} if (channel == null) {
SocketBufferHandler bufhandler = new SocketBufferHandler(
socketProperties.getAppReadBufSize(),
socketProperties.getAppWriteBufSize(),
socketProperties.getDirectBuffer()); if (isSSLEnabled()) {
channel = new SecureNio2Channel(bufhandler, this);
} else {
channel = new Nio2Channel(bufhandler);
}
}
Nio2SocketWrapper newWrapper = new Nio2SocketWrapper(channel, this);
channel.reset(socket, newWrapper);
connections.put(socket, newWrapper);
socketWrapper = newWrapper;
// Set socket properties
socketProperties.setProperties(socket);
socketWrapper.setReadTimeout(getConnectionTimeout());
socketWrapper.setWriteTimeout(getConnectionTimeout());
socketWrapper.setKeepAliveLeft(Nio2Endpoint.this.getMaxKeepAliveRequests()); // Continue processing on the same thread as the acceptor is async return processSocket(socketWrapper, SocketEvent.OPEN_READ, false);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
log.error(sm.getString("endpoint.socketOptionsError"), t); if (socketWrapper == null) {
destroySocket(socket);
}
} // Tell to close the socket if needed returnfalse;
}
public Nio2Acceptor(AbstractEndpoint<?, AsynchronousSocketChannel> endpoint) { super(endpoint);
}
@Override publicvoid run() { // The initial accept will be called in a separate utility thread if (!isPaused()) { //if we have reached max connections, wait try {
countUpOrAwaitConnection();
} catch (InterruptedException e) { // Ignore
} if (!isPaused()) { // Note: as a special behavior, the completion handler for accept is // always called in a separate thread.
serverSock.accept(null, this);
} else {
state = AcceptorState.PAUSED;
}
} else {
state = AcceptorState.PAUSED;
}
}
@Override publicvoid completed(AsynchronousSocketChannel socket, Void attachment) { // Successful accept, reset the error delay
errorDelay = 0; // Continue processing the socket on the current thread // Configure the socket if (isRunning() && !isPaused()) { if (getMaxConnections() == -1) {
serverSock.accept(null, this);
} elseif (getConnectionCount() < getMaxConnections()) { try { // This will not block
countUpOrAwaitConnection();
} catch (InterruptedException e) { // Ignore
}
serverSock.accept(null, this);
} else { // Accept again on a new thread since countUpOrAwaitConnection may block
getExecutor().execute(this);
} if (!setSocketOptions(socket)) {
closeSocket(socket);
}
} else { if (isRunning()) {
state = AcceptorState.PAUSED;
}
destroySocket(socket);
}
}
@Override publicvoid failed(Throwable t, Void attachment) { if (isRunning()) { if (!isPaused()) { if (getMaxConnections() == -1) {
serverSock.accept(null, this);
} else { // Accept again on a new thread since countUpOrAwaitConnection may block
getExecutor().execute(this);
}
} else {
state = AcceptorState.PAUSED;
} // We didn't get a socket
countDownConnection(); // Introduce delay if necessary
errorDelay = handleExceptionWithDelay(errorDelay);
ExceptionUtils.handleThrowable(t);
log.error(sm.getString("endpoint.accept.fail"), t);
} else { // We didn't get a socket
countDownConnection();
}
}
public Nio2SocketWrapper(Nio2Channel channel, final Nio2Endpoint endpoint) { super(channel, endpoint);
nioChannels = endpoint.getNioChannels();
socketBufferHandler = channel.getBufHandler();
this.readCompletionHandler = new CompletionHandler<>() {
@Override publicvoid completed(Integer nBytes, ByteBuffer attachment) { if (log.isDebugEnabled()) {
log.debug("Socket: [" + Nio2SocketWrapper.this + "], Interest: [" + readInterest + "]");
} boolean notify = false; synchronized (readCompletionHandler) {
readNotify = false; if (nBytes.intValue() < 0) {
failed(new EOFException(), attachment);
} else { if (readInterest && !isInline()) {
readNotify = true;
} else { // Release here since there will be no // notify/dispatch to do the release.
readPending.release();
}
readInterest = false;
}
notify = readNotify;
} if (notify) {
getEndpoint().processSocket(Nio2SocketWrapper.this, SocketEvent.OPEN_READ, false);
}
}
@Override publicvoid failed(Throwable exc, ByteBuffer attachment) {
IOException ioe; if (exc instanceof IOException) {
ioe = (IOException) exc;
} else {
ioe = new IOException(exc);
}
setError(ioe); if (exc instanceof AsynchronousCloseException) { // Release here since there will be no // notify/dispatch to do the release.
readPending.release(); // If already closed, don't call onError and close again
getEndpoint().processSocket(Nio2SocketWrapper.this, SocketEvent.STOP, false);
} elseif (!getEndpoint().processSocket(Nio2SocketWrapper.this, SocketEvent.ERROR, true)) {
close();
}
}
};
this.writeCompletionHandler = new CompletionHandler<>() {
@Override publicvoid completed(Integer nBytes, ByteBuffer attachment) { boolean notify = false; synchronized (writeCompletionHandler) {
writeNotify = false; if (nBytes.intValue() < 0) {
failed(new EOFException(sm.getString("iob.failedwrite")), attachment);
} elseif (!nonBlockingWriteBuffer.isEmpty()) { // Continue writing data using a gathering write
ByteBuffer[] array = nonBlockingWriteBuffer.toArray(attachment);
getSocket().write(array, 0, array.length,
toTimeout(getWriteTimeout()), TimeUnit.MILLISECONDS,
array, gatheringWriteCompletionHandler);
} elseif (attachment.hasRemaining()) { // Regular write
getSocket().write(attachment, toTimeout(getWriteTimeout()),
TimeUnit.MILLISECONDS, attachment, writeCompletionHandler);
} else { // All data has been written if (writeInterest && !isInline()) {
writeNotify = true; // Set extra flag so that write nesting does not cause multiple notifications
notify = true;
} else { // Release here since there will be no // notify/dispatch to do the release.
writePending.release();
}
writeInterest = false;
}
} if (notify) { if (!endpoint.processSocket(Nio2SocketWrapper.this, SocketEvent.OPEN_WRITE, true)) {
close();
}
}
}
@Override publicvoid failed(Throwable exc, ByteBuffer attachment) {
IOException ioe; if (exc instanceof IOException) {
ioe = (IOException) exc;
} else {
ioe = new IOException(exc);
}
setError(ioe);
writePending.release(); if (!endpoint.processSocket(Nio2SocketWrapper.this, SocketEvent.ERROR, true)) {
close();
}
}
};
gatheringWriteCompletionHandler = new CompletionHandler<>() {
@Override publicvoid completed(Long nBytes, ByteBuffer[] attachment) { boolean notify = false; synchronized (writeCompletionHandler) {
writeNotify = false; if (nBytes.longValue() < 0) {
failed(new EOFException(sm.getString("iob.failedwrite")), attachment);
} elseif (!nonBlockingWriteBuffer.isEmpty() || buffersArrayHasRemaining(attachment, 0, attachment.length)) { // Continue writing data using a gathering write
ByteBuffer[] array = nonBlockingWriteBuffer.toArray(attachment);
getSocket().write(array, 0, array.length,
toTimeout(getWriteTimeout()), TimeUnit.MILLISECONDS,
array, gatheringWriteCompletionHandler);
} else { // All data has been written if (writeInterest && !isInline()) {
writeNotify = true; // Set extra flag so that write nesting does not cause multiple notifications
notify = true;
} else { // Release here since there will be no // notify/dispatch to do the release.
writePending.release();
}
writeInterest = false;
}
} if (notify) { if (!endpoint.processSocket(Nio2SocketWrapper.this, SocketEvent.OPEN_WRITE, true)) {
close();
}
}
}
@Override publicvoid failed(Throwable exc, ByteBuffer[] attachment) {
IOException ioe; if (exc instanceof IOException) {
ioe = (IOException) exc;
} else {
ioe = new IOException(exc);
}
setError(ioe);
writePending.release(); if (!endpoint.processSocket(Nio2SocketWrapper.this, SocketEvent.ERROR, true)) {
close();
}
}
};
@Override publicboolean isReadyForRead() throws IOException { synchronized (readCompletionHandler) { // A notification has been sent, it is possible to read at least once if (readNotify) { returntrue;
} // If a read is pending, reading is not possible until a notification is sent if (!readPending.tryAcquire()) {
readInterest = true; returnfalse;
} // It is possible to read directly from the buffer contents if (!socketBufferHandler.isReadBufferEmpty()) {
readPending.release(); returntrue;
} // Try to read some data boolean isReady = fillReadBuffer(false) > 0; if (!isReady) {
readInterest = true;
} return isReady;
}
}
@Override publicboolean isReadyForWrite() { synchronized (writeCompletionHandler) { // A notification has been sent, it is possible to write at least once if (writeNotify) { returntrue;
} // If a write is pending, writing is not possible until a notification is sent if (!writePending.tryAcquire()) {
writeInterest = true; returnfalse;
} // If the buffer is empty, it is possible to write to it if (socketBufferHandler.isWriteBufferEmpty() && nonBlockingWriteBuffer.isEmpty()) {
writePending.release(); returntrue;
} // Try to flush all data boolean isReady = !flushNonBlockingInternal(true); if (!isReady) {
writeInterest = true;
} return isReady;
}
}
@Override publicint read(boolean block, byte[] b, int off, int len) throws IOException {
checkError();
if (log.isDebugEnabled()) {
log.debug("Socket: [" + this + "], block: [" + block + "], length: [" + len + "]");
}
if (socketBufferHandler == null) { thrownew IOException(sm.getString("socket.closed"));
}
if (!notify) { if (block) { try {
readPending.acquire();
} catch (InterruptedException e) { thrownew IOException(e);
}
} else { if (!readPending.tryAcquire()) { if (log.isDebugEnabled()) {
log.debug("Socket: [" + this + "], Read in progress. Returning [0]");
} return0;
}
}
}
int nRead = populateReadBuffer(b, off, len); if (nRead > 0) { // The code that was notified is now reading its data synchronized (readCompletionHandler) {
readNotify = false;
} // This may be sufficient to complete the request and we // don't want to trigger another read since if there is no // more data to read and this request takes a while to // process the read will timeout triggering an error.
readPending.release(); return nRead;
}
synchronized (readCompletionHandler) { // Fill the read buffer as best we can.
nRead = fillReadBuffer(block); // Fill as much of the remaining byte array as possible with the // data that was just read if (nRead > 0) {
socketBufferHandler.configureReadBufferForRead();
nRead = Math.min(nRead, len);
socketBufferHandler.getReadBuffer().get(b, off, nRead);
} elseif (nRead == 0 && !block) {
readInterest = true;
} if (log.isDebugEnabled()) {
log.debug("Socket: [" + this + "], Read: [" + nRead + "]");
} return nRead;
}
}
if (!notify) { if (block) { try {
readPending.acquire();
} catch (InterruptedException e) { thrownew IOException(e);
}
} else { if (!readPending.tryAcquire()) { if (log.isDebugEnabled()) {
log.debug("Socket: [" + this + "], Read in progress. Returning [0]");
} return0;
}
}
}
int nRead = populateReadBuffer(to); if (nRead > 0) { // The code that was notified is now reading its data synchronized (readCompletionHandler) {
readNotify = false;
} // This may be sufficient to complete the request and we // don't want to trigger another read since if there is no // more data to read and this request takes a while to // process the read will timeout triggering an error.
readPending.release(); return nRead;
}
synchronized (readCompletionHandler) { // The socket read buffer capacity is socket.appReadBufSize int limit = socketBufferHandler.getReadBuffer().capacity(); if (block && to.remaining() >= limit) {
to.limit(to.position() + limit);
nRead = fillReadBuffer(block, to); if (log.isDebugEnabled()) {
log.debug("Socket: [" + this + "], Read direct from socket: [" + nRead + "]");
}
} else { // Fill the read buffer as best we can.
nRead = fillReadBuffer(block); if (log.isDebugEnabled()) {
log.debug("Socket: [" + this + "], Read into buffer: [" + nRead + "]");
} // Fill as much of the remaining byte array as possible with the // data that was just read if (nRead > 0) {
nRead = populateReadBuffer(to);
} elseif (nRead == 0 && !block) {
readInterest = true;
}
} return nRead;
}
}
@Override publicvoid run() { if (read) { long nBytes = 0; // If there is still data inside the main read buffer, it needs to be read first if (!socketBufferHandler.isReadBufferEmpty()) { synchronized (readCompletionHandler) {
socketBufferHandler.configureReadBufferForRead(); for (int i = 0; i < length && !socketBufferHandler.isReadBufferEmpty(); i++) {
nBytes += transfer(socketBufferHandler.getReadBuffer(), buffers[offset + i]);
}
} if (nBytes > 0) {
completion.completed(Long.valueOf(nBytes), this);
}
} if (nBytes == 0) {
getSocket().read(buffers, offset, length, timeout, unit, this, completion);
}
} else { // If there is still data inside the main write buffer, it needs to be written first if (!socketBufferHandler.isWriteBufferEmpty()) { synchronized (writeCompletionHandler) {
socketBufferHandler.configureWriteBufferForRead();
ByteBuffer[] array = nonBlockingWriteBuffer.toArray(socketBufferHandler.getWriteBuffer()); if (buffersArrayHasRemaining(array, 0, array.length)) {
getSocket().write(array, 0, array.length, timeout, unit,
array, new CompletionHandler<Long, ByteBuffer[]>() {
@Override publicvoid completed(Long nBytes, ByteBuffer[] buffers) { if (nBytes.longValue() < 0) {
failed(new EOFException(), null);
} elseif (buffersArrayHasRemaining(buffers, 0, buffers.length)) {
getSocket().write(buffers, 0, buffers.length, toTimeout(getWriteTimeout()),
TimeUnit.MILLISECONDS, buffers, this);
} else { // Continue until everything is written
process();
}
}
@Override publicvoid failed(Throwable exc, ByteBuffer[] buffers) {
completion.failed(exc, Nio2OperationState.this);
}
}); return;
}
}
}
getSocket().write(buffers, offset, length, timeout, unit, this, completion);
}
}
}
privateint fillReadBuffer(boolean block, ByteBuffer to) throws IOException { int nRead = 0;
Future<Integer> integer = null; if (block) { try {
integer = getSocket().read(to); long timeout = getReadTimeout(); if (timeout > 0) {
nRead = integer.get(timeout, TimeUnit.MILLISECONDS).intValue();
} else {
nRead = integer.get().intValue();
}
} catch (ExecutionException e) { if (e.getCause() instanceof IOException) { throw (IOException) e.getCause();
} else { thrownew IOException(e);
}
} catch (InterruptedException e) { thrownew IOException(e);
} catch (TimeoutException e) {
integer.cancel(true); thrownew SocketTimeoutException();
} finally { // Blocking read so need to release here since there will // not be a callback to a completion handler.
readPending.release();
}
} else {
startInline();
getSocket().read(to, toTimeout(getReadTimeout()), TimeUnit.MILLISECONDS, to,
readCompletionHandler);
endInline(); if (readPending.availablePermits() == 1) {
nRead = to.position();
}
} return nRead;
}
/** *{@inheritDoc} *<p> *OverriddenforNIO2toenableagatheringwritetobeusedtowrite *alloftheremainingdatainasingleadditionalwriteshoulda *non-blockingwriteleavedatainthebuffer.
*/
@Override protectedvoid writeNonBlocking(byte[] buf, int off, int len) throws IOException { // Note: Possible alternate behavior: // If there's non blocking abuse (like a test writing 1MB in a single // "non blocking" write), then block until the previous write is // done rather than continue buffering // Also allows doing autoblocking // Could be "smart" with coordination with the main CoyoteOutputStream to // indicate the end of a write // Uses: if (writePending.tryAcquire(socketWrapper.getTimeout(), TimeUnit.MILLISECONDS)) synchronized (writeCompletionHandler) {
checkError(); if (writeNotify || writePending.tryAcquire()) { // No pending completion handler, so writing to the main buffer // is possible
socketBufferHandler.configureWriteBufferForWrite(); int thisTime = transfer(buf, off, len, socketBufferHandler.getWriteBuffer());
len = len - thisTime;
off = off + thisTime; if (len > 0) { // Remaining data must be buffered
nonBlockingWriteBuffer.add(buf, off, len);
}
flushNonBlockingInternal(true);
} else {
nonBlockingWriteBuffer.add(buf, off, len);
}
}
}
/** *{@inheritDoc} *<p> *OverriddenforNIO2toenableagatheringwritetobeusedtowrite *alloftheremainingdatainasingleadditionalwriteshoulda *non-blockingwriteleavedatainthebuffer.
*/
@Override protectedvoid writeNonBlockingInternal(ByteBuffer from) throws IOException { // Note: Possible alternate behavior: // If there's non blocking abuse (like a test writing 1MB in a single // "non blocking" write), then block until the previous write is // done rather than continue buffering // Also allows doing autoblocking // Could be "smart" with coordination with the main CoyoteOutputStream to // indicate the end of a write // Uses: if (writePending.tryAcquire(socketWrapper.getTimeout(), TimeUnit.MILLISECONDS)) synchronized (writeCompletionHandler) {
checkError(); if (writeNotify || writePending.tryAcquire()) { // No pending completion handler, so writing to the main buffer // is possible
socketBufferHandler.configureWriteBufferForWrite();
transfer(from, socketBufferHandler.getWriteBuffer()); if (from.remaining() > 0) { // Remaining data must be buffered
nonBlockingWriteBuffer.add(from);
}
flushNonBlockingInternal(true);
} else {
nonBlockingWriteBuffer.add(from);
}
}
}
// Before doing a blocking flush, make sure that any pending non // blocking write has completed. try { if (writePending.tryAcquire(toTimeout(getWriteTimeout()), TimeUnit.MILLISECONDS)) {
writePending.release();
} else { thrownew SocketTimeoutException();
}
} catch (InterruptedException e) { // Ignore
}
@Override publicvoid registerReadInterest() { synchronized (readCompletionHandler) { // A notification is already being sent if (readNotify) { return;
} if (log.isDebugEnabled()) {
log.debug(sm.getString("endpoint.debug.registerRead", this));
}
readInterest = true; if (readPending.tryAcquire()) { // No read pending, so do a read try { if (fillReadBuffer(false) > 0) { // Special case where the read completed inline, there is no notification // in that case so it has to be done here if (!getEndpoint().processSocket(this, SocketEvent.OPEN_READ, true)) {
close();
}
}
} catch (IOException e) { // Will never happen
setError(e);
}
}
}
}
@Override publicvoid registerWriteInterest() { synchronized (writeCompletionHandler) { // A notification is already being sent if (writeNotify) { return;
} if (log.isDebugEnabled()) {
log.debug(sm.getString("endpoint.debug.registerWrite", this));
}
writeInterest = true; if (writePending.availablePermits() == 1) { // If no write is pending, notify that writing is possible if (!getEndpoint().processSocket(this, SocketEvent.OPEN_WRITE, true)) {
close();
}
}
}
}
@Override public SendfileDataBase createSendfileData(String filename, long pos, long length) { returnnew SendfileData(filename, pos, length);
}
@Override public SendfileState processSendfile(SendfileDataBase sendfileData) {
SendfileData data = (SendfileData) sendfileData;
setSendfileData(data); // Configure the send file data if (data.fchannel == null || !data.fchannel.isOpen()) {
java.nio.file.Path path = new File(sendfileData.fileName).toPath(); try {
data.fchannel = FileChannel.open(path, StandardOpenOption.READ).position(sendfileData.pos);
} catch (IOException e) { return SendfileState.ERROR;
}
}
getSocket().getBufHandler().configureWriteBufferForWrite();
ByteBuffer buffer = getSocket().getBufHandler().getWriteBuffer(); int nRead = -1; try {
nRead = data.fchannel.read(buffer);
} catch (IOException e1) { return SendfileState.ERROR;
}
try { if (socketWrapper.getSocket().isHandshakeComplete()) { // No TLS handshaking required. Let the handler // process this socket / event combination.
handshake = 0;
} elseif (event == SocketEvent.STOP || event == SocketEvent.DISCONNECT ||
event == SocketEvent.ERROR) { // Unable to complete the TLS handshake. Treat it as // if the handshake failed.
handshake = -1;
} else {
handshake = socketWrapper.getSocket().handshake(); // The handshake process reads/writes from/to the // socket. status may therefore be OPEN_WRITE once // the handshake completes. However, the handshake // happens when the socket is opened so the status // must always be OPEN_READ after it completes. It // is OK to always set this as it is only used if // the handshake completes.
event = SocketEvent.OPEN_READ;
}
} catch (IOException x) {
handshake = -1; if (logHandshake.isDebugEnabled()) {
logHandshake.debug(sm.getString("endpoint.err.handshake",
socketWrapper.getRemoteAddr(), Integer.toString(socketWrapper.getRemotePort())), x);
}
} if (handshake == 0) {
SocketState state = SocketState.OPEN; // Process the request from this socket if (event == null) {
state = getHandler().process(socketWrapper, SocketEvent.OPEN_READ);
} else {
state = getHandler().process(socketWrapper, event);
} if (state == SocketState.CLOSED) { // Close socket and pool
socketWrapper.close();
} elseif (state == SocketState.UPGRADING) {
launch = true;
}
} elseif (handshake == -1 ) {
getHandler().process(socketWrapper, SocketEvent.CONNECT_FAIL);
socketWrapper.close();
}
} catch (VirtualMachineError vme) {
ExceptionUtils.handleThrowable(vme);
} catch (Throwable t) {
log.error(sm.getString("endpoint.processing.fail"), t); if (socketWrapper != null) {
((Nio2SocketWrapper) socketWrapper).close();
}
} finally { if (launch) { try {
getExecutor().execute(new SocketProcessor(socketWrapper, SocketEvent.OPEN_READ));
} catch (NullPointerException npe) { if (running) {
log.error(sm.getString("endpoint.launch.fail"),
npe);
}
}
}
socketWrapper = null;
event = null; //return to cache if (running && processorCache != null) {
processorCache.push(this);
}
}
}
}
// ----------------------------------------------- SendfileData Inner Class /** *SendfileDataclass.
*/ publicstaticclass SendfileData extends SendfileDataBase { private FileChannel fchannel; // Internal use only privateboolean doneInline = false; privateboolean error = false;
public SendfileData(String filename, long pos, long length) { super(filename, pos, length);
}
}
}
Messung V0.5 in Prozent
¤ Diese beiden folgenden Angebotsgruppen bietet das Unternehmen0.59Angebot
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.