All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.hcl.domino.jna.mq.JNAMessageQueue Maven / Gradle / Ivy

There is a newer version: 1.41.0
Show newest version
/*
 * ==========================================================================
 * Copyright (C) 2019-2022 HCL America, Inc. ( http://www.hcl.com/ )
 *                            All rights reserved.
 * ==========================================================================
 * 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 .
 *
 * 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.hcl.domino.jna.mq;

import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.ref.ReferenceQueue;
import java.nio.ByteBuffer;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.text.MessageFormat;
import java.util.AbstractQueue;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;

import com.hcl.domino.DominoException;
import com.hcl.domino.commons.errors.INotesErrorConstants;
import com.hcl.domino.commons.gc.APIObjectAllocations;
import com.hcl.domino.commons.gc.IAPIObject;
import com.hcl.domino.commons.gc.IGCDominoClient;
import com.hcl.domino.commons.util.NotesErrorUtils;
import com.hcl.domino.commons.util.PlatformUtils;
import com.hcl.domino.jna.BaseJNAAPIObject;
import com.hcl.domino.jna.internal.DisposableMemory;
import com.hcl.domino.jna.internal.NotesStringUtils;
import com.hcl.domino.jna.internal.callbacks.NotesCallbacks;
import com.hcl.domino.jna.internal.callbacks.Win32NotesCallbacks;
import com.hcl.domino.jna.internal.capi.NotesCAPI;
import com.hcl.domino.jna.internal.gc.allocations.JNAMessageQueueAllocations;
import com.hcl.domino.misc.NotesConstants;
import com.hcl.domino.mq.MessageQueue;
import com.hcl.domino.mq.MessageQueue.IMQCallback.Action;
import com.hcl.domino.mq.MessageQueues;
import com.sun.jna.Memory;
import com.sun.jna.ptr.ShortByReference;

/**
 * Implementation of the Domino message-queue as a {@link BlockingQueue}.
 * 
 * @author Tammo Riedinger
 */
public class JNAMessageQueue extends BaseJNAAPIObject implements MessageQueue {
	private String m_queueName;
	private BlockingQueue m_blockingQueue=new BlockingMessageQueue();

	/**
	 * Creates a new instance. Use {@link MessageQueues#createAndOpen(String, int)} to create a
	 * new queue or {@link MessageQueues#open(String, boolean)} to open an existing one.
	 * 
	 * @param client	the parent client of the queue
	 * @param queueName name of the message queue
	 * @param queueHandle the handle of the queue
	 */
	public JNAMessageQueue(IGCDominoClient client, String queueName, int queueHandle) {
		super(client);

		m_queueName = queueName;
		getAllocations().setMessageQueueHandle(queueHandle);

		setInitialized();
	}

	@SuppressWarnings("rawtypes")
	@Override
	protected JNAMessageQueueAllocations createAllocations(IGCDominoClient parentDominoClient,
			APIObjectAllocations parentAllocations, ReferenceQueue queue) {
		return new JNAMessageQueueAllocations(parentDominoClient, parentAllocations, this, queue);
	}

	@Override
	public final String getName() {
		return m_queueName;
	}
	
	@Override
	public String toStringLocal() {
		if (isDisposed()) {
			return "MessageQueue [disposed]"; //$NON-NLS-1$
		}
		else {
			return MessageFormat.format("MessageQueue [handle={0}, name={1}]", getAllocations().getMessageQueueHandle(), getName()); //$NON-NLS-1$
		}
	}

	@Override
	public int scan(byte[] buffer, IMQCallback callback) {
		DisposableMemory outMem=null;

		if (buffer!=null && buffer.length>0) {
			// TODO eventually a cache could be introduced to allocate commonly used memory-buffers
			outMem=new DisposableMemory(buffer.length);
		}

		try {
			int bytesWritten=scan(outMem, callback);

			if (outMem!=null && bytesWritten>0) {
				outMem.read(0, buffer, 0, bytesWritten);
			}

			return bytesWritten;
		}
		finally {
			if (outMem!=null) {
				outMem.close();
			}
		}
	}

	/**
	 * This function parses a message queue, calling an action routine for each message in the queue.
* If the message queue is empty, or if all messages in the queue have been enumerated without * returning an error code, MQScan returns ERR_MQ_EMPTY.
*
* In the simple case, MQScan() does not modify the contents of the queue; * by returning the appropriate error codes to MQScan, the action routine can specify that * messages are to be removed from the queue or skipped, or that enumeration is to be * terminated immediately. See the reference entry for {@link IMQCallback} for more details.
*
* Note: MQScan locks out all other message queue function calls until it completes. * @param buffer buffer to be used to read messages, max size is {@link NotesConstants#MQ_MAX_MSGSIZE} (65326 bytes) * @param offset the offset in the buffer where to start writing the message * @param length the max length of the message in the buffer * @param callback callback to be called for each message; if null, we dequeue the a message and return it in the specified buffer * @return The number of bytes written to the buffer (important if callback has been set to null) */ private int scan(final Memory buffer, final IMQCallback callback) { checkDisposed(); final long length=(buffer!=null) ? buffer.size():0; if (buffer!=null && length==0) { throw new IllegalArgumentException("Buffer cannot be empty"); } else if (buffer!=null && length > NotesConstants.MQ_MAX_MSGSIZE) { throw new IllegalArgumentException(MessageFormat.format("Max size for the buffer is {0} bytes. You specified one with {1} bytes.", NotesConstants.MQ_MAX_MSGSIZE, length)); } final NotesCallbacks.MQScanCallback cCallback; if (PlatformUtils.isWin32()) { cCallback = (Win32NotesCallbacks.MQScanCallbackWin32) (pBuffer, length1, priority, ctx) -> { if (callback==null) { return INotesErrorConstants.ERR_MQSCAN_DEQUEUE; } ByteBuffer byteBuf = pBuffer.getByteBuffer(0, length1 & 0xffff); ByteBuffer roByteBuf = byteBuf.asReadOnlyBuffer(); Action action = callback.dataReceived(roByteBuf, priority & 0xffff); switch (action) { case Continue: return 0; case Abort: return INotesErrorConstants.ERR_MQSCAN_ABORT; case Dequeue: return INotesErrorConstants.ERR_MQSCAN_DEQUEUE; case Delete: return INotesErrorConstants.ERR_MQSCAN_DELETE; default: //No action taken } return 0; }; } else { cCallback = (pBuffer, length1, priority, ctx) -> { if (callback==null) { return INotesErrorConstants.ERR_MQSCAN_DEQUEUE; } ByteBuffer byteBuf = pBuffer.getByteBuffer(0, length1 & 0xffff); ByteBuffer roByteBuf = byteBuf.asReadOnlyBuffer(); Action action = callback.dataReceived(roByteBuf, priority & 0xffff); switch (action) { case Continue: return 0; case Abort: return INotesErrorConstants.ERR_MQSCAN_ABORT; case Dequeue: return INotesErrorConstants.ERR_MQSCAN_DEQUEUE; case Delete: return INotesErrorConstants.ERR_MQSCAN_DELETE; default: //No action taken } return 0; }; } final ShortByReference retMsgLength = new ShortByReference(); short result; try { //AccessController call required to prevent SecurityException when running in XPages result = AccessController.doPrivileged((PrivilegedExceptionAction) () -> NotesCAPI.get().MQScan(getAllocations().getMessageQueueHandle(), buffer, (short) (length & 0xffff), 0, cCallback, null, retMsgLength)); } catch (PrivilegedActionException e) { if (e.getCause() instanceof RuntimeException) { throw (RuntimeException) e.getCause(); } else { throw new DominoException(0, "Error scanning message queue", e); } } NotesErrorUtils.checkResult(result); return retMsgLength.getValue() & 0xffff; } @Override public void put(byte[] buffer, int priority, int offset, int length) { if (length > NotesConstants.MQ_MAX_MSGSIZE) { throw new IllegalArgumentException(MessageFormat.format("Max size for the buffer is {0} bytes. You specified one with {1} bytes.", NotesConstants.MQ_MAX_MSGSIZE, length)); } // TODO eventually a cache could be introduced to allocate commonly used memory-buffers try(DisposableMemory mem = new DisposableMemory(length)) { // note: bounds-check should be performed by Memory-class mem.write(0, buffer, offset, length); put(mem, priority); } } /** * This function adds a message to the message queue.
* The message will be placed in the queue according to the value of its priority argument - * higher priority messages will be enqueued ahead of lower priority messages.
*
* If the queue is full or in a QUIT state, the message will not be put in the queue, and * the function will return an appropriate error code. * * @param buffer buffer containing the message. Maximum buffer length is {@link NotesConstants#MQ_MAX_MSGSIZE} (65326 bytes) * @param priority priority */ private void put(Memory buffer, int priority) { checkDisposed(); if (priority<0 || priority>65535) { throw new IllegalArgumentException("Priority must be between 0 and 65535 (WORD datatype in C API)"); } long length = buffer.size(); if (length > NotesConstants.MQ_MAX_MSGSIZE) { throw new IllegalArgumentException(MessageFormat.format("Max size for the buffer is {0} bytes. You specified one with {1} bytes.", NotesConstants.MQ_MAX_MSGSIZE, length)); } short result = NotesCAPI.get().MQPut(getAllocations().getMessageQueueHandle(), (short) (priority & 0xffff), buffer, (short) (length & 0xffff), 0); NotesErrorUtils.checkResult(result); } /** * Note: Currently not used, since every read operation currently uses {@link #scan(Memory, com.hcl.domino.mq.MessageQueue.IMQCallback)} * * Retrieves a message from a message queue, provided the queue is not in a QUIT state. * The message will be stored in the buffer specified in the Buffer argument.
* Note: The error code {@link INotesErrorConstants#ERR_MQ_QUITTING} indicates that the * message queue is in the QUIT state, denoting that applications that are reading * the message queue should terminate. For instance, a server addin's message queue * will be placed in the QUIT state when a "tell <addin> quit" command is input at the console. * @param buffer buffer used to read data * @param waitForMessage if the specified message queue is empty, wait for a message to appear in the queue. The timeout argument specifies the amount of time to wait for a message. * @param timeoutMillis if waitForMessage is set to true, the number of milliseconds to wait for a message before timing out. Specify 0 to wait forever. If the message queue goes into a QUIT state before the Timeout expires, MQGet will return immediately. * @param offset the offset in the buffer where to start writing the message * @param length the max length of the message in the buffer * @return Number of bytes written to the buffer */ @SuppressWarnings("unused") private int get(Memory buffer, boolean waitForMessage, int timeoutMillis, int offset, int length) { checkDisposed(); if (length > NotesConstants.MQ_MAX_MSGSIZE) { throw new IllegalArgumentException(MessageFormat.format("Max size for the buffer is {0} bytes. You specified one with {1} bytes.", NotesConstants.MQ_MAX_MSGSIZE, length)); } ShortByReference retMsgLength = new ShortByReference(); short result = NotesCAPI.get().MQGet(getAllocations().getMessageQueueHandle(), buffer, (short) (length & 0xffff), waitForMessage ? NotesConstants.MQ_WAIT_FOR_MSG : 0, timeoutMillis, retMsgLength); NotesErrorUtils.checkResult(result); return retMsgLength.getValue(); } @Override public Optional get(long timeout, TimeUnit unit) throws InterruptedException { checkDisposed(); try(DisposableMemory buffer = new DisposableMemory(NotesConstants.MQ_MAX_MSGSIZE)) { ShortByReference retMsgLength = new ShortByReference(); long millis = unit.toMillis(timeout); if(millis > Integer.MAX_VALUE) { throw new IllegalArgumentException(MessageFormat.format("Timeout value {0} is larger than Integer.MAX_VALUE", Long.toString(millis))); } short result = NotesCAPI.get().MQGet(getAllocations().getMessageQueueHandle(), buffer, (short) (NotesConstants.MQ_MAX_MSGSIZE & 0xffff), NotesConstants.MQ_WAIT_FOR_MSG, (int)millis, retMsgLength); if(result == INotesErrorConstants.ERR_MQ_EMPTY) { return Optional.empty(); } else if(result == INotesErrorConstants.ERR_MQ_TIMEOUT) { return Optional.empty(); } else if(result == INotesErrorConstants.ERR_MQ_QUITTING) { throw new InterruptedException("Received ERR_MQ_QUITTING"); } NotesErrorUtils.checkResult(result); return Optional.of(NotesStringUtils.fromLMBCS(buffer, Short.toUnsignedInt(retMsgLength.getValue()))); } } @Override public void putQuitMsg() { checkDisposed(); NotesCAPI.get().MQPutQuitMsg(getAllocations().getMessageQueueHandle()); } @Override public boolean isQuitPending() { checkDisposed(); boolean quitPending = NotesCAPI.get().MQIsQuitPending(getAllocations().getMessageQueueHandle()); return quitPending; } @Override public void close() { getAllocations().dispose(); } @Override public boolean add(String e) { return m_blockingQueue.add(e); } @Override public boolean offer(String e) { return m_blockingQueue.offer(e); } @Override public void put(String e) throws InterruptedException { m_blockingQueue.put(e); } @Override public boolean offer(String e, long timeout, TimeUnit unit) throws InterruptedException { return m_blockingQueue.offer(e, timeout, unit); } @Override public String take() throws InterruptedException { return m_blockingQueue.take(); } @Override public String poll(long timeout, TimeUnit unit) throws InterruptedException { return m_blockingQueue.poll(timeout, unit); } @Override public int remainingCapacity() { return m_blockingQueue.remainingCapacity(); } @Override public boolean remove(Object o) { return m_blockingQueue.remove(o); } @Override public boolean contains(Object o) { return m_blockingQueue.contains(o); } @Override public int drainTo(Collection c) { return m_blockingQueue.drainTo(c); } @Override public int drainTo(Collection c, int maxElements) { return m_blockingQueue.drainTo(c, maxElements); } @Override public String remove() { return m_blockingQueue.remove(); } @Override public String poll() { return m_blockingQueue.poll(); } @Override public String element() { return m_blockingQueue.element(); } @Override public String peek() { return m_blockingQueue.peek(); } @Override public int size() { return m_blockingQueue.size(); } @Override public boolean isEmpty() { return m_blockingQueue.isEmpty(); } @Override public Iterator iterator() { return m_blockingQueue.iterator(); } @Override public Object[] toArray() { return m_blockingQueue.toArray(); } @Override public T[] toArray(T[] a) { return m_blockingQueue.toArray(a); } @Override public boolean containsAll(Collection c) { return m_blockingQueue.containsAll(c); } @Override public boolean addAll(Collection c) { return m_blockingQueue.addAll(c); } @Override public boolean removeAll(Collection c) { return m_blockingQueue.removeAll(c); } @Override public boolean retainAll(Collection c) { return m_blockingQueue.retainAll(c); } @Override public boolean removeIf(Predicate filter) { return m_blockingQueue.removeIf(filter); } @Override public void clear() { m_blockingQueue.clear(); } /** * Implementation of a blocking messages that uses only {@link JNAMessageQueue#put(byte[], int, int, int)} * and {@link JNAMessageQueue#scan(Memory, com.hcl.domino.mq.MessageQueue.IMQCallback)} to store and retrieve * messages from the Domino message-queue. * * @author Tammo Riedinger */ private class BlockingMessageQueue extends AbstractQueue implements BlockingQueue { @Override public void put(String e) { offer(e); } @Override public boolean offer(String e, long timeout, TimeUnit unit) { long start=System.nanoTime(); do { if (this.offer(e)==true) { return true; } try { Thread.sleep(Math.min(10, unit.toMillis(Math.round(timeout/2d)))); } catch (InterruptedException ie) { // ignore } } while (timeout > unit.convert(System.nanoTime()-start, TimeUnit.NANOSECONDS)); return false; } @Override public boolean offer(String e) { byte[] data=e.getBytes(); try { JNAMessageQueue.this.put(data, 0, 0, data.length); return true; } catch (DominoException ex) { if (ex.getId()==INotesErrorConstants.ERR_MQ_EXCEEDED_QUOTA) { return false; } throw ex; } } @Override public String poll(long timeout, TimeUnit unit) throws InterruptedException { long start=System.nanoTime(); String[] e=new String[1]; long nanoDelayInUnits=-1; do { e[0]=null; try { JNAMessageQueue.this.scan((Memory)null, (IMQCallback) (buffer, priority) -> { e[0] = JNAMessageQueue.bufferToString(buffer); return Action.Dequeue; }); break; } catch (DominoException de) { if (de.getId()!=INotesErrorConstants.ERR_MQ_EMPTY) { throw de; } } if (timeout<0 && (Thread.currentThread().isInterrupted() || !Thread.currentThread().isAlive())) { throw new InterruptedException("Thread was interrupted or died"); } try { // TODO think about this sleep interval Thread.sleep(Math.min(10, unit.toMillis(Math.round(timeout/2d)))); } catch (InterruptedException ie) { // ignore } nanoDelayInUnits=unit.convert(System.nanoTime()-start, TimeUnit.NANOSECONDS); } while (timeout<0 || timeout > nanoDelayInUnits); return e[0]; } @Override public String poll() { try { return poll(0, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { // shouldn't happen return null; } } @Override public String peek() { String[] e=new String[1]; try { JNAMessageQueue.this.scan((Memory)null, (IMQCallback) (buffer, priority) -> { e[0] = JNAMessageQueue.bufferToString(buffer); return Action.Abort; }); } catch (DominoException de) { if (de.getId()!=INotesErrorConstants.ERR_MQSCAN_ABORT && de.getId()!=INotesErrorConstants.ERR_MQ_EMPTY) { throw de; } } return e[0]; } @Override public Iterator iterator() { // TODO maybe implement an incremental iterator final ArrayList allElements=new ArrayList<>(); try { JNAMessageQueue.this.scan((Memory)null, (IMQCallback) (buffer, priority) -> { allElements.add(JNAMessageQueue.bufferToString(buffer)); return Action.Continue; }); } catch (DominoException e) { if (e.getId()!=INotesErrorConstants.ERR_MQ_EMPTY) { throw e; } } return Collections.unmodifiableList(allElements).iterator(); } @Override public int size() { final AtomicInteger count=new AtomicInteger(); try { JNAMessageQueue.this.scan((Memory)null, (IMQCallback) (buffer, priority) -> { count.incrementAndGet(); return Action.Continue; }); } catch (DominoException e) { if (e.getId()!=INotesErrorConstants.ERR_MQ_EMPTY) { throw e; } } return count.intValue(); } @Override public String take() throws InterruptedException { return poll(-1, TimeUnit.MILLISECONDS); } @Override public int remainingCapacity() { return MessageQueue.MAX_MESSAGE_COUNT - size(); } @Override public int drainTo(Collection c) { return drainTo(c, -1); } @Override public int drainTo(final Collection c, int maxElements) { final AtomicInteger counter=new AtomicInteger(); try { JNAMessageQueue.this.scan((Memory)null, (IMQCallback) (buffer, priority) -> { c.add(JNAMessageQueue.bufferToString(buffer)); counter.getAndIncrement(); return Action.Delete; }); } catch (DominoException e) { if (e.getId()!=INotesErrorConstants.ERR_MQ_EMPTY) { throw e; } } return counter.intValue(); } @Override public boolean contains(Object o) { return containsAll(Arrays.asList(o)); } @Override public boolean containsAll(Collection c) { final AtomicBoolean contains=new AtomicBoolean(); try { contains.set(true); JNAMessageQueue.this.scan((Memory)null, (IMQCallback) (buffer, priority) -> { if (!c.contains(JNAMessageQueue.bufferToString(buffer))) { contains.set(false); return Action.Abort; } return Action.Continue; }); } catch (DominoException e) { if (e.getId()==INotesErrorConstants.ERR_MQSCAN_ABORT) { return false; } else if (e.getId()!=INotesErrorConstants.ERR_MQ_EMPTY) { throw e; } } return contains.get(); } @Override public void clear() { try { JNAMessageQueue.this.scan((Memory)null, (IMQCallback) (buffer, priority) -> Action.Delete); } catch (DominoException e) { if (e.getId()==INotesErrorConstants.ERR_MQ_EMPTY) { // this is expected, when queue is empty return; } throw e; } } @Override public boolean remove(Object o) { return removeIf(t -> t.equals(o)); } @Override public boolean removeAll(Collection c) { return removeIf(t -> c.contains(t)); } @Override public boolean retainAll(final Collection c) { return removeIf(t -> !c.contains(t)); } @Override public boolean removeIf(final Predicate filter) { Objects.requireNonNull(filter); final AtomicInteger counter=new AtomicInteger(); try { scan((Memory)null, (IMQCallback) (buffer, priority) -> { if (filter.test(JNAMessageQueue.bufferToString(buffer))) { counter.getAndIncrement(); return Action.Delete; } return Action.Continue; }); } catch (DominoException e) { if (e.getId()!=INotesErrorConstants.ERR_MQ_EMPTY) { throw e; } } return counter.intValue()>0; } @Override public boolean isEmpty() { try { JNAMessageQueue.this.scan((Memory)null, (IMQCallback) (buffer, priority) -> Action.Abort); } catch (DominoException e) { if (e.getId()==INotesErrorConstants.ERR_MQSCAN_ABORT) { return false; } else if (e.getId()==INotesErrorConstants.ERR_MQ_EMPTY) { return true; } throw e; } return true; } } private static String bufferToString(ByteBuffer buffer) { ByteArrayOutputStream bOut=new ByteArrayOutputStream(); while (buffer.hasRemaining()) { bOut.write(buffer.get()); } try { return bOut.toString("UTF-8"); //$NON-NLS-1$ } catch (UnsupportedEncodingException e) { // shouldn't happen throw new DominoException(0, MessageFormat.format("Couldn''t decode message: {0}", e.getMessage())); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy