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

com.hcl.domino.jna.data.JNADocument Maven / Gradle / Ivy

There is a newer version: 1.44.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.data;

import static java.text.MessageFormat.format;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.lang.ref.ReferenceQueue;
import java.nio.ByteBuffer;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.text.MessageFormat;
import java.time.temporal.Temporal;
import java.time.temporal.TemporalAccessor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.EnumSet;
import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import com.hcl.domino.DominoException;
import com.hcl.domino.admin.idvault.UserId;
import com.hcl.domino.commons.constants.UpdateNote;
import com.hcl.domino.commons.data.AbstractTypedAccess;
import com.hcl.domino.commons.data.DefaultDominoDateRange;
import com.hcl.domino.commons.data.IDefaultDocument;
import com.hcl.domino.commons.data.SignatureDataImpl;
import com.hcl.domino.commons.design.FormFieldImpl;
import com.hcl.domino.commons.design.view.CollationDecoder;
import com.hcl.domino.commons.design.view.CollationEncoder;
import com.hcl.domino.commons.design.view.DominoCalendarFormat;
import com.hcl.domino.commons.design.view.DominoCollationInfo;
import com.hcl.domino.commons.design.view.DominoViewFormat;
import com.hcl.domino.commons.design.view.ViewFormatDecoder;
import com.hcl.domino.commons.design.view.ViewFormatEncoder;
import com.hcl.domino.commons.errors.INotesErrorConstants;
import com.hcl.domino.commons.errors.UnsupportedItemValueError;
import com.hcl.domino.commons.gc.APIObjectAllocations;
import com.hcl.domino.commons.gc.IGCDominoClient;
import com.hcl.domino.commons.mime.MimePartOptions;
import com.hcl.domino.commons.mime.NotesMIMEPart;
import com.hcl.domino.commons.mime.NotesMIMEPart.PartType;
import com.hcl.domino.commons.richtext.DefaultRichTextList;
import com.hcl.domino.commons.structures.MemoryStructureUtil;
import com.hcl.domino.commons.util.ListUtil;
import com.hcl.domino.commons.util.NotesDateTimeUtils;
import com.hcl.domino.commons.util.NotesErrorUtils;
import com.hcl.domino.commons.util.NotesItemDataUtil;
import com.hcl.domino.commons.util.PlatformUtils;
import com.hcl.domino.commons.util.StringUtil;
import com.hcl.domino.data.Attachment;
import com.hcl.domino.data.Attachment.Compression;
import com.hcl.domino.data.AutoCloseableDocument;
import com.hcl.domino.data.Database;
import com.hcl.domino.data.Database.OpenDocumentMode;
import com.hcl.domino.data.Document;
import com.hcl.domino.data.DocumentClass;
import com.hcl.domino.data.DocumentValueConverter;
import com.hcl.domino.data.DominoDateRange;
import com.hcl.domino.data.DominoDateTime;
import com.hcl.domino.data.DominoOriginatorId;
import com.hcl.domino.data.DominoUniversalNoteId;
import com.hcl.domino.data.IAdaptable;
import com.hcl.domino.data.IDTable;
import com.hcl.domino.data.Item;
import com.hcl.domino.data.Item.ItemFlag;
import com.hcl.domino.data.ItemDataType;
import com.hcl.domino.data.PreV3Author;
import com.hcl.domino.data.UserData;
import com.hcl.domino.design.DesignAgent;
import com.hcl.domino.design.DesignConstants;
import com.hcl.domino.design.Form;
import com.hcl.domino.exception.LotusScriptCompilationException;
import com.hcl.domino.exception.ObjectDisposedException;
import com.hcl.domino.jna.BaseJNAAPIObject;
import com.hcl.domino.jna.JNADominoClient;
import com.hcl.domino.jna.data.JNADatabaseObjectProducer.ObjectInfo;
import com.hcl.domino.jna.internal.AgentRunInfoDecoder;
import com.hcl.domino.jna.internal.DisposableMemory;
import com.hcl.domino.jna.internal.ItemDecoder;
import com.hcl.domino.jna.internal.JNAMemoryUtils;
import com.hcl.domino.jna.internal.JNANotesConstants;
import com.hcl.domino.jna.internal.Mem;
import com.hcl.domino.jna.internal.Mem.LockedMemory;
import com.hcl.domino.jna.internal.NotesNamingUtils;
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.INotesCAPI1201;
import com.hcl.domino.jna.internal.capi.NotesCAPI;
import com.hcl.domino.jna.internal.capi.NotesCAPI1201;
import com.hcl.domino.jna.internal.gc.allocations.JNADatabaseAllocations;
import com.hcl.domino.jna.internal.gc.allocations.JNADocumentAllocations;
import com.hcl.domino.jna.internal.gc.handles.DHANDLE;
import com.hcl.domino.jna.internal.gc.handles.DHANDLE32;
import com.hcl.domino.jna.internal.gc.handles.DHANDLE64;
import com.hcl.domino.jna.internal.gc.handles.HANDLE;
import com.hcl.domino.jna.internal.gc.handles.LockUtil;
import com.hcl.domino.jna.internal.richtext.JNARichtextNavigator;
import com.hcl.domino.jna.internal.structs.NotesBlockIdStruct;
import com.hcl.domino.jna.internal.structs.NotesFileObjectStruct;
import com.hcl.domino.jna.internal.structs.NotesMIMEPartStruct;
import com.hcl.domino.jna.internal.structs.NotesNumberPairStruct;
import com.hcl.domino.jna.internal.structs.NotesObjectDescriptorStruct;
import com.hcl.domino.jna.internal.structs.NotesOriginatorIdStruct;
import com.hcl.domino.jna.internal.structs.NotesRangeStruct;
import com.hcl.domino.jna.internal.structs.NotesTimeDatePairStruct;
import com.hcl.domino.jna.internal.structs.NotesTimeDateStruct;
import com.hcl.domino.jna.internal.structs.NotesUniversalNoteIdStruct;
import com.hcl.domino.jna.misc.LMBCSCharsetProvider.LMBCSCharset;
import com.hcl.domino.jna.richtext.JNARichtextWriter;
import com.hcl.domino.jna.utils.JNADominoUtils;
import com.hcl.domino.misc.DominoEnumUtil;
import com.hcl.domino.misc.JNXServiceFinder;
import com.hcl.domino.misc.Loop;
import com.hcl.domino.misc.NotesConstants;
import com.hcl.domino.misc.Ref;
import com.hcl.domino.richtext.FormField;
import com.hcl.domino.richtext.RichTextRecordList;
import com.hcl.domino.richtext.RichTextWriter;
import com.hcl.domino.richtext.conversion.IRichTextConversion;
import com.hcl.domino.richtext.records.CDField;
import com.hcl.domino.richtext.records.RecordType;
import com.hcl.domino.richtext.records.RichTextRecord;
import com.hcl.domino.richtext.structures.MemoryStructureWrapperService;
import com.hcl.domino.richtext.structures.ObjectDescriptor;
import com.hcl.domino.richtext.structures.RFC822ItemDesc;
import com.sun.jna.Memory;
import com.sun.jna.Native;
import com.sun.jna.Platform;
import com.sun.jna.Pointer;
import com.sun.jna.Structure;
import com.sun.jna.ptr.ByteByReference;
import com.sun.jna.ptr.IntByReference;
import com.sun.jna.ptr.PointerByReference;
import com.sun.jna.ptr.ShortByReference;

import jakarta.mail.MessagingException;
import jakarta.mail.internet.InternetHeaders;

public class JNADocument extends BaseJNAAPIObject implements IDefaultDocument, AutoCloseableDocument {
	private Set m_documentClass;
	private AbstractTypedAccess m_typedAccess;
	private ThreadLocal>> readingItemType = ThreadLocal.withInitial(HashSet::new);
	private boolean m_saveMessageOnSend;

	public JNADocument(JNADatabase parent, IAdaptable adaptable) {
		this(parent, adaptable, false);
	}
	
	public JNADocument(JNADatabase parent, IAdaptable adaptable, boolean noRecycle) {
		super(parent);
		
		DHANDLE handle = adaptable.getAdapter(DHANDLE.class);
		if (handle==null) {
			throw new DominoException(0, "Missing expected note handle");
		}
		getAllocations().setNoteHandle(handle);
		getAllocations().setNoRecycle(noRecycle);
		
		m_typedAccess = new AbstractTypedAccess() {
			@Override
			public boolean hasItem(String itemName) {
				return JNADocument.this.hasItem(itemName);
			}

			@Override
			public List getItemNames() {
				return JNADocument.this.getItemNames();
			}
			
			@Override
			protected List getItemValue(String itemName) {
				return JNADocument.this.getItemValue(itemName);
			}
			
			@Override
			protected  T getViaValueConverter(String itemName, Class valueType, T defaultValue) {
				DocumentValueConverter converter = JNXServiceFinder.findServices(DocumentValueConverter.class)
					.filter(c -> c.supportsRead(valueType))
					.sorted(Comparator.comparing(DocumentValueConverter::getPriority).reversed())
					.findFirst()
					.orElse(null);

				if (converter!=null) {
					if (readingItemType.get().contains(converter.getClass())) {
						throw new IllegalStateException(format("Infinite loop detected reading the value of item {0} as type {1}",
								itemName, valueType.getName()));
					}
					readingItemType.get().add(converter.getClass());
					try {
						return converter.getValue(JNADocument.this, itemName, valueType, defaultValue);
					}
					finally {
						readingItemType.get().remove(converter.getClass());
					}
				}
				else {
					throw new IllegalArgumentException(format("Unsupported return value type: {0}", valueType.getName()));
				}
			}
			
			@Override
			protected  List getAsListViaValueConverter(String itemName, Class valueType, List defaultValue) {
				DocumentValueConverter converter = JNXServiceFinder.findServices(DocumentValueConverter.class)
					.filter(c -> c.supportsRead(valueType))
					.sorted(Comparator.comparing(DocumentValueConverter::getPriority).reversed())
					.findFirst()
					.orElse(null);

				if (converter!=null) {
					if (readingItemType.get().contains(converter.getClass())) {
						throw new IllegalStateException(format("Infinite loop detected reading the value of item {0} as type {1}",
								itemName, valueType.getName()));
					}
					readingItemType.get().add(converter.getClass());
					try {
						return converter.getValueAsList(JNADocument.this, itemName, valueType, defaultValue);
					}
					finally {
						readingItemType.get().remove(converter.getClass());
					}
				}
				else {
					throw new IllegalArgumentException(format("Unsupported return value type: {0}", valueType.getName()));
				}
			}
		};
		
		setInitialized();
	}

	@Override
	public boolean isClosed() {
		JNADocumentAllocations allocations = getAllocations();
		synchronized(allocations) {
		  return allocations.isDisposed();
		}
	}
	
	@Override
	public void close() {
		JNADocumentAllocations allocations = getAllocations();
		
		synchronized(allocations) {
    		if (!allocations.isDisposed()) {
    			allocations.dispose();
    		}
		}
	}
	
	@Override
	public AutoCloseableDocument autoClosable() {
		return this;
	}
	
	@Override
	public Database getParentDatabase() {
		return (Database) super.getParent();
	}
	
	@Override
	@SuppressWarnings("unchecked")
	public final  T getAdapterLocal(Class clazz) {
		if (clazz == DHANDLE.class) {
			return (T) getAllocations().getNoteHandle();
		}
		else if (clazz == AutoCloseableDocument.class) {
			return (T) autoClosable();
		}
		
		return null;
	}
	
	@SuppressWarnings({ "rawtypes", "unchecked" })
	@Override
	protected JNADocumentAllocations createAllocations(IGCDominoClient parentDominoClient,
			APIObjectAllocations parentAllocations, ReferenceQueue queue) {

		return new JNADocumentAllocations(parentDominoClient, parentAllocations, this, queue);
	}

	@Override
	public List getItemValue(String itemName) {
		checkDisposed();
		
		JNAItem item = (JNAItem) getFirstItem(itemName).orElse(null);
		if (item==null) {
			return Collections.emptyList();
		}
		
		int valueLength = item.getValueLength();
		
		//lock and decode value
		NotesBlockIdStruct valueBlockId = item.getValueBlockId();
		
		Pointer valuePtr = Mem.OSLockObject(valueBlockId);
		try {
			List values = getItemValue(itemName, item.getItemBlockId(), valueBlockId, valuePtr, valueLength);
			return values;
		}
		finally {
			Mem.OSUnlockObject(valueBlockId);
		}
	}

	/**
	 * Decodes an item value
	 * 
	 * @param itemName item name (for logging purpose)
	 * @param valueBlockId value block id
	 * @param valueLength item value length plus 2 bytes for the data type WORD
	 * @return item value as list
	 */
	List getItemValue(String itemName, NotesBlockIdStruct itemBlockId, NotesBlockIdStruct valueBlockId, int valueLength) {
		Pointer valuePtr = Mem.OSLockObject(valueBlockId);
		try {
			List values = getItemValue(itemName, itemBlockId, valueBlockId, valuePtr, valueLength);
			return values;
		}
		finally {
			Mem.OSUnlockObject(valueBlockId);
		}
	}
	
	/**
	 * Decodes an item value
	 * 
	 * @param notesAPI Notes API
	 * @param itemName item name (for logging purpose)
	 * @param itemBlockId item block id
	 * @param valueBlockId value block id
	 * @param valuePtr pointer to the item value
	 * @param valueLength item value length plus 2 bytes for the data type WORD
	 * @return item value as list
	 */
	List getItemValue(String itemName, NotesBlockIdStruct itemBlockId, NotesBlockIdStruct valueBlockId,
			Pointer valuePtr, int valueLength) {
		
		short dataType = valuePtr.getShort(0);
		int dataTypeAsInt = dataType & 0xffff;
		
		boolean supportedType = false;
		if (dataTypeAsInt == ItemDataType.TYPE_TEXT.getValue()) {
			supportedType = true;
		}
		else if (dataTypeAsInt == ItemDataType.TYPE_TEXT_LIST.getValue()) {
			supportedType = true;
		}
		else if (dataTypeAsInt == ItemDataType.TYPE_NUMBER.getValue()) {
			supportedType = true;
		}
		else if (dataTypeAsInt == ItemDataType.TYPE_TIME.getValue()) {
			supportedType = true;
		}
		else if (dataTypeAsInt == ItemDataType.TYPE_NUMBER_RANGE.getValue()) {
			supportedType = true;
		}
		else if (dataTypeAsInt == ItemDataType.TYPE_TIME_RANGE.getValue()) {
			supportedType = true;
		}
		else if (dataTypeAsInt == ItemDataType.TYPE_OBJECT.getValue()) {
			supportedType = true;
		}
		else if (dataTypeAsInt == ItemDataType.TYPE_NOTEREF_LIST.getValue()) {
			supportedType = true;
		}
		else if (dataTypeAsInt == ItemDataType.TYPE_COLLATION.getValue()) {
			supportedType = true;
		}
		else if (dataTypeAsInt == ItemDataType.TYPE_VIEW_FORMAT.getValue()) {
			supportedType = true;
		}
		else if (dataTypeAsInt == ItemDataType.TYPE_FORMULA.getValue()) {
			supportedType = true;
		}
		else if (dataTypeAsInt == ItemDataType.TYPE_UNAVAILABLE.getValue()) {
			supportedType = true;
		}
		else if (dataTypeAsInt == ItemDataType.TYPE_MIME_PART.getValue()) {
			supportedType = true;
		}
		else if (dataTypeAsInt == ItemDataType.TYPE_RFC822_TEXT.getValue()) {
			supportedType = true;
		}
		else if (dataTypeAsInt == ItemDataType.TYPE_COMPOSITE.getValue()) {
			supportedType = true;
		}
		else if(dataTypeAsInt == ItemDataType.TYPE_CALENDAR_FORMAT.getValue()) {
      supportedType = true;
    }
		else if(dataTypeAsInt == ItemDataType.TYPE_USERID.getValue()) {
		  supportedType = true;
		} else if(dataTypeAsInt == ItemDataType.TYPE_USERDATA.getValue()) {
		  supportedType = true;
		}
		
		if (!supportedType) {
			throw new DominoException(format("Data type for value of item {0} is currently unsupported: {1}", itemName, dataTypeAsInt));
		}

		int checkDataType = valuePtr.getShort(0) & 0xffff;
		Pointer valueDataPtr = valuePtr.share(2);
		int valueDataLength = valueLength - 2;
		
		if (checkDataType!=dataTypeAsInt) {
			throw new IllegalStateException(format("Value data type does not meet expected date type: found {0}, expected {1}", checkDataType,
					dataTypeAsInt));
		}
		if (dataTypeAsInt == ItemDataType.TYPE_TEXT.getValue()) {
			String txtVal = (String) ItemDecoder.decodeTextValue(valueDataPtr, valueDataLength, false);
			return txtVal==null ? Collections.emptyList() : Arrays.asList((Object) txtVal);
		}
		else if (dataTypeAsInt == ItemDataType.TYPE_TEXT_LIST.getValue()) {
			List textList = valueDataLength==0 ? Collections.emptyList() : ItemDecoder.decodeTextListValue(valueDataPtr, false);
			return textList==null ? Collections.emptyList() : textList;
		}
		else if (dataTypeAsInt == ItemDataType.TYPE_NUMBER.getValue()) {
			double numVal = ItemDecoder.decodeNumber(valueDataPtr, valueDataLength);
			return Arrays.asList((Object) Double.valueOf(numVal));
		}
		else if (dataTypeAsInt == ItemDataType.TYPE_NUMBER_RANGE.getValue()) {
			List numberList = ItemDecoder.decodeNumberList(valueDataPtr, valueDataLength);
			return numberList==null ? Collections.emptyList() : numberList;
		}
		else if (dataTypeAsInt == ItemDataType.TYPE_TIME.getValue()) {
			DominoDateTime td = ItemDecoder.decodeTimeDateAsNotesTimeDate(valueDataPtr, valueDataLength);
			return Arrays.asList((Object) td);
		}
		else if (dataTypeAsInt == ItemDataType.TYPE_TIME_RANGE.getValue()) {
			List tdValues = ItemDecoder.decodeTimeDateListAsNotesTimeDate(valueDataPtr);
			return tdValues==null ? Collections.emptyList() : tdValues;
		}
		else if (dataTypeAsInt == ItemDataType.TYPE_OBJECT.getValue()) {
		  ObjectDescriptor objDescriptor = JNAMemoryUtils.readStructure(ObjectDescriptor.class, valueDataPtr);
			
			int rrv = objDescriptor.getRRV();
			
			ObjectDescriptor.ObjectType type = objDescriptor.getObjectType().orElse(ObjectDescriptor.ObjectType.UNKNOWN); 
			switch(type) {
		  case FILE: {
		    Pointer fileObjectPtr = valueDataPtr;
        
        NotesFileObjectStruct fileObject = NotesFileObjectStruct.newInstance(fileObjectPtr);
        fileObject.read();
        
        short compressionType = fileObject.CompressionType;
        NotesTimeDateStruct fileCreated = Objects.requireNonNull(fileObject.FileCreated, "Unexpected null value for fileObject.FileCreated");
        NotesTimeDateStruct fileModified = Objects.requireNonNull(fileObject.FileModified, "Unexpected null value for fileObject.FileModified");
        DominoDateTime fileCreatedWrap = new JNADominoDateTime(fileCreated.Innards);
        DominoDateTime fileModifiedWrap = new JNADominoDateTime(fileModified.Innards);
        
        short fileNameLength = fileObject.FileNameLength;
        long fileSize = Integer.toUnsignedLong(fileObject.FileSize);
        short flags = fileObject.Flags;
        
        Compression compression = null;
        for (Compression currComp : Compression.values()) {
          if (compressionType == currComp.getValue()) {
            compression = currComp;
            break;
          }
        }
        
        Pointer fileNamePtr = fileObjectPtr.share(JNANotesConstants.fileObjectSize);
        String fileName = NotesStringUtils.fromLMBCS(fileNamePtr, fileNameLength);
        
        JNAAttachment attInfo = new JNAAttachment(fileName, compression, flags, fileSize,
            fileCreatedWrap, fileModifiedWrap, this,
            itemBlockId, rrv);
        
        return Arrays.asList((Object) attInfo);
		  }
		  case ASSIST_RUNDATA: {
		    Optional info = AgentRunInfoDecoder.decodeAgentRunInfo(getParentDatabase(), valueDataPtr, valueDataLength);
		    return info.isPresent() ? Arrays.asList(info) : Collections.emptyList();
		  }
		  default:
	      //TODO add support for other object types
		    break;
			}
			
			//clone values because value data gets unlocked, preventing invalid memory access
			NotesObjectDescriptorStruct clonedObjDescriptor = NotesObjectDescriptorStruct.newInstance();
			clonedObjDescriptor.ObjectType = objDescriptor.getObjectType().map(t -> t.getValue()).orElse((short)0);
			clonedObjDescriptor.RRV = objDescriptor.getRRV();
			return Arrays.asList((Object) clonedObjDescriptor);
		}
		else if (dataTypeAsInt == ItemDataType.TYPE_NOTEREF_LIST.getValue()) {
			int numEntries = valueDataPtr.getShort(0) & 0xffff;
			
			//skip LIST structure, clone data to prevent invalid memory access when buffer gets disposed
			valueDataPtr = valueDataPtr.share(2);
			
			List unids = new ArrayList<>();
			
			for (int i=0; i {
				Pointer formulaPtr = Mem.OSLockObject(hFormulaTextByVal);
				try {
					int textLen = retFormulaTextLength.getValue() & 0xffff;
					String formula = NotesStringUtils.fromLMBCS(formulaPtr, textLen);
					return Arrays.asList((Object) formula);
				}
				finally {
					Mem.OSUnlockObject(hFormulaTextByVal);
					short localResult = Mem.OSMemFree(hFormulaTextByVal);
					NotesErrorUtils.checkResult(localResult);
				}
			});
		}
		else if (dataTypeAsInt == ItemDataType.TYPE_UNAVAILABLE.getValue()) {
			return Collections.emptyList();
		}
		else if (dataTypeAsInt == ItemDataType.TYPE_MIME_PART.getValue()) {
			NotesMIMEPartStruct mimePartStruct = NotesMIMEPartStruct.newInstance(valueDataPtr);
			mimePartStruct.read();
			
			int iByteCount = mimePartStruct.wByteCount & 0xffff;
			int iBoundaryLen = mimePartStruct.wBoundaryLen & 0xffff;
			int iHeadersLen = mimePartStruct.wHeadersLen & 0xffff;
			
			Pointer mimeBoundaryStrPtr = valueDataPtr.share(JNANotesConstants.mimePartSize);
			String boundaryStr = NotesStringUtils.fromLMBCS(mimeBoundaryStrPtr, iBoundaryLen);
			while (boundaryStr.startsWith("\r\n")) { //$NON-NLS-1$
				boundaryStr = boundaryStr.substring(2);
			}
			while (boundaryStr.endsWith("\r\n")) { //$NON-NLS-1$
				boundaryStr = boundaryStr.substring(0, boundaryStr.length()-2);
			}

			Pointer mimeHeadersPtr = mimeBoundaryStrPtr.share(mimePartStruct.wBoundaryLen & 0xffff);
			String headers = NotesStringUtils.fromLMBCS(mimeHeadersPtr, iHeadersLen);

			Pointer mimeDataPtr = mimeHeadersPtr.share(mimePartStruct.wHeadersLen & 0xffff);
			byte[] data = mimeDataPtr.getByteArray(0, iByteCount - iBoundaryLen - iHeadersLen);
			
			EnumSet options = EnumSet.noneOf(MimePartOptions.class);
			
			for (MimePartOptions currOpt : MimePartOptions.values()) {
				if ((mimePartStruct.dwFlags & currOpt.getValue()) == currOpt.getValue()) {
					options.add(currOpt);
				}
			}
			
			byte cPartType = mimePartStruct.cPartType;
			PartType partType;
			if (cPartType==NotesConstants.MIME_PART_PROLOG) {
				partType = PartType.PROLOG;
			}
			else if (cPartType==NotesConstants.MIME_PART_BODY) {
				partType = PartType.BODY;
			}
			else if (cPartType==NotesConstants.MIME_PART_EPILOG) {
				partType = PartType.EPILOG;
			}
			else if (cPartType==NotesConstants.MIME_PART_RETRIEVE_INFO) {
				partType = PartType.RETRIEVE_INFO;
			}
			else if (cPartType==NotesConstants.MIME_PART_MESSAGE) {
				partType = PartType.MESSAGE;
			}
			else {
				partType = null;
			}
			NotesMIMEPart mimePart = new NotesMIMEPart(this, options, partType, boundaryStr, headers, data);
			return Arrays.asList((Object) mimePart);
		}
		else if (dataTypeAsInt == ItemDataType.TYPE_RFC822_TEXT.getValue()) {
			// Read in the byte data and delegate to InternetHeaders
			ByteBuffer buf = valueDataPtr.getByteBuffer(0, valueDataLength);
			RFC822ItemDesc itemDesc = MemoryStructureUtil.forStructure(RFC822ItemDesc.class, () -> buf);
			int bodyOffset = 14;
			
			// Skip over NotesNative value
			bodyOffset += itemDesc.getNotesNativeLength();
			
			// Read the expected header name
			String inetName = NotesStringUtils.fromLMBCS(valueDataPtr.share(bodyOffset), itemDesc.getNameLength());
			
			int dataLen = itemDesc.getNameLength()+itemDesc.getDelimiterLength()+itemDesc.getBodyLength();
			
			// Read in the item data
			byte[] headerBytes = new byte[dataLen];
			valueDataPtr.read(bodyOffset, headerBytes, 0, dataLen);
			InternetHeaders headerHolder;
			try(InputStream is = new ByteArrayInputStream(headerBytes)) {
				headerHolder = new InternetHeaders(is);
			} catch (MessagingException | IOException e) {
				throw new DominoException("Exception while translating RFC 822 text", e);
			}
			String[] headers = headerHolder.getHeader(inetName);
			return headers == null ? Collections.emptyList() : Arrays.stream(headers).collect(Collectors.toList());
		}
		else if(dataTypeAsInt == ItemDataType.TYPE_COMPOSITE.getValue()) {
			@SuppressWarnings("unchecked")
			List result = (List)(List)getRichTextItem(itemName);
			return result;
		}
		else if (dataTypeAsInt == ItemDataType.TYPE_USERID.getValue()) {
		  PreV3Author result = NotesItemDataUtil.parsePreV3Author(valueDataPtr.getByteBuffer(0, valueDataLength));
		  return Arrays.asList((Object)result);
		}
		else if(dataTypeAsInt == ItemDataType.TYPE_USERDATA.getValue()) {
		  UserData result = NotesItemDataUtil.parseUserData(valueDataPtr.getByteBuffer(0, valueDataLength));
		  return Arrays.asList((Object)result);
		}
		else {
			throw new DominoException(format("Data type for value of item {0} is currently unsupported: {1}", itemName, dataTypeAsInt));
		}
	}

	@Override
	public int getNoteID() {
		checkDisposed();
		JNADocumentAllocations allocations = getAllocations();
		
		try(DisposableMemory retNoteId = new DisposableMemory(4)) {
			retNoteId.clear();

			return LockUtil.lockHandle(allocations.getNoteHandle(), (handleByVal) -> {
				NotesCAPI.get().NSFNoteGetInfo(handleByVal, NotesConstants._NOTE_ID, retNoteId);
				return retNoteId.getInt(0);
			});
		}
	}
	
	@Override
	public Optional getThreadID() {
	  String id = get(NotesConstants.ITEM_THREAD_ID, String.class, ""); //$NON-NLS-1$
	  if(id != null && !id.isEmpty()) {
	    return Optional.of(id);
	  } else {
	    return Optional.empty();
	  }
	}

	@Override
	public String getUNID() {
		NotesOriginatorIdStruct oid = getOIDStruct();
		String unid = oid.getUNIDAsString();
		return unid;
	}

	@Override
	public int getSequenceNumber() {
		return getOIDStruct().Sequence;
	}

	/**
	 * Internal method to get the populated {@link NotesOriginatorIdStruct} object
	 * for this note
	 * 
	 * @return oid structure
	 */
	private NotesOriginatorIdStruct getOIDStruct() {
		checkDisposed();
		JNADocumentAllocations allocations = getAllocations();

		return LockUtil.lockHandle(allocations.getNoteHandle(), (handleByVal) -> {
			Memory retOid = new Memory(JNANotesConstants.oidSize);
			retOid.clear();
			
			NotesCAPI.get().NSFNoteGetInfo(handleByVal, NotesConstants._NOTE_OID, retOid);
			
			NotesOriginatorIdStruct oidStruct = NotesOriginatorIdStruct.newInstance(retOid);
			oidStruct.read();
			return oidStruct;
		});
	}

	@Override
	public DominoOriginatorId getOID() {
		NotesOriginatorIdStruct oidStruct = getOIDStruct();
		JNADominoOriginatorId oid = new JNADominoOriginatorId(oidStruct);
		return oid;
	}
	
	@Override
	public DominoDateTime getCreated() {
		checkDisposed();
		
		DominoDateTime creationDate = get("$CREATED", DominoDateTime.class, null); //$NON-NLS-1$
		if (creationDate!=null) {
			return creationDate;
		}
		
		NotesOriginatorIdStruct oidStruct = getOIDStruct();
		NotesTimeDateStruct creationDateStruct = oidStruct.Note;
		return new JNADominoDateTime(creationDateStruct.Innards);
	}

	@Override
	public DominoDateTime getLastModified() {
		DominoOriginatorId oid = getOID();
		return oid.getSequenceTime();
	}

	@Override
	public DominoDateTime getModifiedInThisFile() {
		checkDisposed();
		JNADocumentAllocations allocations = getAllocations();
		
		try(DisposableMemory retTimeDate = new DisposableMemory(JNANotesConstants.timeDateSize)) {
			retTimeDate.clear();

			return LockUtil.lockHandle(allocations.getNoteHandle(), (handleByVal) -> {
				NotesCAPI.get().NSFNoteGetInfo(handleByVal, NotesConstants._NOTE_MODIFIED, retTimeDate);
				NotesTimeDateStruct td = NotesTimeDateStruct.newInstance(retTimeDate);
				td.read();
				return new JNADominoDateTime(td.Innards);

			});
		}
	}

	@Override
	public DominoDateTime getLastAccessed() {
		checkDisposed();
		JNADocumentAllocations allocations = getAllocations();
		
		try(DisposableMemory retTimeDate = new DisposableMemory(JNANotesConstants.timeDateSize)) {
			retTimeDate.clear();

			return LockUtil.lockHandle(allocations.getNoteHandle(), (handleByVal) -> {
				NotesCAPI.get().NSFNoteGetInfo(handleByVal, NotesConstants._NOTE_ACCESSED, retTimeDate);
				NotesTimeDateStruct td = NotesTimeDateStruct.newInstance(retTimeDate);
				td.read();
				return new JNADominoDateTime(td.Innards);

			});
		}
	}

	@Override
	public DominoDateTime getAddedToFile() {
		checkDisposed();
		JNADocumentAllocations allocations = getAllocations();
		
		try(DisposableMemory retTimeDate = new DisposableMemory(JNANotesConstants.timeDateSize)) {
			retTimeDate.clear();

			return LockUtil.lockHandle(allocations.getNoteHandle(), (handleByVal) -> {
				NotesCAPI.get().NSFNoteGetInfo(handleByVal, NotesConstants._NOTE_ADDED_TO_FILE, retTimeDate);
				NotesTimeDateStruct td = NotesTimeDateStruct.newInstance(retTimeDate);
				td.read();
				return new JNADominoDateTime(td.Innards);
			});
		}
	}
	
	@Override
	public Optional getFirstItem(String itemName) {
		Objects.requireNonNull(itemName, "Item name cannot be null. Use getItems() instead.");
		
		final Item[] retItem = new Item[1];
		
		getItems(itemName, new IItemCallback() {

			@Override
			public void itemNotFound() {
				retItem[0]=null;
			}

			@Override
			public Action itemFound(Item itemInfo) {
				retItem[0] = itemInfo;
				return Action.Stop;
			}
		});
		
		return Optional.ofNullable(retItem[0]);
	
	}

	/**
	 * Callback interface for {@link JNADocument#getItems(IItemCallback)}
	 * 
	 * @author Karsten Lehmann
	 */
	private interface IItemCallback {
		public enum Action {Continue, Stop};
		
		/**
		 * Method is called when an item could not be found in the note
		 */
		default void itemNotFound() {};
		
		/**
		 * Method is called for each item in the note. A note may contain the same item name
		 * multiple times. In this case, the method is called for each item instance
		 * 
		 * @param item item object with meta data and access method to decode item value
		 * @return next action, either continue or stop scan
		 */
		Action itemFound(Item item);
	}

	private static class LoopImpl extends Loop {
		
		@Override
		public void setIndex(int index) {
			super.setIndex(index);
		}

		public void next() {
			super.setIndex(getIndex()+1);
		}

		@Override
		public void setIsLast() {
			super.setIsLast();
		}
	}
	

	@Override
	public Document forEachItem(BiConsumer consumer) {
		return forEachItem(null, consumer);
	}

	@Override
	public Document forEachItem(final String searchForItemName, BiConsumer consumer) {
		LoopImpl loop = new LoopImpl();
		
		AtomicInteger itemIdx = new AtomicInteger(-1);
		//to be able to report that the item is the last one, we need to prefetch one
		AtomicReference lastReadItem = new AtomicReference<>();
		
		getItems(searchForItemName, item -> {
			if (itemIdx.get() == -1) {
				//first match
				lastReadItem.set(item);
			}
			else {
				//report last read
				consumer.accept(lastReadItem.get(), loop);
				//store item for next loop run
				lastReadItem.set(item);
			}
			
			loop.setIndex(itemIdx.incrementAndGet());
			
			if (loop.isStopped()) {
				return IItemCallback.Action.Stop;
			}
			else {
				return IItemCallback.Action.Continue;
			}
		});
		
		//report last item
		Item lastItem = lastReadItem.get();
		if (lastItem != null && !loop.isStopped()) {
			loop.setIsLast();
			
			consumer.accept(lastItem, loop);
		}
		return this;
	}
	
	@Override
	public Stream allItems() {
		List items = new ArrayList<>();
		getItems(null, item -> {
			items.add(item);
			return IItemCallback.Action.Continue;
		});
		
		return items.stream();
	}


	
	/**
	 * Scans through all items of this note that have the specified name
	 * 
	 * @param searchForItemName item name to search for or null to scan through all items
	 * @param callback callback is called for each scan result
	 */
	private void getItems(final String searchForItemName, final IItemCallback callback) {
		checkDisposed();
		
		Memory itemNameMem = StringUtil.isEmpty(searchForItemName) ? null : NotesStringUtils.toLMBCS(searchForItemName, false);
		
		NotesBlockIdStruct.ByReference itemBlockId = NotesBlockIdStruct.ByReference.newInstance();
		NotesBlockIdStruct.ByReference valueBlockId = NotesBlockIdStruct.ByReference.newInstance();
		ShortByReference retDataType = new ShortByReference();
		IntByReference retValueLen = new IntByReference();
		
		JNADocumentAllocations allocations = getAllocations();
		short result = LockUtil.lockHandle(allocations.getNoteHandle(), (noteHandleByVal) -> {
			return NotesCAPI.get().NSFItemInfo(noteHandleByVal, itemNameMem,
					itemNameMem==null ? 0 : (short) (itemNameMem.size() & 0xffff),
					itemBlockId, retDataType, valueBlockId, retValueLen);
		});
		
		if (result == INotesErrorConstants.ERR_ITEM_NOT_FOUND) {
			callback.itemNotFound();
			return;
		}

		NotesErrorUtils.checkResult(result);
		
		NotesBlockIdStruct itemBlockIdClone = NotesBlockIdStruct.newInstance();
		itemBlockIdClone.pool = itemBlockId.pool;
		itemBlockIdClone.block = itemBlockId.block;
		itemBlockIdClone.write();
		
		NotesBlockIdStruct valueBlockIdClone = NotesBlockIdStruct.newInstance();
		valueBlockIdClone.pool = valueBlockId.pool;
		valueBlockIdClone.block = valueBlockId.block;
		valueBlockIdClone.write();
		
		int dataType = retDataType.getValue();
		
		Item itemInfo = new JNAItem(this, itemBlockIdClone, dataType,
				valueBlockIdClone);
		
		IItemCallback.Action action = callback.itemFound(itemInfo);
		if (action != IItemCallback.Action.Continue) {
			return;
		}
		
		while (true) {
			IntByReference retNextValueLen = new IntByReference();
			
			NotesBlockIdStruct.ByValue itemBlockIdByVal = NotesBlockIdStruct.ByValue.newInstance();
			itemBlockIdByVal.pool = itemBlockId.pool;
			itemBlockIdByVal.block = itemBlockId.block;
			
			result = LockUtil.lockHandle(allocations.getNoteHandle(), (noteHandleByVal) -> {
				return  NotesCAPI.get().NSFItemInfoNext(noteHandleByVal, itemBlockIdByVal,
						itemNameMem, itemNameMem==null ? 0 : (short) (itemNameMem.size() & 0xffff), itemBlockId, retDataType,
						valueBlockId, retNextValueLen);
			});
			
			if (result == INotesErrorConstants.ERR_ITEM_NOT_FOUND) {
				return;
			}

			NotesErrorUtils.checkResult(result);

			itemBlockIdClone = NotesBlockIdStruct.newInstance();
			itemBlockIdClone.pool = itemBlockId.pool;
			itemBlockIdClone.block = itemBlockId.block;
			itemBlockIdClone.write();
			
			valueBlockIdClone = NotesBlockIdStruct.newInstance();
			valueBlockIdClone.pool = valueBlockId.pool;
			valueBlockIdClone.block = valueBlockId.block;
			valueBlockIdClone.write();
			
			dataType = retDataType.getValue();

			itemInfo = new JNAItem(this, itemBlockIdClone, dataType,
					valueBlockIdClone);
			
			action = callback.itemFound(itemInfo);
			if (action != IItemCallback.Action.Continue) {
				return;
			}
		}
	}
	
	@Override
	public Optional getAttachment(String fileName) {
		final JNAAttachment[] foundAttInfo = new JNAAttachment[1];
		
		getItems("$file", new IItemCallback() { //$NON-NLS-1$
			
			@Override
			public void itemNotFound() {
			}
			
			@Override
			public Action itemFound(Item item) {
				List values = item.getValue();
				if (values!=null && !values.isEmpty() && values.get(0) instanceof JNAAttachment) {
					JNAAttachment attInfo = (JNAAttachment) values.get(0);
					if (attInfo.getFileName().equalsIgnoreCase(fileName)) {
						foundAttInfo[0] = attInfo;
						return Action.Stop;
					}
				}
				return Action.Continue;
			}
		});
		return Optional.ofNullable(foundAttInfo[0]);
	}

	@Override
	public Document forEachAttachment(BiConsumer consumer) {
		List attachments = new ArrayList<>();
		
		getItems("$file", new IItemCallback() { //$NON-NLS-1$
			
			@Override
			public void itemNotFound() {
			}
			
			@Override
			public Action itemFound(Item item) {
				List values = item.getValue();
				if (values!=null && !values.isEmpty() && values.get(0) instanceof Attachment) {
					Attachment attInfo = (Attachment) values.get(0);
					attachments.add(attInfo);
				}
				return Action.Continue;
			}
		});
		
		LoopImpl loop = new LoopImpl();
		Iterator attachmentsIt = attachments.iterator();
		
		while (attachmentsIt.hasNext() && !loop.isStopped()) {
			Attachment currAtt = attachmentsIt.next();
			
			if (!attachmentsIt.hasNext()) {
				loop.setIsLast();
			}
			consumer.accept(currAtt, loop);
			
			loop.next();
		}
		
		return this;
	}
	
	@Override
	public Document forEachCertificate(BiConsumer consumer) {
	  Objects.requireNonNull(consumer, "consumer must not be null");
	  
	  CertificateFactory cf;
	  try {
      cf = CertificateFactory.getInstance("X.509"); //$NON-NLS-1$
    } catch (CertificateException e) {
      throw new RuntimeException(e);
    }
	  
	  NotesErrorUtils.checkResult(LockUtil.lockHandle(getAllocations().getNoteHandle(), (noteHandleByVal) -> {
	    LoopImpl loop = new LoopImpl();
	    NotesCallbacks.SECNABENUMPROC proc = (pCallCtx, pCert, certSize, reserved1, reserved2) -> {
	      if(certSize < 0) {
	        // DWORD larger than INT_MAX
	        throw new UnsupportedOperationException(MessageFormat.format("Unable to operate on certificate with size larger than {0} bytes", Integer.MAX_VALUE));
	      }
	      byte[] certData = pCert.getByteArray(0, certSize);
	      try(ByteArrayInputStream bais = new ByteArrayInputStream(certData)) {
	        X509Certificate cert = (X509Certificate) cf.generateCertificate(bais);
	        consumer.accept(cert, loop);
	      } catch (IOException e) {
          throw new UncheckedIOException(e);
        } catch (CertificateException e) {
          throw new RuntimeException("Encountered exception when parsing certificate data", e);
        }
	      
	      return !loop.isStopped();
	    };
      return NotesCAPI.get().SECNABEnumerateCertificates(noteHandleByVal, proc, null, 0, null);
    }));
	  
	  return this;
	}
	
	@Override
	public void attachCertificate(X509Certificate certificate) {
	  Objects.requireNonNull(certificate, "certificate cannot be null");
	  NotesErrorUtils.checkResult(LockUtil.lockHandle(getAllocations().getNoteHandle(), (noteHandleByVal) -> {
	    try {
        byte[] certData = certificate.getEncoded();
        try(DisposableMemory mem = new DisposableMemory(certData.length)) {
          mem.write(0, certData, 0, certData.length);
          return NotesCAPI.get().SECNABAddCertificate(noteHandleByVal, mem, certData.length, 0, null);
        }
      } catch (CertificateEncodingException e) {
        throw new RuntimeException(e);
      }
	  }));
	}
	
	@Override
	public Document removeCertificate(X509Certificate certificate) {
	  if(certificate == null) {
	    return this;
	  }
	  
	  NotesErrorUtils.checkResult(LockUtil.lockHandle(getAllocations().getNoteHandle(), (noteHandleByVal) -> {
      try {
        byte[] certData = certificate.getEncoded();
        try(DisposableMemory mem = new DisposableMemory(certData.length)) {
          mem.write(0, certData, 0, certData.length);
          return NotesCAPI.get().SECNABRemoveCertificate(noteHandleByVal, mem, certData.length, 0, null);
        }
      } catch (CertificateEncodingException e) {
        throw new RuntimeException(e);
      }
    }));
	  
	  return this;
	}

	@Override
	public Document replaceItemValue(String itemName, Object value) {
		return replaceItemValue(itemName, EnumSet.of(ItemFlag.SUMMARY), value, false);
	}

	@Override
	public Document replaceItemValue(String itemName, Object value, boolean allowDataTypeChanges) {
		return replaceItemValue(itemName, EnumSet.of(ItemFlag.SUMMARY), value, allowDataTypeChanges);
	}

	@Override
	public Document replaceItemValue(String itemName, Set flags, Object value) {
		return replaceItemValue(itemName, flags, value, false);
	}

	@SuppressWarnings("rawtypes")
	private boolean hasSupportedItemObjectType(Object value) {
		if (value==null) {
			return true;
		}
		else if (value instanceof String) {
			return true;
		}
		else if (value instanceof Number) {
			return true;
		}
		else if (value instanceof Calendar || value instanceof Date || value instanceof Temporal) {
			return true;
		}
		else if (value instanceof Iterable && !((Iterable)value).iterator().hasNext()) {
			return true;
		}
		else if (value instanceof Iterable && isStringList((Iterable) value)) {
			return true;
		}
		else if (value instanceof Iterable && isNumberOrNumberArrayList((Iterable) value)) {
			return true;
		}
		else if (value instanceof Iterable && isCalendarOrCalendarArrayList((Iterable) value)) {
			return true;
		}
		else if (value instanceof DominoDateRange) {
			return true;
		}
		else if (value instanceof DominoUniversalNoteId) {
			return true;
		}
		else if (value instanceof JNAFormula) {
			return true;
		}
    else if (value instanceof DominoViewFormat) {
      return true;
    }
    else if (value instanceof DominoCalendarFormat) {
      return true;
    }
    else if (value instanceof DominoCollationInfo) {
      return true;
    }
    else if (value instanceof UserData) {
      return true;
    }
		return false;
	}

	private boolean isNumberOrNumberArrayList(Iterable list) {
		if (list==null || !list.iterator().hasNext()) {
			return false;
		}
		for (Object currObj : list) {
			boolean isAccepted=false;
			
			if (currObj instanceof double[]) {
				double[] valArr = (double[]) currObj;
				if (valArr.length==2) {
					isAccepted = true;
				}
			}
			if (currObj instanceof int[]) {
				int[] valArr = (int[]) currObj;
				if (valArr.length==2) {
					isAccepted = true;
				}
			}
			if (currObj instanceof long[]) {
				long[] valArr = (long[]) currObj;
				if (valArr.length==2) {
					isAccepted = true;
				}
			}
			if (currObj instanceof float[]) {
				float[] valArr = (float[]) currObj;
				if (valArr.length==2) {
					isAccepted = true;
				}
			}
			else if (currObj instanceof Number[]) {
				Number[] valArr = (Number[]) currObj;
				if (valArr.length==2) {
					isAccepted = true;
				}
			}
			else if (currObj instanceof Integer[]) {
				Integer[] valArr = (Integer[]) currObj;
				if (valArr.length==2) {
					isAccepted = true;
				}
			}
			else if (currObj instanceof Long[]) {
				Long[] valArr = (Long[]) currObj;
				if (valArr.length==2) {
					isAccepted = true;
				}
			}
			else if (currObj instanceof Double[]) {
				Double[] valArr = (Double[]) currObj;
				if (valArr.length==2) {
					isAccepted = true;
				}
			}
			else if (currObj instanceof Float[]) {
				Float[] valArr = (Float[]) currObj;
				if (valArr.length==2) {
					isAccepted = true;
				}
			}
			else if (currObj instanceof Number) {
				isAccepted = true;
			}
			
			if (!isAccepted) {
				return false;
			}
		}
		return true;
	}
	
	private boolean isStringList(Iterable list) {
		if (list==null || !list.iterator().hasNext()) {
			return false;
		}
		if(!StreamSupport.stream(list.spliterator(), false).allMatch(String.class::isInstance)) {
		  return false;
		}
		return true;
	}

	private boolean isCalendarOrCalendarArrayList(Iterable list) {
		if (list==null || !list.iterator().hasNext()) {
			return false;
		}
		for(Object currObj : list) {
			boolean isAccepted=false;
			
			if (currObj instanceof Calendar[]) {
				Calendar[] calArr = (Calendar[]) currObj;
				if (calArr.length==2) {
					isAccepted = true;
				}
			}
			else if (currObj instanceof Date[]) {
				Date[] dateArr = (Date[]) currObj;
				if (dateArr.length==2) {
					isAccepted = true;
				}
			}
			else if (currObj instanceof DominoDateTime[]) {
				DominoDateTime[] ndtArr = (DominoDateTime[]) currObj;
				if (ndtArr.length==2) {
					isAccepted = true;
				}
			}
			else if (currObj instanceof Calendar) {
				isAccepted = true;
			}
			else if (currObj instanceof Date) {
				isAccepted = true;
			}
			else if (currObj instanceof DominoDateTime) {
				isAccepted = true;
			}
			else if (currObj instanceof DominoDateRange) {
				isAccepted = true;
			}
			
			if (!isAccepted) {
				return false;
			}
		}
		return true;
	}

	private static String dumpValueType(Object value) {
		if (value instanceof List) {
			List valueList = (List) value;
			StringBuilder sb = new StringBuilder();
			sb.append(value.getClass().getName()).append(" ["); //$NON-NLS-1$
			for (int i=0; i0) {
					sb.append(", "); //$NON-NLS-1$
				}
				sb.append(dumpValueType(valueList.get(i)));
			}
			sb.append("]"); //$NON-NLS-1$
			return sb.toString();
		}
		else if (value!=null) {
			return value.getClass().getName();
		}
		else {
			return "null"; //$NON-NLS-1$
		}
	}

	@Override
	public Document replaceItemValue(String itemName, Set flags, Object value, boolean allowDataTypeChanges) {
		DocumentValueConverter converter = null;
		
		if (!hasSupportedItemObjectType(value)) {
			converter = JNXServiceFinder.findServices(DocumentValueConverter.class)
				.filter(c -> c.supportsWrite(value.getClass(), value))
				.sorted(Comparator.comparing(DocumentValueConverter::getPriority).reversed())
				.findFirst()
				.orElse(null);

			if (converter==null) {
				throw new IllegalArgumentException(format("Unsupported value type: {0}", dumpValueType(value)));
			}
		}
		
		while (hasItem(itemName)) {
			removeItem(itemName);
		}
		
		if (value!=null) {
			return appendItemValue(itemName, flags, value, converter, allowDataTypeChanges);
		}
		else {
			return this;
		}
	}
	
	@Override
	public Document replaceItemValuePlaceholder(String itemName) {
	  while (hasItem(itemName)) {
          removeItem(itemName);
      }
	  
	  short flagsShort = ItemFlag.PLACEHOLDER.getValue().shortValue();
	  Memory itemNameMem = NotesStringUtils.toLMBCS(itemName, false);
    
      short result = LockUtil.lockHandle(getAllocations().getNoteHandle(), (noteHandleByVal) -> {
        return NotesCAPI.get().NSFItemAppend(noteHandleByVal, flagsShort, itemNameMem,
                (short) (itemNameMem==null ? 0 : itemNameMem.size()), ItemDataType.TYPE_INVALID_OR_UNKNOWN.getValue(),
                null, 0);
      });
      NotesErrorUtils.checkResult(result);
      
      return this;
    }

	private List toNumberOrNumberArrayList(Iterable list) {
		boolean allNumbers = StreamSupport.stream(list.spliterator(), false)
		    .allMatch(i -> i instanceof double[] || i instanceof Double);
		
		if (allNumbers) {
			return StreamSupport.stream(list.spliterator(), false).collect(Collectors.toList());
		}
		
		List convertedList = new ArrayList<>();
		for (Object obj : list) {
			if (obj instanceof Number) {
				//ok
				convertedList.add(((Number)obj).doubleValue());
			}
			else if (obj instanceof double[]) {
				if (((double[])obj).length!=2) {
					throw new IllegalArgumentException("Length of double array entry must be 2 for number ranges");
				}
				//ok
				convertedList.add(obj);
			}
			else if (obj instanceof Number[]) {
				Number[] numberArr = (Number[]) obj;
				if (numberArr.length!=2) {
					throw new IllegalArgumentException("Length of Number array entry must be 2 for number ranges");
				}
				
				convertedList.add(new double[] {
						numberArr[0].doubleValue(),
						numberArr[1].doubleValue()
				});
			}
			else if (obj instanceof Double[]) {
				Double[] doubleArr = (Double[]) obj;
				if (doubleArr.length!=2) {
					throw new IllegalArgumentException("Length of Number array entry must be 2 for number ranges");
				}
				
				convertedList.add(new double[] {
						doubleArr[0],
						doubleArr[1]
				});
			}
			else if (obj instanceof Integer[]) {
				Integer[] integerArr = (Integer[]) obj;
				if (integerArr.length!=2) {
					throw new IllegalArgumentException("Length of Integer array entry must be 2 for number ranges");
				}
				
				convertedList.add(new double[] {
						integerArr[0].doubleValue(),
						integerArr[1].doubleValue()
				});
			}
			else if (obj instanceof Long[]) {
				Long[] longArr = (Long[]) obj;
				if (longArr.length!=2) {
					throw new IllegalArgumentException("Length of Long array entry must be 2 for number ranges");
				}
				
				convertedList.add(new double[] {
						longArr[0].doubleValue(),
						longArr[1].doubleValue()
				});
			}
			else if (obj instanceof Float[]) {
				Float[] floatArr = (Float[]) obj;
				if (floatArr.length!=2) {
					throw new IllegalArgumentException("Length of Float array entry must be 2 for number ranges");
				}
				
				convertedList.add(new double[] {
						floatArr[0].doubleValue(),
						floatArr[1].doubleValue()
				});
			}
			else if (obj instanceof int[]) {
				int[] intArr = (int[]) obj;
				if (intArr.length!=2) {
					throw new IllegalArgumentException("Length of int array entry must be 2 for number ranges");
				}
				
				convertedList.add(new double[] {
						intArr[0],
						intArr[1]
				});
			}
			else if (obj instanceof long[]) {
				long[] longArr = (long[]) obj;
				if (longArr.length!=2) {
					throw new IllegalArgumentException("Length of long array entry must be 2 for number ranges");
				}
				
				convertedList.add(new double[] {
						longArr[0],
						longArr[1]
				});
			}
			else if (obj instanceof float[]) {
				float[] floatArr = (float[]) obj;
				if (floatArr.length!=2) {
					throw new IllegalArgumentException("Length of float array entry must be 2 for number ranges");
				}
				
				convertedList.add(new double[] {
						floatArr[0],
						floatArr[1]
				});
			}
			else {
				throw new IllegalArgumentException(format("Unsupported date format found in list: {0}", (obj==null ? "null" : obj.getClass().getName()))); //$NON-NLS-2$
			}
		}
		return convertedList;
	}
	
	private List toDateTimeOrDateTimeRangeList(Iterable list) {
		boolean allDateTime = StreamSupport.stream(list.spliterator(), false)
		    .allMatch(i -> i instanceof DominoDateTime[] || i instanceof DominoDateTime);
		
		if (allDateTime) {
			return StreamSupport.stream(list.spliterator(), false).collect(Collectors.toList());
		}
		
		List convertedList = new ArrayList<>();
		for (Object obj : list) {
			if (obj instanceof GregorianCalendar) {
				convertedList.add(new JNADominoDateTime(((GregorianCalendar) obj).toZonedDateTime()));
			}
			else if (obj instanceof Calendar[]) {
				Calendar[] calArr = (Calendar[]) obj;
				if (calArr.length!=2) {
					throw new IllegalArgumentException("Length of Calendar array entry must be 2 for date ranges");
				}
				DominoDateTime start = new JNADominoDateTime((((GregorianCalendar)calArr[0]).toZonedDateTime()));
				DominoDateTime end = new JNADominoDateTime((((GregorianCalendar)calArr[1]).toZonedDateTime()));
				convertedList.add(new DefaultDominoDateRange(start, end));
			}
			else if (obj instanceof Date) {
				Date dt = (Date) obj;
				convertedList.add(new JNADominoDateTime(dt.getTime()));
			}
			else if (obj instanceof DominoDateTime) {
				convertedList.add(obj);
			}
			else if (obj instanceof Date[]) {
				Date[] dateArr = (Date[]) obj;
				if (dateArr.length!=2) {
					throw new IllegalArgumentException("Length of Date array entry must be 2 for date ranges");
				}
				DominoDateTime start = new JNADominoDateTime(dateArr[0].getTime());
				DominoDateTime end = new JNADominoDateTime(dateArr[1].getTime());
				convertedList.add(new DefaultDominoDateRange(start, end));
			}
			else if (obj instanceof DominoDateTime[]) {
				DominoDateTime[] ntdArr = (DominoDateTime[]) obj;
				if (ntdArr.length!=2) {
					throw new IllegalArgumentException("Length of DominoDateTime array entry must be 2 for date ranges");
				}
				convertedList.add(new DefaultDominoDateRange(ntdArr[0], ntdArr[1]));
			}
			else if(obj instanceof DominoDateRange) {
				DominoDateRange range = (DominoDateRange)obj;
				convertedList.add(new DefaultDominoDateRange(range.getStartDateTime(), range.getEndDateTime()));
			}
			else {
				throw new IllegalArgumentException(format("Unsupported date format found in list: {0}", (obj==null ? "null" : obj.getClass().getName()))); //$NON-NLS-2$
			}
		}
		return convertedList;
	}
	
	private ThreadLocal>> writingItemType = ThreadLocal.withInitial(HashSet::new);

	@Override
	public Document appendItemValue(String itemName, Object value) {
		return appendItemValue(itemName, EnumSet.of(ItemFlag.SUMMARY), value, false);
	}

	@Override
	public Document appendItemValue(String itemName, Set flags, Object value) {
		return appendItemValue(itemName, flags, value, false);
	}
	
	@Override
	public Document appendItemValue(String itemName, Set flags, Object value, boolean allowDataTypeChanges) {
		DocumentValueConverter converter = null;
		
		if (!hasSupportedItemObjectType(value)) {
			converter = JNXServiceFinder.findServices(DocumentValueConverter.class)
				.filter(c -> c.supportsWrite(value.getClass(), value))
				.sorted(Comparator.comparing(DocumentValueConverter::getPriority).reversed())
				.findFirst()
				.orElse(null);

			if (converter==null) {
				throw new IllegalArgumentException(format("Unsupported value type: {0}", dumpValueType(value)));
			}
		}
		
		return appendItemValue(itemName, flags, value, converter, true);
	}
	
	@SuppressWarnings("deprecation")
	private Document appendItemValue(String itemName, Set flagsOrig,
			Object value, DocumentValueConverter valueConverter, boolean allowDataTypeChanges) {
		
		checkDisposed();

		//remove our own pseudo flags:
		boolean keepLineBreaks = flagsOrig.contains(ItemFlag.KEEPLINEBREAKS);
		EnumSet flags = EnumSet.copyOf(flagsOrig);
		flags.remove(ItemFlag.KEEPLINEBREAKS);

		if (value instanceof JNAFormula) {
			//formulas are stored in compiled binary format
			flags.remove(ItemFlag.SUMMARY);
		}
		else if (value instanceof DominoViewFormat) {
		  flags.add(ItemFlag.SUMMARY);
		  flags.add(ItemFlag.SIGNED);
		}
		
		if (value instanceof String) {
			Memory strValueMem;
			if (keepLineBreaks) {
				strValueMem = NotesStringUtils.toLMBCS((String)value, false, false);
			}
			else {
				strValueMem = NotesStringUtils.toLMBCS((String)value, false);
			}

			int valueSize = (int) (2 + (strValueMem==null ? 0 : strValueMem.size()));
			
			DHANDLE.ByReference rethItem = DHANDLE.newInstanceByReference();
			short result = Mem.OSMemAlloc((short) 0, valueSize, rethItem);
			NotesErrorUtils.checkResult(result);
			
			return LockUtil.lockHandle(rethItem, (hItemByVal) -> {
				Pointer valuePtr = Mem.OSLockObject(hItemByVal);
				
				try {
					valuePtr.setShort(0, ItemDataType.TYPE_TEXT.getValue().shortValue());
					valuePtr = valuePtr.share(2);
					if (strValueMem!=null) {
						valuePtr.write(0, strValueMem.getByteArray(0, (int) strValueMem.size()), 0, (int) strValueMem.size());
					}
					return appendItemValue(itemName, flags, hItemByVal, valueSize);
				}
				finally {
					Mem.OSUnlockObject(hItemByVal);
				}
			});
		
		}
		else if (value instanceof Number) {
			int valueSize = 2 + 8;
			
			DHANDLE.ByReference rethItem = DHANDLE.newInstanceByReference();
			short result = Mem.OSMemAlloc((short) 0, valueSize, rethItem);
			NotesErrorUtils.checkResult(result);
			
			return LockUtil.lockHandle(rethItem, (hItemByVal) -> {
				Pointer valuePtr = Mem.OSLockObject(hItemByVal);
				
				try {
					valuePtr.setShort(0, ItemDataType.TYPE_NUMBER.getValue().shortValue());
					valuePtr = valuePtr.share(2);
					valuePtr.setDouble(0, ((Number)value).doubleValue());
					return appendItemValue(itemName, flags, hItemByVal, valueSize);
				}
				finally {
					Mem.OSUnlockObject(hItemByVal);
				}
			});
		}
		else if (value instanceof Calendar || value instanceof Temporal || value instanceof Date) {
			int[] innards;
			
			if (value instanceof DominoDateTime) {
				//no date conversion to innards needing, we already have them
				innards = ((DominoDateTime)value).getAdapter(int[].class);
			}
			else if (value instanceof Calendar) {
				Calendar calValue = (Calendar) value;
				innards = NotesDateTimeUtils.calendarToInnards(calValue);
			}
			else if (value instanceof Date) {
				innards = new JNADominoDateTime(((Date)value).toInstant()).getInnards();
			}
			else if(value instanceof Temporal) {
				innards = new JNADominoDateTime((Temporal)value).getInnards();
			}
			else {
				throw new UnsupportedItemValueError(format("Unsupported value type: {0}", (value==null ? "null" : value.getClass().getName()))); //$NON-NLS-2$
			}

			int valueSize = 2 + 8;
			
			DHANDLE.ByReference rethItem = DHANDLE.newInstanceByReference();
			short result = Mem.OSMemAlloc((short) 0, valueSize, rethItem);
			NotesErrorUtils.checkResult(result);
			
			return LockUtil.lockHandle(rethItem, (hItemByVal) -> {
				Pointer valuePtr = Mem.OSLockObject(hItemByVal);
				
				try {
					valuePtr.setShort(0, ItemDataType.TYPE_TIME.getValue().shortValue());
					valuePtr = valuePtr.share(2);

					NotesTimeDateStruct timeDate = NotesTimeDateStruct.newInstance(valuePtr);
					timeDate.Innards[0] = innards[0];
					timeDate.Innards[1] = innards[1];
					timeDate.write();

					return appendItemValue(itemName, flags, hItemByVal, valueSize);
				}
				finally {
					Mem.OSUnlockObject(hItemByVal);
				}
			});
		}
		else if (value instanceof Iterable && (!((Iterable)value).iterator().hasNext() || isStringList((Iterable) value))) {
			@SuppressWarnings("unchecked")
			List strList = StreamSupport.stream(((Iterable) value).spliterator(), false)
			  .collect(Collectors.toList());
			
			if (strList.size() > 65535) {
				throw new IllegalArgumentException(format("String list size must fit in a WORD ({0}>65535)", strList.size()));
			}
			
			boolean useLarge = ((JNADatabase)getParentDatabase()).hasLargeItemSupport();
			
			if (!useLarge) {
			  DHANDLE.ByReference rethList = DHANDLE.newInstanceByReference();
			  ShortByReference retListSize = new ShortByReference();
			  Memory retpList = new Memory(Native.POINTER_SIZE);

			  short result = NotesCAPI.get().ListAllocate((short) 0, 
			      (short) 0,
			      1, rethList, retpList, retListSize);

			  NotesErrorUtils.checkResult(result);

			  return LockUtil.lockHandle(rethList, (hListByVal) -> {
			    Mem.OSUnlockObject(hListByVal);

			    int i = 0;
			    for(String currStr : strList) {
			      Memory currStrMem = NotesStringUtils.toLMBCS(currStr, false);
            if (currStrMem!=null && currStrMem.size() > 65535) {
              throw new DominoException(MessageFormat.format("List item at position {0} exceeds max lengths of 0xffff bytes", i));
            }

            char textSize = currStrMem==null ? 0 : (char) currStrMem.size();

			      short localResult = NotesCAPI.get().ListAddEntry(hListByVal, 1, retListSize, (char) (i & 0xffff), currStrMem,
			          textSize);
			      NotesErrorUtils.checkResult(localResult);
			      i++;
			    }

			    int listSize = retListSize.getValue() & 0xffff;
			    return appendItemValue(itemName, flags, hListByVal, listSize);
			  });
			}
			else {
			  INotesCAPI1201 capi1201 = NotesCAPI1201.get();

			  IntByReference rethList = new IntByReference();
        PointerByReference retpList = null;
			  IntByReference retListSize = new IntByReference();
			  
			  short result = capi1201.ListAllocate2Ext((short) 0,
			      0,
			      false,
			      rethList,
			      retpList,
			      retListSize,
			      true);
			  NotesErrorUtils.checkResult(result);

			  int hList = rethList.getValue();

			  if (hList==0) {
			    throw new DominoException("Method to create list returned a null handle");
			  }

			  try {
			    int i=0;
			    for (String currStr : strList) {
			      Memory currStrMem = NotesStringUtils.toLMBCS(currStr, false);
			      if (currStrMem!=null && currStrMem.size() > 65535) {
			        throw new DominoException(MessageFormat.format("List item at position {0} exceeds max lengths of 65535 bytes", i));
			      }
			      
            //somehow these two lines produce different results for the ListAddEntry2Ext call with text lengths >32767 bytes
			      //short textSize = (short) (currStrMem==null ? 0 : (currStrMem.size() & 0xffff));
            char textSize = currStrMem==null ? 0 : (char) currStrMem.size();
			      
			      short addResult = capi1201.ListAddEntry2Ext(hList,
			          false,
			          retListSize,
			          (char) i,
			          currStrMem,
			          textSize,
			          true);
			      NotesErrorUtils.checkResult(addResult);

			      i++;
			    }

			    int listSize = retListSize.getValue();

			    //copy list content into item and free memory
			    try (LockedMemory lockedMem = Mem.OSMemoryLock(hList, false)) {
			      Document retDoc = appendItemValue(itemName, flags, ItemDataType.TYPE_TEXT_LIST.getValue(), lockedMem.getPointer(), listSize);
			      return retDoc;
			    }
			  }
			  finally {
			    if (hList!=0) {
			      Mem.OSMemoryFree(hList);
			    }
			  }

			}
		}
		else if (value instanceof Iterable && isNumberOrNumberArrayList((Iterable) value)) {
		  List numberOrNumberArrList = toNumberOrNumberArrayList((Iterable) value);
			
			List numberList = new ArrayList<>();
			List numberArrList = new ArrayList<>();
			
			for (int i=0; i 65535) {
				throw new IllegalArgumentException(format("Number list size must fit in a WORD ({0}>65535)", numberList.size()));
			}

			if (numberArrList.size()> 65535) {
				throw new IllegalArgumentException(format("Number range list size must fit in a WORD ({0}>65535)", numberList.size()));
			}

			int valueSize = 2 + JNANotesConstants.rangeSize + 
					8 * numberList.size() +
					JNANotesConstants.numberPairSize * numberArrList.size();


			DHANDLE.ByReference rethItem = DHANDLE.newInstanceByReference();
			short result = Mem.OSMemAlloc((short) 0, valueSize, rethItem);
			NotesErrorUtils.checkResult(result);
			
			return LockUtil.lockHandle(rethItem, (hItemByVal) -> {
				Pointer valuePtr = Mem.OSLockObject(hItemByVal);
				
				try {
					valuePtr.setShort(0, ItemDataType.TYPE_NUMBER_RANGE.getValue().shortValue());
					valuePtr = valuePtr.share(2);
					
					Pointer rangePtr = valuePtr;
					NotesRangeStruct range = NotesRangeStruct.newInstance(rangePtr);
					range.ListEntries = (short) (numberList.size() & 0xffff);
					range.RangeEntries = (short) (numberArrList.size() & 0xffff);
					range.write();

					Pointer doubleListPtr = rangePtr.share(JNANotesConstants.rangeSize);
					
					for (int i=0; i) value)) {
			List dateOrDateTimeRangeList = toDateTimeOrDateTimeRangeList((Iterable) value);
			
			List dateTimeList = new ArrayList<>();
			List dateRangeList = new ArrayList<>();
			
			for (int i=0; i 65535) {
				throw new IllegalArgumentException(format("Date list size must fit in a WORD ({0}>65535)", dateTimeList.size()));
			}
			if (dateRangeList.size() > 65535) {
				throw new IllegalArgumentException(format("Date range list size must fit in a WORD ({0}>65535)", dateRangeList.size()));
			}

			int valueSize = 2 + JNANotesConstants.rangeSize + 
					8 * dateTimeList.size() +
					JNANotesConstants.timeDatePairSize * dateRangeList.size();
			

			DHANDLE.ByReference rethItem = DHANDLE.newInstanceByReference();
			short result = Mem.OSMemAlloc((short) 0, valueSize, rethItem);
			NotesErrorUtils.checkResult(result);
			
			return LockUtil.lockHandle(rethItem, (hItemByVal) -> {
				Pointer valuePtr = Mem.OSLockObject(hItemByVal);
				
				try {
					valuePtr.setShort(0, ItemDataType.TYPE_TIME_RANGE.getValue().shortValue());
					valuePtr = valuePtr.share(2);
					
					Pointer rangePtr = valuePtr;
					NotesRangeStruct range = NotesRangeStruct.newInstance(rangePtr);
					range.ListEntries = (short) (dateTimeList.size() & 0xffff);
					range.RangeEntries = (short) (dateRangeList.size() & 0xffff);
					range.write();

					Pointer dateListPtr = rangePtr.share(JNANotesConstants.rangeSize);
					
					for (DominoDateTime currDateTime : dateTimeList) {
						int[] innards = currDateTime.getAdapter(int[].class);

						dateListPtr.setInt(0, innards[0]);
						dateListPtr = dateListPtr.share(4);
						dateListPtr.setInt(0, innards[1]);
						dateListPtr = dateListPtr.share(4);
					}
					
					Pointer rangeListPtr = dateListPtr;
					
					for (int i=0; i {
				Pointer valuePtr = Mem.OSLockObject(hItemByVal);
				
				try {
					valuePtr.setShort(0, ItemDataType.TYPE_NOTEREF_LIST.getValue().shortValue());
					valuePtr = valuePtr.share(2);
					
					//LIST structure
					valuePtr.setShort(0, (short) 1);
					valuePtr = valuePtr.share(2);
					
					struct.write();
					valuePtr.write(0, struct.getAdapter(Pointer.class).getByteArray(0, 2*JNANotesConstants.timeDateSize), 0, 2*JNANotesConstants.timeDateSize);

					return appendItemValue(itemName, flags, hItemByVal, valueSize);
				}
				finally {
					Mem.OSUnlockObject(hItemByVal);
				}
			});
		}
		else if (value instanceof JNAFormula) {
			byte[] compiledFormula = ((JNAFormula)value).getAdapter(byte[].class);
			if (compiledFormula==null) {
				throw new IllegalArgumentException(format("Unable to read the data of the compiled formula: {0}", ((JNAFormula)value).getFormula()));
			}
			
			//date type + compiled formula
			int valueSize = 2 + compiledFormula.length;
			
			DHANDLE.ByReference rethItem = DHANDLE.newInstanceByReference();
			short result = Mem.OSMemAlloc((short) 0, valueSize, rethItem);
			NotesErrorUtils.checkResult(result);
			
			return LockUtil.lockHandle(rethItem, (hItemByVal) -> {
				Pointer valuePtr = Mem.OSLockObject(hItemByVal);
				
				try {
					valuePtr.setShort(0, ItemDataType.TYPE_FORMULA.getValue().shortValue());
					valuePtr = valuePtr.share(2);
					
					valuePtr.write(0, compiledFormula, 0, compiledFormula.length);

					return appendItemValue(itemName, flags, hItemByVal, valueSize);
				}
				finally {
					Mem.OSUnlockObject(hItemByVal);
				}
			
			});
		}
		else if (value instanceof DominoViewFormat) {
		  ByteBuffer viewFormatData = ViewFormatEncoder.encodeViewFormat((DominoViewFormat) value);
		  viewFormatData.position(0);
		  byte[] viewFormatDataArr = new byte[viewFormatData.capacity()];
		  viewFormatData.get(viewFormatDataArr);
		  
		  //date type + compiled formula
      int valueSize = 2 + viewFormatDataArr.length;
      
      DHANDLE.ByReference rethItem = DHANDLE.newInstanceByReference();
      short result = Mem.OSMemAlloc((short) 0, valueSize, rethItem);
      NotesErrorUtils.checkResult(result);
      
      return LockUtil.lockHandle(rethItem, (hItemByVal) -> {
        Pointer valuePtr = Mem.OSLockObject(hItemByVal);
        
        try {
          valuePtr.setShort(0, ItemDataType.TYPE_VIEW_FORMAT.getValue().shortValue());
          valuePtr = valuePtr.share(2);
          
          valuePtr.write(0, viewFormatDataArr, 0, viewFormatDataArr.length);

          return appendItemValue(itemName, flags, hItemByVal, valueSize);
        }
        finally {
          Mem.OSUnlockObject(hItemByVal);
        }
      
      });
		}
		else if (value instanceof DominoCalendarFormat) {
      ByteBuffer calendarFormatData = ViewFormatEncoder.encodeCalendarFormat((DominoCalendarFormat) value);
      calendarFormatData.position(0);
      byte[] calendarFormatDataArr = new byte[calendarFormatData.capacity()];
      calendarFormatData.get(calendarFormatDataArr);
      
      //date type + compiled formula
      int valueSize = 2 + calendarFormatDataArr.length;
      
      DHANDLE.ByReference rethItem = DHANDLE.newInstanceByReference();
      short result = Mem.OSMemAlloc((short) 0, valueSize, rethItem);
      NotesErrorUtils.checkResult(result);
      
      return LockUtil.lockHandle(rethItem, (hItemByVal) -> {
        Pointer valuePtr = Mem.OSLockObject(hItemByVal);
        
        try {
          valuePtr.setShort(0, ItemDataType.TYPE_CALENDAR_FORMAT.getValue().shortValue());
          valuePtr = valuePtr.share(2);
          
          valuePtr.write(0, calendarFormatDataArr, 0, calendarFormatDataArr.length);

          return appendItemValue(itemName, flags, hItemByVal, valueSize);
        }
        finally {
          Mem.OSUnlockObject(hItemByVal);
        }
      
      });
    }
    else if (value instanceof DominoCollationInfo) {
      ByteBuffer collationData = CollationEncoder.encode((DominoCollationInfo) value);
      collationData.position(0);
      byte[] collationDataArr = new byte[collationData.capacity()];
      collationData.get(collationDataArr);
      
      //date type + compiled formula
      int valueSize = 2 + collationDataArr.length;
      
      DHANDLE.ByReference rethItem = DHANDLE.newInstanceByReference();
      short result = Mem.OSMemAlloc((short) 0, valueSize, rethItem);
      NotesErrorUtils.checkResult(result);
      
      return LockUtil.lockHandle(rethItem, (hItemByVal) -> {
        Pointer valuePtr = Mem.OSLockObject(hItemByVal);
        
        try {
          valuePtr.setShort(0, ItemDataType.TYPE_COLLATION.getValue().shortValue());
          valuePtr = valuePtr.share(2);
          
          valuePtr.write(0, collationDataArr, 0, collationDataArr.length);

          return appendItemValue(itemName, flags, hItemByVal, valueSize);
        }
        finally {
          Mem.OSUnlockObject(hItemByVal);
        }
      
      });
    }
    else if(value instanceof UserData) {
      byte[] formatNameLmbcs = ((UserData)value).getFormatName().getBytes(LMBCSCharset.INSTANCE);
      if(formatNameLmbcs.length > 255) {
        throw new IllegalArgumentException("User data format name must be less than 255 bytes when encoded as LMBCS");
      }
      byte[] data = ((UserData)value).getData();
      if(data == null) {
        data = new byte[0];
      }
      
      //date type + byte Pascal length + name + data
      int valueSize = 2 + 1 + formatNameLmbcs.length + data.length;
      
      DHANDLE.ByReference rethItem = DHANDLE.newInstanceByReference();
      short result = Mem.OSMemAlloc((short) 0, valueSize, rethItem);
      NotesErrorUtils.checkResult(result);
      
      byte[] fData = data;
      return LockUtil.lockHandle(rethItem, (hItemByVal) -> {
        Pointer valuePtr = Mem.OSLockObject(hItemByVal);
        
        try {
          valuePtr.setShort(0, ItemDataType.TYPE_USERDATA.getValue().shortValue());
          valuePtr = valuePtr.share(2);
          
          // Pascal string format name
          valuePtr.setByte(0, (byte)formatNameLmbcs.length);
          valuePtr = valuePtr.share(1);
          valuePtr.write(0, formatNameLmbcs, 0, formatNameLmbcs.length);
          
          // Data array
          valuePtr = valuePtr.share(formatNameLmbcs.length);
          valuePtr.write(0, fData, 0, fData.length);

          return appendItemValue(itemName, flags, hItemByVal, valueSize);
        }
        finally {
          Mem.OSUnlockObject(hItemByVal);
        }
      });
    }
		else if (valueConverter!=null) {
			if (writingItemType.get().contains(valueConverter.getClass())) {
				throw new IllegalStateException(format("Infinite loop detected writing the value of item {0} as type {1}", itemName,
						value.getClass().getName()));
			}
			writingItemType.get().add(valueConverter.getClass());
			try {
			  if (valueConverter instanceof DocumentValueConverter) {
	        valueConverter.setValue(this, flags, itemName, value);
			  }
			  else {
	        valueConverter.setValue(this, itemName, value);
			  }
				return this;
			}
			finally {
				writingItemType.get().remove(valueConverter.getClass());
			}
		}
		else {
			throw new UnsupportedItemValueError(format("Unsupported value type: {0}", (value==null ? "null" : value.getClass().getName()))); //$NON-NLS-2$
		}
	}

	/**
	 * Internal method that calls the C API method to write the item with a handle to populate the BLOCKID structure.
	 * 
	 * @param itemName item name
	 * @param flags item flags
	 * @param hItemValue handle to memory block with item value beginning with data type short
	 * @param valueLength length of binary item value (without data type short)
	 * @return this document
	 */
	public Document appendItemValue(String itemName, Set flags, DHANDLE.ByValue hItemValue, int valueLength) {
		checkDisposed();

		Memory itemNameMem = NotesStringUtils.toLMBCS(itemName, false);
		
		short flagsShort = (short) (DominoEnumUtil.toBitField(ItemFlag.class, flags) & 0xffff);
		
		NotesBlockIdStruct.ByValue valueBlockIdByVal = NotesBlockIdStruct.ByValue.newInstance();
		if (PlatformUtils.is64Bit()) {
			valueBlockIdByVal.pool = (int) ((DHANDLE64.ByValue)hItemValue).hdl;
		}
		else {
			valueBlockIdByVal.pool = ((DHANDLE32.ByValue)hItemValue).hdl;
		}
		valueBlockIdByVal.block = 0;
		valueBlockIdByVal.write();
		
		NotesBlockIdStruct retItemBlockId = NotesBlockIdStruct.newInstance();
		retItemBlockId.pool = 0;
		retItemBlockId.block = 0;
		retItemBlockId.write();
		
		JNADocumentAllocations allocations = getAllocations();
		short result = LockUtil.lockHandle(allocations.getNoteHandle(), (noteHandleByVal) -> {
			return NotesCAPI.get().NSFItemAppendByBLOCKID(noteHandleByVal, flagsShort, itemNameMem,
					(short) (itemNameMem==null ? 0 : itemNameMem.size()), valueBlockIdByVal,
					valueLength, retItemBlockId);
		});
		NotesErrorUtils.checkResult(result);
		
		return this;
	}
	
	 /**
   * Internal method that calls the C API method to write the item with a pointer to the item value.
   * 
   * @param itemName item name
   * @param flags item flags
   * @param itemType item type
   * @param ptr value pointer without data type short
   * @param valueLength length of binary item value (without data type short)
   * @return this document
   */
  public Document appendItemValue(String itemName, Set flags, int itemType, Pointer ptr, int valueLength) {
    checkDisposed();

    Memory itemNameMem = NotesStringUtils.toLMBCS(itemName, false);
    
    short flagsShort = (short) (DominoEnumUtil.toBitField(ItemFlag.class, flags) & 0xffff);
    
    NotesBlockIdStruct retItemBlockId = NotesBlockIdStruct.newInstance();
    retItemBlockId.pool = 0;
    retItemBlockId.block = 0;
    retItemBlockId.write();
    
    JNADocumentAllocations allocations = getAllocations();
    short result = LockUtil.lockHandle(allocations.getNoteHandle(), (noteHandlyByVal) -> {
      return NotesCAPI.get().NSFItemAppend(
          noteHandlyByVal,
          flagsShort,
          itemNameMem,
          (short) (itemNameMem==null ? 0 : itemNameMem.size()),
          (short) (itemType & 0xffff),
          ptr,
          valueLength);
    });
    NotesErrorUtils.checkResult(result);
    
    return this;
  }
  
	@Override
	public RichTextWriter createRichTextItem(String itemName) {
		checkDisposed();
		JNADocumentAllocations allocations = getAllocations();
		
		JNARichtextWriter writer = new JNARichtextWriter(this, itemName);
		allocations.registerRichtextWriter(itemName, writer);
		
		return writer;
	}

	@Override
	public RichTextRecordList getRichTextItem(String itemName, RecordType.Area variant) {
		return new DefaultRichTextList(new JNARichtextNavigator(this, itemName), variant);
	}

	@Override
	public Attachment attachFile(String filePathOnDisk, String uniqueFileNameInNote, Compression compression) {
		checkDisposed();

		//make sure that the unique filename is really unique, since it will be used to return the NotesAttachment object
		JNAFormula formula = (JNAFormula) getParentDominoClient().createFormula("@AttachmentNames"); //$NON-NLS-1$
		List existingFileItems = formula.evaluate(this);
		formula.dispose();

		String reallyUniqueFileName = uniqueFileNameInNote;
		if (existingFileItems.contains(reallyUniqueFileName)) {
			String newFileName=reallyUniqueFileName;
			int idx = 1;
			while (existingFileItems.contains(reallyUniqueFileName)) {
				idx++;
				
				int iPos = reallyUniqueFileName.lastIndexOf('.');
				if (iPos==-1) {
					newFileName = reallyUniqueFileName+"_"+idx; //$NON-NLS-1$
				}
				else {
					newFileName = reallyUniqueFileName.substring(0, iPos)+"_"+idx+reallyUniqueFileName.substring(iPos); //$NON-NLS-1$
				}
				reallyUniqueFileName = newFileName;
			}
		}
		
		Memory $fileItemName = NotesStringUtils.toLMBCS("$FILE", true); //$NON-NLS-1$
		Memory filePathOnDiskMem = NotesStringUtils.toLMBCS(filePathOnDisk, true);
		Memory uniqueFileNameInNoteMem = NotesStringUtils.toLMBCS(reallyUniqueFileName, true);
		short compressionAsShort = (short) (compression.getValue() & 0xffff);
		
		short result = LockUtil.lockHandle(getAllocations().getNoteHandle(), (noteHandleByVal) -> {
			return NotesCAPI.get().NSFNoteAttachFile(noteHandleByVal, $fileItemName,
					(short) (($fileItemName.size()-1) & 0xffff), filePathOnDiskMem, uniqueFileNameInNoteMem, compressionAsShort);
		});
		NotesErrorUtils.checkResult(result);
		
		String fUniqueName = reallyUniqueFileName;
		return getAttachment(reallyUniqueFileName).orElseThrow(() -> new IllegalStateException(MessageFormat.format("Unable to locate newly-created attachment \"{0}\"", fUniqueName)));
	}

	@Override
	public Attachment attachFile(String uniqueFileNameInDoc,
			TemporalAccessor fileCreated,
			TemporalAccessor fileModified, IAttachmentProducer producer) {

		checkDisposed();

		//currently we do not support compression, because we could not find a Java OutputStream
		//implementation for Huffman that produced compatible result and no implementation at all
		//for LZ1 (tried LZW, but that did not work either)
		final Compression compression = Compression.NONE;
		
		//make sure that the unique filename is really unique, since it will be used to return the NotesAttachment object
		JNAFormula formulaObj = (JNAFormula) getParentDominoClient().createFormula("@AttachmentNames"); //$NON-NLS-1$
		List existingFileItems = formulaObj.evaluate(this);
		formulaObj.dispose();
		
		String reallyUniqueFileName = uniqueFileNameInDoc;
		if (existingFileItems.contains(reallyUniqueFileName)) {
			String newFileName=reallyUniqueFileName;
			int idx = 1;
			while (existingFileItems.contains(reallyUniqueFileName)) {
				idx++;

				int iPos = reallyUniqueFileName.lastIndexOf('.');
				if (iPos==-1) {
					newFileName = reallyUniqueFileName+"_"+idx; //$NON-NLS-1$
				}
				else {
					newFileName = reallyUniqueFileName.substring(0, iPos)+"_"+idx+reallyUniqueFileName.substring(iPos); //$NON-NLS-1$
				}
				reallyUniqueFileName = newFileName;
			}
		}
		
		Memory fileItemNameMem = NotesStringUtils.toLMBCS("$FILE", false); //$NON-NLS-1$
		Memory reallyUniqueFileNameMem = NotesStringUtils.toLMBCS(reallyUniqueFileName, false);
		
		JNADatabaseAllocations dbAllocations = (JNADatabaseAllocations) getParentDatabase().getAdapter(APIObjectAllocations.class);
		dbAllocations.checkDisposed();
		
		ObjectInfo objectInfo = JNADatabaseObjectProducer.createDbObject((JNADatabase) getParentDatabase(),
				 NotesConstants.NOTE_CLASS_DOCUMENT, NotesConstants.OBJECT_FILE, producer);
		
		LockUtil.lockHandles(dbAllocations.getDBHandle(), getAllocations().getNoteHandle(),
				(dbHandleByVal, noteHandleByVal) -> {

			//allocate memory for the $FILE item value:
			//datatype WORD + FILEOBJECT structure + unique filename
			int sizeOfFileObjectWithFileName = (int) (2 + JNANotesConstants.fileObjectSize + reallyUniqueFileNameMem.size());
			DHANDLE.ByReference retFileObjectWithFileNameHandle = DHANDLE.newInstanceByReference();;
			short result = Mem.OSMemAlloc((short) 0, sizeOfFileObjectWithFileName, retFileObjectWithFileNameHandle);
			NotesErrorUtils.checkResult(result);
			
			//produce FILEOBJECT data structure
			LockUtil.lockHandle(retFileObjectWithFileNameHandle, (retFileObjectWithFileNameHandleByVal) -> {
				Pointer ptrFileObjectWithDatatype = Mem.OSLockObject(retFileObjectWithFileNameHandleByVal);
				try {
					//write datatype WORD
					ptrFileObjectWithDatatype.setShort(0, (short) (ItemDataType.TYPE_OBJECT.getValue() & 0xffff));
					NotesFileObjectStruct fileObjectStruct = NotesFileObjectStruct.newInstance(ptrFileObjectWithDatatype.share(2));
					fileObjectStruct.CompressionType = (short) (compression.getValue() & 0xffff);
					fileObjectStruct.FileAttributes = 0;
					
					JNADominoDateTime fileCreatedDt = JNADominoDateTime.from(fileCreated);
					fileObjectStruct.FileCreated = NotesTimeDateStruct.newInstance(fileCreatedDt.getAdapter(int[].class));
					JNADominoDateTime fileModifiedDt = JNADominoDateTime.from(fileModified);
					fileObjectStruct.FileModified = NotesTimeDateStruct.newInstance(fileModifiedDt.getAdapter(int[].class));
					fileObjectStruct.FileNameLength = (short) (reallyUniqueFileNameMem.size() & 0xffff);
					fileObjectStruct.FileSize = (int) (objectInfo.getObjectSize() & 0xffffffff);
					fileObjectStruct.Flags = 0;
					fileObjectStruct.Header.RRV = objectInfo.getObjectId();
					fileObjectStruct.Header.ObjectType = NotesConstants.OBJECT_FILE;
					
					fileObjectStruct.write();
					
					//append unique filename
					ptrFileObjectWithDatatype.share(2 + JNANotesConstants.fileObjectSize).write(0, reallyUniqueFileNameMem.getByteArray(0, (int) reallyUniqueFileNameMem.size()), 0, (int) reallyUniqueFileNameMem.size());
				}
				finally {
					Mem.OSUnlockObject(retFileObjectWithFileNameHandleByVal);
				}

				NotesBlockIdStruct.ByValue bhValue = NotesBlockIdStruct.ByValue.newInstance();
				if (retFileObjectWithFileNameHandleByVal instanceof DHANDLE64) {
					bhValue.pool = (int) ((DHANDLE64)retFileObjectWithFileNameHandleByVal).hdl;
				}
				else if (retFileObjectWithFileNameHandleByVal instanceof DHANDLE32) {
					bhValue.pool = ((DHANDLE32)retFileObjectWithFileNameHandleByVal).hdl;
				}

				int fDealloc = 1;
				//transfers ownership of the item value buffer to the note
				short itemAppendResult = NotesCAPI.get().NSFItemAppendObject(noteHandleByVal,
						NotesConstants.ITEM_SUMMARY,
						fileItemNameMem,
						(short) (fileItemNameMem.size() & 0xffff),
						bhValue,
						sizeOfFileObjectWithFileName,
						fDealloc);
				NotesErrorUtils.checkResult(itemAppendResult);

				return null;
			});
			
			return null;
		});

		//load and return created attachment
		String fUniqueName = reallyUniqueFileName;
		Attachment att = getAttachment(reallyUniqueFileName).orElseThrow(() -> new IllegalStateException(MessageFormat.format("Unable to locate newly-created attachment \"{0}\"", fUniqueName)));
		return att;
	
	}

	@Override
	public Document removeAttachment(String uniqueFileNameInDoc) {
		getAttachment(uniqueFileNameInDoc).ifPresent(Attachment::deleteFromDocument);
		return this;
	}

	@Override
	public Document makeResponse(Document doc) {
		return makeResponse(doc.getUNID());
	}

	@Override
	public Document makeResponse(String unid) {
		replaceItemValue("$REF", EnumSet.of(ItemFlag.SUMMARY), new JNADominoUniversalNoteId(unid)); //$NON-NLS-1$
		return this;
	}

	@Override
	public Document sign() {
		checkDisposed();

		Set docClass = getDocumentClass();

		LockUtil.lockHandle(getAllocations().getNoteHandle(), (hNoteByVal) -> {
			short result;
			boolean expandNote = false;
			if (docClass.contains(DocumentClass.FORM) || docClass.contains(DocumentClass.INFO) ||
					docClass.contains(DocumentClass.HELP) || docClass.contains(DocumentClass.FIELD)) {
				expandNote = true;
			}

			if (expandNote) {
				result = NotesCAPI.get().NSFNoteExpand(hNoteByVal);
				NotesErrorUtils.checkResult(result);
			}

			result = NotesCAPI.get().NSFNoteSign(hNoteByVal);
			NotesErrorUtils.checkResult(result);

			if (expandNote) {
				result = NotesCAPI.get().NSFNoteContract(hNoteByVal);
				NotesErrorUtils.checkResult(result);
			}

			return 0;
		});
		return this;
	}

	@Override
	public Document sign(UserId id, boolean signNotesIfMimePresent) {
		checkDisposed();
		
		LockUtil.lockHandle(getAllocations().getNoteHandle(), (hNoteByVal) -> {
			short result = NotesCAPI.get().NSFNoteExpand(hNoteByVal);
			NotesErrorUtils.checkResult(result);

			short signResult;
			if (id==null) {
				signResult = NotesCAPI.get().NSFNoteSignExt3(hNoteByVal, (Pointer) null, null,
						NotesConstants.MAXWORD, (DHANDLE.ByReference) null,
						signNotesIfMimePresent ? NotesConstants.SIGN_NOTES_IF_MIME_PRESENT : 0, 0, (Pointer) null);
			}
			else {
				signResult = JNADominoUtils.accessKFC(id, phKFC ->
					NotesCAPI.get().NSFNoteSignExt3(hNoteByVal, phKFC.getValue(), null,
						NotesConstants.MAXWORD, (DHANDLE.ByReference) null,
						signNotesIfMimePresent ? NotesConstants.SIGN_NOTES_IF_MIME_PRESENT : 0, 0, (Pointer) null)
				);
			}
			NotesErrorUtils.checkResult(signResult);

			result = NotesCAPI.get().NSFNoteContract(hNoteByVal);
			NotesErrorUtils.checkResult(result);
			
			//verify signature
			NotesTimeDateStruct retWhenSigned = NotesTimeDateStruct.newInstance();
			try(
			    DisposableMemory retSigner = new DisposableMemory(NotesConstants.MAXUSERNAME);
			    DisposableMemory retCertifier = new DisposableMemory(NotesConstants.MAXUSERNAME);
			) {
    			result = NotesCAPI.get().NSFNoteVerifySignature (hNoteByVal, null, retWhenSigned, retSigner, retCertifier);
    			NotesErrorUtils.checkResult(result);
			}
			
			return 0;
		});
		
		return this;
	}

	@Override
	public Document unsign() {
		checkDisposed();
		
		short result = LockUtil.lockHandle(getAllocations().getNoteHandle(), (hNoteByVal) -> {
			return NotesCAPI.get().NSFNoteUnsign(hNoteByVal);
		});
		NotesErrorUtils.checkResult(result);
		return this;
	}

	@Override
	public Document copyAndEncrypt(UserId id, Collection encryptionMode) {
		checkDisposed();
		
		int flags = 0;
		for (EncryptionMode currMode : encryptionMode) {
			flags = flags | currMode.getMode();
		}
		
		short flagsShort = (short) (flags & 0xffff);
		
		short result;

		DHANDLE.ByReference rethDstNote = DHANDLE.newInstanceByReference();

		result = LockUtil.lockHandle(getAllocations().getNoteHandle(), (hNoteByVal) -> {
			if (id!=null) {
				return JNADominoUtils.accessKFC(id, phKFC ->
					NotesCAPI.get().NSFNoteCopyAndEncryptExt2(hNoteByVal, phKFC.getValue(), flagsShort,
						rethDstNote, 0, null)
				);
			}
			else {
				return NotesCAPI.get().NSFNoteCopyAndEncryptExt2(hNoteByVal, null, flagsShort,
						rethDstNote, 0, null);
			}
		});
		NotesErrorUtils.checkResult(result);
		
		return new JNADocument((JNADatabase) getParentDatabase(), rethDstNote);
	}

	@Override
	public Document setUNID(String newUNID) {
		checkDisposed();
		JNADocumentAllocations allocations = getAllocations();
		
		try(DisposableMemory retOid = new DisposableMemory(JNANotesConstants.oidSize)) {
			retOid.clear();

			LockUtil.lockHandle(allocations.getNoteHandle(), (handleByVal) -> {
				NotesCAPI.get().NSFNoteGetInfo(handleByVal, NotesConstants._NOTE_OID, retOid);
				
				NotesOriginatorIdStruct oidStruct = NotesOriginatorIdStruct.newInstance(retOid);
				oidStruct.read();
				oidStruct.setUNID(newUNID);
				
				NotesCAPI.get().NSFNoteSetInfo(handleByVal, NotesConstants._NOTE_OID, retOid);
				
				return null;
			});
		}
		return this;
	}

	@Override
	public Document copyToDatabase(Database otherDb) {
		checkDisposed();
		JNADocumentAllocations allocations = getAllocations();
		
		if (!(otherDb instanceof JNADatabase)) {
			throw new DominoException("Unsupported target database type, not a JNADatabase");
		}
		JNADatabase otherJNADb = (JNADatabase) otherDb;
		JNADatabaseAllocations otherJNADbAllocations = (JNADatabaseAllocations) otherJNADb.getAdapter(APIObjectAllocations.class);
		otherJNADbAllocations.checkDisposed();
		
		DHANDLE.ByReference newNoteHandle = DHANDLE.newInstanceByReference();
		
		short result = LockUtil.lockHandle(allocations.getNoteHandle(), (noteHandleByVal) -> {
			return NotesCAPI.get().NSFNoteCopy(noteHandleByVal, newNoteHandle);
		});
		NotesErrorUtils.checkResult(result);
		
		NotesOriginatorIdStruct newOID = otherJNADb.generateOIDStruct();
		
		LockUtil.lockHandles(
				otherJNADbAllocations.getDBHandle(),
				newNoteHandle, (otherDbHandleByVal, newNoteHandleByVal) -> {
					NotesCAPI.get().NSFNoteSetInfo(newNoteHandleByVal, NotesConstants._NOTE_ID, null);
					
					NotesCAPI.get().NSFNoteSetInfo(newNoteHandleByVal, NotesConstants._NOTE_OID, newOID.getPointer());

					HANDLE.ByReference targetDbHdlByReference = HANDLE.newInstanceByReference(otherDbHandleByVal);
					
					NotesCAPI.get().NSFNoteSetInfo(newNoteHandleByVal, NotesConstants._NOTE_DB,
							((Structure) targetDbHdlByReference).getPointer());
					
					return null;
				});
		
		return new JNADocument(otherJNADb, newNoteHandle);
	}

	@Override
	public Document decrypt(UserId id) {
		checkDisposed();
		
		short decryptFlags = NotesConstants.DECRYPT_ATTACHMENTS_IN_PLACE;
		
		LockUtil.lockHandle(getAllocations().getNoteHandle(), (hNoteByVal) -> {
			short decryptResult;
			
			if (id!=null) {
				decryptResult = JNADominoUtils.accessKFC(id, phKFC ->
					NotesCAPI.get().NSFNoteCipherDecrypt(hNoteByVal, phKFC.getValue(), decryptFlags,
						null, 0, null)
				);
			}
			else {
				decryptResult = NotesCAPI.get().NSFNoteCipherDecrypt(hNoteByVal, null, decryptFlags,
						null, 0, null);
			}
			NotesErrorUtils.checkResult(decryptResult);
			return 0;
		});
		return this;
	}

	@Override
	public void delete() {
		if (checkForProfileAndDelete()) {
			return;
		}
		
		checkDisposed();
		int noteId = getNoteID();
		close();
		((JNADatabase)getParent()).deleteDocument(noteId);
	}

	@Override
	public void delete(boolean noStub) {
		if (checkForProfileAndDelete()) {
			return;
		}

        int noteId = getNoteID();
        close();
		((JNADatabase)getParent()).deleteDocument(noteId, noStub ? EnumSet.of(UpdateNote.NOSTUB) : EnumSet.noneOf(UpdateNote.class));
	}
	
	@Override
	public Document undelete() {
		checkDisposed();
		JNADocumentAllocations allocations = getAllocations();
		
		Set updateFlags = EnumSet.of(UpdateNote.RESTORE_SOFT_DELETED);
		int updateFlagsBitmask = DominoEnumUtil.toBitField(UpdateNote.class, updateFlags);
		
		allocations.closeAllRichtextWriters();
		
		short result = LockUtil.lockHandle(allocations.getNoteHandle(), (handleByVal) -> {
			return NotesCAPI.get().NSFNoteUpdateExtended(handleByVal, updateFlagsBitmask);
		});
		
		NotesErrorUtils.checkResult(result);
		return this;
	}
	
	/**
	 * Checks if this note is a profile. If it is, we use a different C method
	 * to delete it in the database and also delete the profile cache entry.
	 * 
	 * @return true if note is a profile note that has been saved, false otherwise
	 */
	private boolean checkForProfileAndDelete() {
		checkDisposed();
		
		if (!isHiddenFromViews()) {
			return false;
		}
		
		String[] profileNameAndUsername = parseProfileAndUserName();
		
		if (profileNameAndUsername==null) {
			return false;
		}
		
		String profileName = profileNameAndUsername[0];
		String profileUsername = profileNameAndUsername[1];
		
		Memory profileNameMem = NotesStringUtils.toLMBCS(profileName, false);
		Memory profileUsernameMem = StringUtil.isEmpty(profileUsername) ? null : NotesStringUtils.toLMBCS(profileUsername, false);
		
		JNADatabase parentDb = (JNADatabase) getParentDatabase();
		if (parentDb.isDisposed()) {
			throw new ObjectDisposedException(parentDb);
		}
		JNADatabaseAllocations parentDbAllocations = (JNADatabaseAllocations) parentDb.getAdapter(APIObjectAllocations.class);
		
		JNADocumentAllocations docAllocations = getAllocations();
		
		short result = LockUtil.lockHandles(
				parentDbAllocations.getDBHandle(), docAllocations.getNoteHandle(),
				(dbHandleByVal, noteHandleByVal) -> {
					//delete note and remove from profile cache
					return NotesCAPI.get().NSFProfileDelete(dbHandleByVal,
							profileNameMem, (short) (profileNameMem.size() & 0xffff), profileUsernameMem, (short) ((profileUsernameMem==null ? 0 : profileUsernameMem.size()) & 0xffff));
				});
		NotesErrorUtils.checkResult(result);
		
		return true;
	}

	
	@Override
	public Document save() {
		return save(false);
	}

	@Override
	public Document save(boolean force) {
		if (force) {
			return save(EnumSet.of(UpdateNote.FORCE));
		}
		else {
			return save(EnumSet.noneOf(UpdateNote.class));
		}
	}

	Document save(Set updateFlags) {
		if (checkForProfileAndSave()) {
			return this;
		}
		
		checkDisposed();
		JNADocumentAllocations allocations = getAllocations();
		
		int updateFlagsBitmask = DominoEnumUtil.toBitField(UpdateNote.class, updateFlags);
		
		allocations.closeAllRichtextWriters();
		
		short result = LockUtil.lockHandle(allocations.getNoteHandle(), (handleByVal) -> {
			return NotesCAPI.get().NSFNoteUpdateExtended(handleByVal, updateFlagsBitmask);
		});
		
		NotesErrorUtils.checkResult(result);
		return this;
	}
	
	/**
	 * Checks if this note is a profile. If it is, we use a different C method
	 * to update it in the database and also update the profile cache.
	 * 
	 * @return true if note is a profile note that has been saved, false otherwise
	 */
	private boolean checkForProfileAndSave() {
		checkDisposed();
		
		if (!isHiddenFromViews()) {
			return false;
		}
		
		String[] profileNameAndUsername = parseProfileAndUserName();
		
		if (profileNameAndUsername==null) {
			return false;
		}
		String profileName = profileNameAndUsername[0];
		String profileUsername = profileNameAndUsername[1];
		
		parseProfileAndUserName();
		
		Memory profileNameMem = NotesStringUtils.toLMBCS(profileName, false);
		Memory userNameMem = StringUtil.isEmpty(profileUsername) ? null : NotesStringUtils.toLMBCS(profileUsername, false);

		JNADocumentAllocations allocations = getAllocations();
		
		short result = LockUtil.lockHandle(allocations.getNoteHandle(), (noteHandleByVal) -> {
			return  NotesCAPI.get().NSFProfileUpdate(noteHandleByVal,
					profileNameMem, (short) (profileNameMem.size() & 0xffff), userNameMem,
					(short) (userNameMem==null ? 0 : (userNameMem.size() & 0xffff)));
		});
		NotesErrorUtils.checkResult(result);
		
		return true;
 	}

	@Override
	public List getLockHolders() {
		checkDisposed();

		if (isNew()) {
			return Collections.emptyList();
		}
		
		int lockFlags = NotesConstants.NOTE_LOCK_STATUS;
		
		JNADatabaseAllocations dbAllocations = (JNADatabaseAllocations) getParentDatabase().getAdapter(APIObjectAllocations.class);
		dbAllocations.checkDisposed();

		return LockUtil.lockHandle(dbAllocations.getDBHandle(),
				(dbHandleByVal) -> {

					DHANDLE.ByReference rethLockers = DHANDLE.newInstanceByReference();
					IntByReference retLength = new IntByReference();

					short result = NotesCAPI.get().NSFDbNoteLock(dbHandleByVal, getNoteID(), lockFlags,
							null, rethLockers, retLength);
					NotesErrorUtils.checkResult(result);

					if (rethLockers.isNull()) {
						return Collections.emptyList();
					}

					return LockUtil.lockHandle(rethLockers, (rethLockersByVal) -> {
						Pointer retLockersPtr = Mem.OSLockObject(rethLockersByVal);
						try {
							String retLockHoldersConc = NotesStringUtils.fromLMBCS(retLockersPtr, retLength.getValue());
							if (StringUtil.isEmpty(retLockHoldersConc)) {
								return Collections.emptyList();
							}

							String[] retLockHoldersArr = retLockHoldersConc.split(";"); //$NON-NLS-1$
							return Arrays.asList(retLockHoldersArr);
						}
						finally {
							Mem.OSUnlockObject(rethLockersByVal);
						}
					});
				});
	}

	@Override
	public Set getDocumentClass() {
		if (m_documentClass==null) {
			checkDisposed();
			
			try(DisposableMemory retNoteClass = new DisposableMemory(2)) {
    			retNoteClass.clear();
    			
    			JNADocumentAllocations allocations = getAllocations();
    			m_documentClass = LockUtil.lockHandle(allocations.getNoteHandle(), (noteHandleByVal) -> {
    				NotesCAPI.get().NSFNoteGetInfo(noteHandleByVal, NotesConstants._NOTE_CLASS, retNoteClass);
    				int noteClassMask = retNoteClass.getShort(0);
    
    				Set docClass = EnumSet.noneOf(DocumentClass.class);
    				
    				if (noteClassMask==0) {
    					docClass.add(DocumentClass.NONE);
    				}
    				else {
    					for (DocumentClass currClass : DocumentClass.values()) {
    						if (currClass.getValue()!=0) {
    							if ((noteClassMask & currClass.getValue()) == currClass.getValue()) {
    								docClass.add(currClass);
    							}
    						}
    					}
    				}
    				
    				return docClass;
    			});
			}
		}
		return m_documentClass;
	}

	 @Override
	 public Document setDocumentClass(DocumentClass docClass) {
	   return setDocumentClass(EnumSet.of(docClass));
	 }

	@Override
	public Document setDocumentClass(Collection docClass) {
		checkDisposed();
		
		m_documentClass = null;
		Short docClassVal = DominoEnumUtil.toBitField(DocumentClass.class, docClass);
		
		JNADocumentAllocations allocations = getAllocations();
		LockUtil.lockHandle(allocations.getNoteHandle(), (noteHandleByVal) -> {
			ShortByReference noteClassVal = new ShortByReference();
			noteClassVal.setValue(docClassVal.shortValue());
			NotesCAPI.get().NSFNoteSetInfo(noteHandleByVal, NotesConstants._NOTE_CLASS, noteClassVal.getPointer());
			return null;
		});
		
		return this;
	}

	@Override
	public boolean lock(String lockHolder, LockMode mode) {
		return lock(Arrays.asList(lockHolder), mode);
	}

	@Override
	public boolean lock(List lockHolders, LockMode mode) {
		checkDisposed();

		if (isNew()) {
			throw new DominoException("Note must be saved before locking");
		}
		
		int lockFlags = 0;
		if (mode==LockMode.Hard || mode==LockMode.HardOrProvisional) {
			lockFlags = NotesConstants.NOTE_LOCK_HARD;
		}
		else if (mode==LockMode.Provisional) {
			lockFlags = NotesConstants.NOTE_LOCK_PROVISIONAL;
		} else {
			throw new IllegalArgumentException("Missing lock mode");
		}
		
		int fLockFlags = lockFlags;
		
		String lockHoldersConc = StringUtil.join(lockHolders, ";"); //$NON-NLS-1$
		Memory lockHoldersMem = NotesStringUtils.toLMBCS(lockHoldersConc, true);
		
		JNADatabaseAllocations dbAllocations = (JNADatabaseAllocations) getParentDatabase().getAdapter(APIObjectAllocations.class);
		dbAllocations.checkDisposed();

		short result = LockUtil.lockHandle(dbAllocations.getDBHandle(), (dbHandleByVal) -> {
			DHANDLE.ByReference rethLockers = DHANDLE.newInstanceByReference();
			IntByReference retLength = new IntByReference();
			
			short lockResult = NotesCAPI.get().NSFDbNoteLock(dbHandleByVal, getNoteID(), fLockFlags,
					lockHoldersMem, rethLockers, retLength);
			
			if (lockResult==1463) { //Unable to connect to Master Lock Database
				if (mode==LockMode.HardOrProvisional) {
					lockResult = NotesCAPI.get().NSFDbNoteLock(dbHandleByVal, getNoteID(), NotesConstants.NOTE_LOCK_PROVISIONAL,
							lockHoldersMem, rethLockers, retLength);
				}
			}
		
			return lockResult;
		});
		
		if (result == INotesErrorConstants.ERR_NOTE_LOCKED) {
			return false;
		}
		NotesErrorUtils.checkResult(result);

		return true;
	}

	@Override
	public Document unlock(LockMode mode) {
		checkDisposed();
		
		if (isNew()) {
			return this;
		}
		
		int lockFlags = 0;
		if (mode==LockMode.Hard || mode==LockMode.HardOrProvisional) {
			lockFlags = NotesConstants.NOTE_LOCK_HARD;
		}
		else if (mode==LockMode.Provisional) {
			lockFlags = NotesConstants.NOTE_LOCK_PROVISIONAL;
		} else {
			throw new IllegalArgumentException("Missing lock mode");
		}
		
		final int fLockFlags = lockFlags;
		
		JNADatabaseAllocations dbAllocations = (JNADatabaseAllocations) getParentDatabase().getAdapter(APIObjectAllocations.class);
		dbAllocations.checkDisposed();
		
		short result = LockUtil.lockHandle(dbAllocations.getDBHandle(), (dbHandleByVal) -> {
			short lockResult = NotesCAPI.get().NSFDbNoteUnlock(dbHandleByVal, getNoteID(), fLockFlags);

			if (lockResult==1463) { //Unable to connect to Master Lock Database
				if (mode==LockMode.HardOrProvisional) {
					lockResult = NotesCAPI.get().NSFDbNoteUnlock(dbHandleByVal, getNoteID(), NotesConstants.NOTE_LOCK_PROVISIONAL);
				}
			}
			
			return lockResult;
		});
	
		NotesErrorUtils.checkResult(result);
		return this;
	}

	/**
	 * Reads the note flags (e.g. {@link NotesConstants#NOTE_FLAG_READONLY})
	 * 
	 * @return flags
	 */
	private short getFlags() {
		checkDisposed();
		
		JNADocumentAllocations allocations = getAllocations();
		return LockUtil.lockHandle(allocations.getNoteHandle(), (noteHandleByVal) -> {
		    try(DisposableMemory retFlags = new DisposableMemory(2)) {
    			retFlags.clear();
    			
    			NotesCAPI.get().NSFNoteGetInfo(noteHandleByVal, NotesConstants._NOTE_FLAGS, retFlags);
    			short flags = retFlags.getShort(0);
    			return flags;
		    }
		});
	}
	
	/**
	 * Reads the note flags2 (e.g. {@link NotesConstants#NOTE_FLAG2_SOFT_DELETED})
	 * 
	 * @return flags
	 */
	private short getFlags2() {
		checkDisposed();
		
		JNADocumentAllocations allocations = getAllocations();
		return LockUtil.lockHandle(allocations.getNoteHandle(), (noteHandleByVal) -> {
		    try(DisposableMemory retFlags = new DisposableMemory(2)) {
    			retFlags.clear();
    			
    			NotesCAPI.get().NSFNoteGetInfo(noteHandleByVal, NotesConstants._NOTE_FLAGS2, retFlags);
    			short flags = retFlags.getShort(0);
    			return flags;
		    }
		});
	}
	
	private void setFlags(short flags) {
		checkDisposed();

		try(DisposableMemory flagsMem = new DisposableMemory(2)) {
			flagsMem.setShort(0, flags);

			LockUtil.lockHandle(getAllocations().getNoteHandle(), (noteHandleByVal) -> {
				NotesCAPI.get().NSFNoteSetInfo(noteHandleByVal, NotesConstants._NOTE_FLAGS, flagsMem);
				return 0;
			});
		}
	}
	
	@Override
	public boolean isEditable() {
		int flags = getFlags();
		return (flags & NotesConstants.NOTE_FLAG_READONLY) != NotesConstants.NOTE_FLAG_READONLY;
	}

	@Override
	public boolean hasReadersField() {
		checkDisposed();
		
		JNADocumentAllocations allocations = getAllocations();
		
		NotesBlockIdStruct blockId = NotesBlockIdStruct.newInstance();

		return LockUtil.lockHandle(allocations.getNoteHandle(), (noteHandleByVal) -> {
			return NotesCAPI.get().NSFNoteHasReadersField(noteHandleByVal, blockId) == 1;
		});
	}

	@Override
	public List getReadersFields() {
		checkDisposed();
		
		JNADocumentAllocations allocations = getAllocations();
		
		NotesBlockIdStruct blockId = NotesBlockIdStruct.newInstance();

		boolean hasReaders = LockUtil.lockHandle(allocations.getNoteHandle(), (noteHandleByVal) -> {
			return NotesCAPI.get().NSFNoteHasReadersField(noteHandleByVal, blockId) == 1;
		});
		
		if (!hasReaders) {
			return Collections.emptyList();
		}
		
		List readerFields = new ArrayList<>();
		
		NotesBlockIdStruct.ByValue itemBlockIdByVal = NotesBlockIdStruct.ByValue.newInstance();
		itemBlockIdByVal.pool = blockId.pool;
		itemBlockIdByVal.block = blockId.block;
		
		ByteByReference retSeqByte = new ByteByReference();
		ByteByReference retDupItemID = new ByteByReference();

		try(DisposableMemory item_name = new DisposableMemory(NotesConstants.MAXUSERNAME)) {
    		ShortByReference retName_len = new ShortByReference();
    		ShortByReference retItem_flags = new ShortByReference();
    		ShortByReference retDataType = new ShortByReference();
    		IntByReference retValueLen = new IntByReference();
    
    		NotesBlockIdStruct retValueBid = NotesBlockIdStruct.newInstance();
    		
    		LockUtil.lockHandle(allocations.getNoteHandle(), (noteHandleByVal) -> {
    
    			NotesCAPI.get().NSFItemQueryEx(noteHandleByVal,
    					itemBlockIdByVal, item_name, (short) (item_name.size() & 0xffff), retName_len,
    					retItem_flags, retDataType, retValueBid, retValueLen, retSeqByte, retDupItemID);
    	
    			NotesBlockIdStruct itemBlockIdForItemCreation = NotesBlockIdStruct.newInstance();
    			itemBlockIdForItemCreation.pool = itemBlockIdByVal.pool;
    			itemBlockIdForItemCreation.block = itemBlockIdByVal.block;
    			itemBlockIdForItemCreation.write();
    			
    			if ((retItem_flags.getValue() & NotesConstants.ITEM_READERS) == NotesConstants.ITEM_READERS) {
    				JNAItem firstItem = new JNAItem(this, itemBlockIdForItemCreation, retDataType.getValue() & 0xffff,
    						retValueBid);
    				readerFields.add(firstItem);
    			}
    	
    			short result;
    
    			//now search for more items with readers flag
    			while (true) {
    				IntByReference retNextValueLen = new IntByReference();
    				
    				NotesBlockIdStruct retItemBlockId = NotesBlockIdStruct.newInstance();
    				
    				result = NotesCAPI.get().NSFItemInfoNext(noteHandleByVal, itemBlockIdByVal,
    						null, (short) 0, retItemBlockId, retDataType,
    						retValueBid, retNextValueLen);
    				
    				if (result == INotesErrorConstants.ERR_ITEM_NOT_FOUND) {
    					return readerFields;
    				}
    	
    				NotesErrorUtils.checkResult(result);
    	
    				itemBlockIdForItemCreation = NotesBlockIdStruct.newInstance();
    				itemBlockIdForItemCreation.pool = retItemBlockId.pool;
    				itemBlockIdForItemCreation.block = retItemBlockId.block;
    				itemBlockIdForItemCreation.write();
    				
    				NotesBlockIdStruct valueBlockIdClone = NotesBlockIdStruct.newInstance();
    				valueBlockIdClone.pool = retValueBid.pool;
    				valueBlockIdClone.block = retValueBid.block;
    				valueBlockIdClone.write();
    				
    				short dataType = retDataType.getValue();
    	
    				JNAItem newItem = new JNAItem(this, itemBlockIdForItemCreation, dataType,
    						valueBlockIdClone);
    				if (newItem.isReaders()) {
    					readerFields.add(newItem);
    				}
    				
    				itemBlockIdByVal.pool = retItemBlockId.pool;
    				itemBlockIdByVal.block = retItemBlockId.block;
    				itemBlockIdByVal.write();
    			}
    		});
		}
		
		return readerFields;
	}
	
	@Override
	public int getResponseCount() {
		checkDisposed();
		
		JNADocumentAllocations allocations = getAllocations();
		
		try(DisposableMemory retResponseCount = new DisposableMemory(4)) {
			retResponseCount.clear();

			return LockUtil.lockHandle(allocations.getNoteHandle(), (handleByVal) -> {
				NotesCAPI.get().NSFNoteGetInfo(handleByVal, NotesConstants._NOTE_RESPONSE_COUNT, retResponseCount);
				return retResponseCount.getInt(0);
			});
		}
	}
	
	@Override
	public IDTable getResponses() {
		checkDisposed();
		
		JNADocumentAllocations allocations = getAllocations();

		DHANDLE dhandle = LockUtil.lockHandle(allocations.getNoteHandle(), (handleByVal) -> {
			DHANDLE ret = DHANDLE.newInstanceByValue();
			NotesCAPI.get().NSFNoteGetInfo(handleByVal, NotesConstants._NOTE_RESPONSES, ret.getAdapter(Pointer.class));
			ret.getAdapter(Structure.class).read();
			return ret;
		});
		
		if(dhandle.isNull()) {
			return getParentDominoClient().createIDTable();
		} else {
			JNAIDTable responseTable = new JNAIDTable(getParentDominoClient(), dhandle, true);
			//create a copy of the table because the original instance will get freed via NSFNoteClose
			//and we don't want users to store and access this id table after document disposal
			JNAIDTable responseTableCopy = (JNAIDTable) responseTable.clone();
			return responseTableCopy;
		}
	}

	@Override
	public boolean hasMIME() {
		checkDisposed();
		
		return LockUtil.lockHandle(getAllocations().getNoteHandle(), (noteHandleByVal) -> {
			return NotesCAPI.get().NSFNoteHasMIME(noteHandleByVal) == 1;
		});
	}

	@Override
	public boolean hasMIMEPart() {
		checkDisposed();
		
		return LockUtil.lockHandle(getAllocations().getNoteHandle(), (noteHandleByVal) -> {
			return NotesCAPI.get().NSFNoteHasMIMEPart(noteHandleByVal) == 1;
		});
	}

	@Override
	public boolean hasComposite() {
		checkDisposed();
		
		return LockUtil.lockHandle(getAllocations().getNoteHandle(), (noteHandleByVal) -> {
			return NotesCAPI.get().NSFNoteHasComposite(noteHandleByVal) == 1;
		});
	}

	@Override
	public boolean hasItem(String itemName) {
		checkDisposed();
		
		Memory itemNameMem = NotesStringUtils.toLMBCS(itemName, false);

		JNADocumentAllocations allocations = getAllocations();
		short result = LockUtil.lockHandle(allocations.getNoteHandle(), (noteHandleByVal) -> {
			short name_len = itemNameMem == null ? 0 : (short)(itemNameMem.size() & 0xffff);
			return NotesCAPI.get().NSFItemInfo(noteHandleByVal, itemNameMem, name_len,
					null, null, null, null);
			
		});
		return result == 0;	
	}

	@Override
	public List getItemNames() {
		Set itemNames = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
		getItems("", item -> { //$NON-NLS-1$
			itemNames.add(item.getName());
			return IItemCallback.Action.Continue;
		});
		
		return new ArrayList<>(itemNames);
	}
	
	@Override
	public String getSigner() {
		try {
			SignatureData signatureData = verifySignature();
			
			return signatureData.getSigner();
		}
		catch (DominoException e) {
			if (e.getId()==INotesErrorConstants.ERR_NOTE_NOT_SIGNED) {
				return ""; //$NON-NLS-1$
			}
			else {
				throw e;
			}
		}
	}

	/**
	 * This function verifies a signature on a note or section(s) within a note.
* It returns an error if a signature did not verify.
*
* @return signer data */ @Override public SignatureData verifySignature() { checkDisposed(); NotesTimeDateStruct retWhenSigned = NotesTimeDateStruct.newInstance(); try( DisposableMemory retSigner = new DisposableMemory(NotesConstants.MAXUSERNAME); DisposableMemory retCertifier = new DisposableMemory(NotesConstants.MAXUSERNAME); ) { JNADocumentAllocations allocations = getAllocations(); LockUtil.lockHandle(allocations.getNoteHandle(), (noteHandleByVal) -> { short result = NotesCAPI.get().NSFNoteExpand(noteHandleByVal); NotesErrorUtils.checkResult(result); result = NotesCAPI.get().NSFNoteVerifySignature (noteHandleByVal, null, retWhenSigned, retSigner, retCertifier); NotesErrorUtils.checkResult(result); result = NotesCAPI.get().NSFNoteContract(noteHandleByVal); NotesErrorUtils.checkResult(result); return 0; }); String signer = NotesStringUtils.fromLMBCS(retSigner, NotesStringUtils.getNullTerminatedLength(retSigner)); String certifier = NotesStringUtils.fromLMBCS(retCertifier, NotesStringUtils.getNullTerminatedLength(retCertifier)); SignatureData data = new SignatureDataImpl(new JNADominoDateTime(retWhenSigned.Innards), signer, certifier); return data; } } @Override public boolean isSigned() { checkDisposed(); ByteByReference signed_flag_ptr = new ByteByReference(); ByteByReference sealed_flag_ptr = new ByteByReference(); JNADocumentAllocations allocations = getAllocations(); return LockUtil.lockHandle(allocations.getNoteHandle(), (noteHandleByVal) -> { NotesCAPI.get().NSFNoteIsSignedOrSealed(noteHandleByVal, signed_flag_ptr, sealed_flag_ptr); byte signed = signed_flag_ptr.getValue(); return signed == 1; }); } @Override public boolean isNew() { return getNoteID() == 0; } @Override public boolean isEncrypted() { checkDisposed(); ByteByReference signed_flag_ptr = new ByteByReference(); ByteByReference sealed_flag_ptr = new ByteByReference(); JNADocumentAllocations allocations = getAllocations(); return LockUtil.lockHandle(allocations.getNoteHandle(), (noteHandleByVal) -> { NotesCAPI.get().NSFNoteIsSignedOrSealed(noteHandleByVal, signed_flag_ptr, sealed_flag_ptr); byte sealed = sealed_flag_ptr.getValue(); return sealed == 1; }); } @Override public boolean isTruncated() { int flags = getFlags(); return (flags & NotesConstants.NOTE_FLAG_ABSTRACTED) == NotesConstants.NOTE_FLAG_ABSTRACTED; } @Override public Document removeItem(String itemName) { checkDisposed(); Memory itemNameMem = NotesStringUtils.toLMBCS(itemName, false); JNADocumentAllocations allocations = getAllocations(); short result = LockUtil.lockHandle(allocations.getNoteHandle(), (noteHandleByVal) -> { //removes all items with this name (not just the first): return NotesCAPI.get().NSFItemDelete(noteHandleByVal, itemNameMem, (short) (itemNameMem.size() & 0xffff)); }); if (result==INotesErrorConstants.ERR_ITEM_NOT_FOUND) { return this; } NotesErrorUtils.checkResult(result); return this; } @Override public T get(String itemName, Class valueType, T defaultValue) { return m_typedAccess.get(itemName, valueType, defaultValue); } @Override public List getAsList(String itemName, Class valueType, List defaultValue) { return m_typedAccess.getAsList(itemName, valueType, defaultValue); } @Override public Optional getOptional(String itemName, Class valueType) { return m_typedAccess.getOptional(itemName, valueType); } @Override public Optional> getAsListOptional(String itemName, Class valueType) { return m_typedAccess.getAsListOptional(itemName, valueType); } private String[] parseProfileAndUserName() { String name = get("$name", String.class, ""); //$profile_015calendarprofile_ //$NON-NLS-1$ //$NON-NLS-2$ if (StringUtil.isEmpty(name) || !name.startsWith("$profile_")) { //$NON-NLS-1$ return null; } String remainder = name.substring(9); //"$profile_".length() if (remainder.length()<3) { return null; } String profileNameLengthStr = remainder.substring(0, 3); int profileNameLength = Integer.parseInt(profileNameLengthStr); remainder = remainder.substring(3); String profileName = remainder.substring(0, profileNameLength); remainder = remainder.substring(profileNameLength+1); String userName = remainder; return new String[] {profileName, userName}; } @Override public String getProfileName() { if (isHiddenFromViews()) { String[] profileAndUsername = parseProfileAndUserName(); if (profileAndUsername!=null) { return profileAndUsername[0]; } } return ""; //$NON-NLS-1$ } @Override public String getProfileUserName() { if (isHiddenFromViews()) { String[] profileAndUsername = parseProfileAndUserName(); if (profileAndUsername!=null) { return profileAndUsername[1]; } } return ""; //$NON-NLS-1$ } /** * Writes a primary key information to the note. This primary key can be used for * efficient note retrieval without any lookup views.
*
* Both category and objectKey are combined * to a string that is expected to be unique within the database. * * @param category category part of primary key * @param objectId object id part of primary key */ @Override public Document setPrimaryKey(String category, String objectId) { String name = JNADatabase.getApplicationNoteName(category, objectId); return replaceItemValue("$name", name); //$NON-NLS-1$ } /** * Returns the category part of the note primary key * * @return category or empty string if no primary key has been assigned */ @Override public String getPrimaryKeyCategory() { String name = get("$name", String.class, ""); //$NON-NLS-1$ //$NON-NLS-2$ if (!StringUtil.isEmpty(name)) { String[] parsedParts = JNADatabase.parseApplicationNamedNoteName(name); if (parsedParts!=null) { return parsedParts[0]; } } return ""; //$NON-NLS-1$ } /** * Returns the object id part of the note primary key * * @return object id or empty string if no primary key has been assigned */ @Override public String getPrimaryKeyObjectId() { String name = get("$name", String.class, ""); //$NON-NLS-1$ //$NON-NLS-2$ if (!StringUtil.isEmpty(name)) { String[] parsedParts = JNADatabase.parseApplicationNamedNoteName(name); if (parsedParts!=null) { return parsedParts[1]; } } return ""; //$NON-NLS-1$ } @Override public boolean isHiddenFromViews() { int flags = getFlags(); return (flags & NotesConstants.NOTE_FLAG_GHOST) == NotesConstants.NOTE_FLAG_GHOST; } /** * Changes the note's ghost flag. Ghost notes do not appear in any view or search. * * @param b true if ghost */ void setHiddenFromViews(boolean b) { short flags = getFlags(); short newFlags; if (b) { if ((flags & NotesConstants.NOTE_FLAG_GHOST) == NotesConstants.NOTE_FLAG_GHOST) { return; } newFlags = (short) ((flags | NotesConstants.NOTE_FLAG_GHOST) & 0xffff); } else { if ((flags & NotesConstants.NOTE_FLAG_GHOST) == 0) { return; } newFlags = (short) ((flags & ~NotesConstants.NOTE_FLAG_GHOST) & 0xffff); } setFlags(newFlags); } @Override public String toStringLocal() { if (isDisposed()) { return "JNADocument [disposed]"; //$NON-NLS-1$ } else { return format( "JNADocument [handle={0}, unid={1}, noteid={2}]", //$NON-NLS-1$ getAllocations().getNoteHandle(), getUNID(), getNoteID() ); } } @Override public boolean isSaveMessageOnSend() { return m_saveMessageOnSend; } @Override public Document setSaveMessageOnSend(boolean b) { m_saveMessageOnSend = b; return this; } @Override public Document send() { return send(false, (Collection) null); } @Override public Document send(String recipient) { return send(false, Arrays.asList(recipient)); } @Override public Document send(Collection recipients) { return send(false, recipients); } @Override public Document send(boolean attachform) { return send(attachform, (Collection) null); } @Override public Document send(boolean attachform, String recipient) { return send(attachform, Arrays.asList(recipient)); } /** * Clones the document without items * * @return clone without items */ private JNADocument cloneDocument() { checkDisposed(); return LockUtil.lockHandle(getAllocations().getNoteHandle(), (hNoteByVal) -> { DHANDLE.ByReference rethDstNote = DHANDLE.newInstanceByReference(); short result = NotesCAPI.get().NSFNoteCreateClone(hNoteByVal, rethDstNote); NotesErrorUtils.checkResult(result); return new JNADocument((JNADatabase) getParentDatabase(), rethDstNote); }); } /** * copy specified items from form when mailing * * @param formNote form note * @param tmpNote target note * @param excludeList exclude list of item names */ private void copyFormItems(JNADocument formNote, JNADocument tmpNote, List excludeList) { copyFormItems(formNote, tmpNote, excludeList, (short) 0, null); } /** * copy specified items from form when mailing * * @param formNote form note * @param tmpNote target note * @param excludeList exclude list of item names * @param namemod counter * @param subformname subform name or null */ private void copyFormItems(JNADocument formNote, JNADocument tmpNote, List excludeList, short namemod, String subformname) { formNote.checkDisposed(); tmpNote.checkDisposed(); // table of items which get renamed, if this is a subform copy List rename_list = Arrays.asList( NotesConstants.FORM_SCRIPT_ITEM_NAME, NotesConstants.DOC_SCRIPT_ITEM, NotesConstants.DOC_SCRIPT_NAME, NotesConstants.DOC_ACTION_ITEM ); /* These are the object-code item names, derived from the ones above (prepended '$', appended "_O") */ List rename_list_special = Arrays.asList( "$" + NotesConstants.FORM_SCRIPT_ITEM_NAME + "_O", //$NON-NLS-1$ //$NON-NLS-2$ "$" + NotesConstants.DOC_SCRIPT_ITEM + "_O" //$NON-NLS-1$ //$NON-NLS-2$ ); /* Copy all items beginning with '$' from the form note to * the current note. Certain item names are skipped for the * form ($FLAGS, $CLASS, $UPDATEDBY) and for subforms (indicated by * namemod > 0) ($TITLE, $BODY). * * If the "namemod" argument is > 0, then we also have to make * the new item name different: append the integer to the name * again, for selected item names. */ formNote.forEachItem((item, loop) -> { String itemName = item.getName(); if (itemName.startsWith("$")) { //$NON-NLS-1$ boolean exclude = false; if (ListUtil.containsIgnoreCase(excludeList, itemName)) { exclude = true; } else if (NotesConstants.ITEM_NAME_NOTE_SIGNATURE.equals(itemName)) { exclude = true; } if (!exclude) { /* If the item's name is not being modified, then use the easy ItemCopy call. If it is, though, we have to do a bit more work. If this is a subform, check the rename list to see if it really is getting renamed. Note that there are script object-code items with the same name as the source-code items, but with an appended "_O". If we find one of those, then we have to specially rename it, the number goes before the _O, not after */ boolean rename = false; boolean rename_special = false; boolean rename_signature = false; String itemname2 = ""; //$NON-NLS-1$ if (NotesConstants.ITEM_NAME_NOTE_SIGNATURE.equalsIgnoreCase(itemName)) { rename = true; rename_signature = true; if (namemod == 0) { itemname2 = NotesConstants.ITEM_NAME_NOTE_STOREDFORM_SIG; } else { itemname2 = NotesConstants.ITEM_NAME_NOTE_STOREDFORM_SIG_PREFIX + subformname; if (itemname2.length() > NotesConstants.MAXPATH) { itemname2 = itemname2.substring(0, NotesConstants.MAXPATH); } } } if (namemod>0 && !rename_signature) { if (ListUtil.containsIgnoreCase(rename_list, itemName)) { rename = true; } else if (ListUtil.containsIgnoreCase(rename_list_special, itemName)) { // check for special rename = true; rename_special = true; } } // "special" rename? if (rename_special) { itemname2 = itemName.substring(0, itemName.length()-2); // trim _O itemname2 += Short.toString(namemod); } else if (!rename_signature) { itemname2 = itemName + Short.toString(namemod); } if (rename) { //we need to check if this call does the same as the C code below item.copyToDocument(tmpNote, itemname2, false); } else { item.copyToDocument(tmpNote, false); } } } }); } /** * copy relevant subform items * * @param formNote source note * @param tmpNote target note */ private void copySubformItems(JNADocument formNote, JNADocument tmpNote) { checkDisposed(); JNADatabase parentDb = (JNADatabase) getParentDatabase(); JNADatabaseAllocations parentDbAllocations = (JNADatabaseAllocations) parentDb.getAdapter(APIObjectAllocations.class); parentDbAllocations.checkDisposed(); List exclude_list = Arrays.asList( NotesConstants.DESIGN_CLASS, NotesConstants.DESIGN_FLAGS, NotesConstants.FIELD_UPDATED_BY, NotesConstants.ITEM_NAME_TEMPLATE, NotesConstants.FIELD_TITLE, NotesConstants.ITEM_NAME_DOCUMENT ); /* If the form doesn't contain a subform name list, then there's nothing to do */ if (!formNote.hasItem(NotesConstants.SUBFORM_ITEM_NAME)) { return; } // get the list, iterate over the subform names List subformNames = formNote.getAsList(NotesConstants.SUBFORM_ITEM_NAME, String.class, Collections.emptyList()); for (int i = 0; i < subformNames.size(); i++) { String subformname = subformNames.get(i); Memory subformnameMem = NotesStringUtils.toLMBCS(subformname, true); if (NotesCAPI.get().StoredFormHasSubformToken(subformnameMem)) { continue; } PointerByReference ppName = new PointerByReference(); ShortByReference pNameLen = new ShortByReference(); PointerByReference ppAlias = new PointerByReference(); ShortByReference pAliasLen = new ShortByReference(); NotesCAPI.get().DesignGetNameAndAlias(subformnameMem, ppName, pNameLen, ppAlias, pAliasLen); Pointer subformaliasMem; short subformaliasLen; if (Short.toUnsignedInt(pAliasLen.getValue()) > 0) { subformaliasMem = ppAlias.getValue(); subformaliasLen = pAliasLen.getValue(); } else { subformaliasMem = ppName.getValue(); subformaliasLen = pNameLen.getValue(); } String subformalias = NotesStringUtils.fromLMBCS(subformaliasMem, Short.toUnsignedInt(subformaliasLen)); Memory designPatternMem = NotesStringUtils.toLMBCS(NotesConstants.DFLAGPAT_SUBFORM_ALL_VERSIONS, true); IntByReference fnid = new IntByReference(); short lkSubformResult = LockUtil.lockHandle(parentDbAllocations.getDBHandle(), (hDbByVal) -> { return NotesCAPI.get().DesignLookupNameFE(hDbByVal, NotesConstants.NOTE_CLASS_FORM, designPatternMem, subformaliasMem, subformaliasLen, NotesConstants.DGN_ONLYSHARED, fnid, null, null, null); }); if (lkSubformResult!=0) { /* Try private. */ lkSubformResult = LockUtil.lockHandle(parentDbAllocations.getDBHandle(), (hDbByVal) -> { return NotesCAPI.get().DesignLookupNameFE(hDbByVal, NotesConstants.NOTE_CLASS_FORM, designPatternMem, subformaliasMem, subformaliasLen, NotesConstants.DGN_ONLYPRIVATE, fnid, null, null, null); }); } if (lkSubformResult!=0) { continue; } try { JNADocument subformNote = (JNADocument) parentDb.getDocumentById(fnid.getValue(), EnumSet.of(OpenDocumentMode.CACHE)).orElse(null); if (subformNote!=null) { copyFormItems(subformNote, tmpNote, exclude_list, (short) ((i+2) & 0xffff), subformname); } } catch (DominoException e) { throw new DominoException(e.getId(), format("Error opening subform {0}", subformalias), e); } } } /** * detect any MIME_PART items not named "Body"; if not found, detect any TYPE_MIME_PART item named "Body" * * @param foundNonBodyMIME returns {@link Boolean#TRUE} if there are TYPE_MIME_PART items with a different name than "body" * @param foundBodyMIME returns {@link Boolean#TRUE} if there are TYPE_MIME_PART items with the name than "body" */ private void searchForNonBodyMIME(Ref foundNonBodyMIME, Ref foundBodyMIME) { foundNonBodyMIME.set(Boolean.FALSE); foundBodyMIME.set(Boolean.FALSE); /* Optimization: assume by this point that if there is no $NoteHasNativeMIME item, then * there aren't any MIME_PART items at all. */ if (!hasItem(NotesConstants.ITEM_IS_NATIVE_MIME)) { return; } /* Finally, scan all the items, looking for non-"Body" item of TYPE_MIME_PART */ forEachItem((item, loop) -> { if (item.getType() == ItemDataType.TYPE_MIME_PART) { String itemName = item.getName(); if (NotesConstants.MAIL_BODY_ITEM.equalsIgnoreCase(itemName)) { // don't stop here; finding MIME "Body" is secondary to finding MIME non-"Body"s foundBodyMIME.set(Boolean.TRUE); } else { foundNonBodyMIME.set(Boolean.TRUE); loop.stop(); } } }); } @Override public Document send(boolean attachform, Collection recipients) { checkDisposed(); JNADatabase parentDb = (JNADatabase) getParentDatabase(); if (parentDb.isDisposed()) { throw new ObjectDisposedException(parentDb); } short flags = 0; if (this.isSigned() || attachform) { flags |= NotesConstants.MSN_SIGN; } if (this.isEncrypted()) { flags |= NotesConstants.MSN_SEAL; } boolean cancelsend = false; if (cancelsend) { flags |= NotesConstants.MSN_PUBKEY_ONLY; } Ref foundNonBodyMIME = new Ref<>(); Ref foundBodyMIME = new Ref<>(); this.searchForNonBodyMIME(foundNonBodyMIME, foundBodyMIME); if (Boolean.TRUE.equals(foundNonBodyMIME.get())) { throw new DominoException("Found items of type MIME_PART that are not named 'Body'. This is currently unsupported."); } short wMailNoteFlags = NotesConstants.MAILNOTE_ANYRECIPIENT; if (Boolean.TRUE.equals(foundBodyMIME.get())) { wMailNoteFlags |= NotesConstants.MAILNOTE_MIMEBODY; wMailNoteFlags |= NotesConstants.MAILNOTE_NOTES_ENCRYPT_MIME; } /* SPR ajrs39vm4l: We're about to modify the user's note. * If they do something stupid, like send it a second time, * we're going to modify it again, ending up with all sorts * of duplicate items that will cause problems for the recipient. * So, we fix that by cloning the hnote here, making all * modifications on the copy. * * Note that we use NULL for the hdb when creating the note * to prevent a network round trip. Then we have to set the * hdb explicitly into the note. Later, after sending the thing, * we have to check to see if the original hnote needs updating * (for example, if the current note is not on disk, but the * user specified "save on send", then we have to update the * note id). */ //create a clone to not modify this note instance and prevent issues when sending a second time JNADocument tmpDoc = cloneDocument(); JNADocumentAllocations docAllocations = getAllocations(); JNADocumentAllocations tmpDocAllocations = (JNADocumentAllocations) tmpDoc.getAdapter(APIObjectAllocations.class); // now copy all items to the new note short copyItemsResult = LockUtil.lockHandles( docAllocations.getNoteHandle(), tmpDocAllocations.getNoteHandle(), (hNoteByVal, hTmpNoteByVal) -> { return NotesCAPI.get().NSFNoteReplaceItems(hNoteByVal, hTmpNoteByVal, null, true); }); NotesErrorUtils.checkResult(copyItemsResult); // did caller provide a recipients list? if (recipients!=null) { // if there's already a SendTo item, delete it tmpDoc.getFirstItem(NotesConstants.MAIL_SENDTO_ITEM).ifPresent(Item::remove); recipients = NotesNamingUtils.toCanonicalNames(recipients); tmpDoc.replaceItemValue(NotesConstants.MAIL_SENDTO_ITEM, recipients); } if (tmpDoc.hasItem(NotesConstants.MAIL_COPYTO_ITEM)) { List copyToNames = tmpDoc.getAsList(NotesConstants.MAIL_COPYTO_ITEM, String.class, Collections.emptyList()); copyToNames = NotesNamingUtils.toCanonicalNames(copyToNames); tmpDoc.replaceItemValue(NotesConstants.MAIL_COPYTO_ITEM, copyToNames); } if (tmpDoc.hasItem(NotesConstants.MAIL_BLINDCOPYTO_ITEM)) { List blindCopyToNames = tmpDoc.getAsList(NotesConstants.MAIL_COPYTO_ITEM, String.class, Collections.emptyList()); blindCopyToNames = NotesNamingUtils.toCanonicalNames(blindCopyToNames); tmpDoc.replaceItemValue(NotesConstants.MAIL_BLINDCOPYTO_ITEM, blindCopyToNames); } if (!tmpDoc.hasItem(NotesConstants.MAIL_SENDTO_ITEM) && !tmpDoc.hasItem(NotesConstants.MAIL_COPYTO_ITEM) && !tmpDoc.hasItem(NotesConstants.MAIL_BLINDCOPYTO_ITEM)) { throw new DominoException(0, "Missing mail recipient items"); } /* To attach the form, find it in the database, get the all * items from the form note beginning with '$' (with some * exceptions), and copy them to the mail note. Then, * we have to delete the Form item, which points to the original * form name. By deleting it, we're telling the editor to look * for the stored form instead. * * If the form contains a SUBFORM_ITEM (textlist of the * names of subforms used in the form), then we have * to copy a bunch of sub-form stuff too */ if (attachform) { // Remove old stored form items before adding items from another form short removeStoredFormResult = LockUtil.lockHandle(tmpDocAllocations.getNoteHandle(), (hTmpNoteByVal) -> { return NotesCAPI.get().StoredFormRemoveItems(hTmpNoteByVal, 0); }); NotesErrorUtils.checkResult(removeStoredFormResult); Item itmForm = tmpDoc.getFirstItem(NotesConstants.FIELD_FORM).orElse(null); Item itmBody = tmpDoc.getFirstItem(NotesConstants.ITEM_NAME_TEMPLATE).orElse(null); IntByReference retFormNoteId = new IntByReference(); retFormNoteId.setValue(0); if (itmForm!=null) { String formnameStr = tmpDoc.get(NotesConstants.FIELD_FORM, String.class, ""); //$NON-NLS-1$ /* Delete the form item from the note, we copied the name. * We don't want a Form item on the note when we attach * the form itself, otherwise the editor gets confused. * Doing the Remove also deletes the object. */ itmForm.remove(); if (!StringUtil.isEmpty(formnameStr)) { Memory formnameStrMem = NotesStringUtils.toLMBCS(formnameStr, true); PointerByReference pName = new PointerByReference(); ShortByReference wNameLen = new ShortByReference(); PointerByReference pAlias = new PointerByReference(); ShortByReference wAliasLen = new ShortByReference(); NotesCAPI.get().DesignGetNameAndAlias(formnameStrMem, pName, wNameLen, pAlias, wAliasLen); DisposableMemory szBuffer = new DisposableMemory(NotesConstants.DESIGN_ALL_NAMES_MAX); szBuffer.clear(); if (wAliasLen.getValue()>0) { byte[] aliasArr = pAlias.getValue().getByteArray(0, wAliasLen.getValue() & 0xffff); szBuffer.write(0, aliasArr, 0, aliasArr.length); } else { byte[] wNameArr = pName.getValue().getByteArray(0, wNameLen.getValue() & 0xffff); szBuffer.write(0, wNameArr, 0, wNameArr.length); } short lkFormResult; Memory szFlagsPatternMem = NotesStringUtils.toLMBCS(NotesConstants.DFLAGPAT_VIEWFORM_ALL_VERSIONS, true); JNADatabaseAllocations dbAllocations = (JNADatabaseAllocations) getParentDatabase().getAdapter(APIObjectAllocations.class); lkFormResult = LockUtil.lockHandle(dbAllocations.getDBHandle(), (hDbByVal) -> { return NotesCAPI.get().DesignLookupNameFE(hDbByVal, NotesConstants.NOTE_CLASS_FORM, szFlagsPatternMem, szBuffer, wAliasLen.getValue()>0 ? wAliasLen.getValue() : wNameLen.getValue(), NotesConstants.DGN_ONLYSHARED, retFormNoteId, (IntByReference) null, (NotesCallbacks.DESIGN_COLL_OPENCLOSE_PROC) null, null); }); if (lkFormResult!=0) { /* Try private. */ lkFormResult = LockUtil.lockHandle(dbAllocations.getDBHandle(), (hDbByVal) -> { return NotesCAPI.get().DesignLookupNameFE(hDbByVal, NotesConstants.NOTE_CLASS_FORM, szFlagsPatternMem, szBuffer, wAliasLen.getValue()>0 ? wAliasLen.getValue() : wNameLen.getValue(), NotesConstants.DGN_ONLYPRIVATE, retFormNoteId, (IntByReference) null, (NotesCallbacks.DESIGN_COLL_OPENCLOSE_PROC) null, null); }); } if (lkFormResult==0) { // delete existing $body while (tmpDoc.hasItem(NotesConstants.ITEM_NAME_TEMPLATE)) { tmpDoc.removeItem(NotesConstants.ITEM_NAME_TEMPLATE); } JNADocument formNote = (JNADocument) getParentDatabase().getDocumentById(retFormNoteId.getValue(), EnumSet.of(OpenDocumentMode.CACHE)).orElse(null); if (formNote!=null) { List excludeList = Arrays.asList( NotesConstants.DESIGN_CLASS, NotesConstants.DESIGN_FLAGS, NotesConstants.FIELD_UPDATED_BY); // do the $ items copyFormItems(formNote, tmpDoc, excludeList); // check for subforms copySubformItems(formNote, tmpDoc); // this will add the new, more secure style of stored form and subform items // to the target doc // Note: This has to be done after $Body item(s) have been added to the note // by copyFormItems JNADocumentAllocations formDocAllocations = (JNADocumentAllocations) formNote.getAdapter(APIObjectAllocations.class); short addStoredFormResult = LockUtil.lockHandles( dbAllocations.getDBHandle(), formDocAllocations.getNoteHandle(), tmpDocAllocations.getNoteHandle(), (hDbByVal, hFormNoteByVal, hTmpNoteByVal) -> { return NotesCAPI.get().StoredFormAddItems(hDbByVal, hFormNoteByVal, hTmpNoteByVal, true, 0); }); NotesErrorUtils.checkResult(addStoredFormResult); formNote.dispose(); } } } } else { /* If have body and blank form, use existing body */ if (itmBody==null) { throw new DominoException("Found no form name in the document to look up the form to attach"); } } } final boolean isMsgComingFromAgent = false; if (isMsgComingFromAgent) { if (!tmpDoc.hasItem(NotesConstants.ASSIST_MAIL_ITEM)) { tmpDoc.replaceItemValue(NotesConstants.ASSIST_MAIL_ITEM, "1"); //$NON-NLS-1$ } } /* IETF standard auto flag */ tmpDoc.replaceItemValue(NotesConstants.MAIL_ITEM_AUTOSUBMITTED, NotesConstants.MAIL_AUTOGENERATED); /* If this is happening on a server, then we want to tag the * message as being from the effective user, not from the server. * Always check for the special "from" item, though, in case the * user is getting tricky with us */ // if there's already a From item, delete it tmpDoc.getFirstItem(NotesConstants.MAIL_FROM_ITEM).ifPresent(Item::remove); JNADominoClient parentClient = (JNADominoClient)getParentDominoClient(); if (parentClient.isOnServer() || Boolean.TRUE.equals(parentClient.getCustomValue("notesnote.sendasotheruser"))) { // we added a flag here to test this in the client //$NON-NLS-1$ String effUserName = parentClient.getEffectiveUserName(); if (!StringUtil.isEmpty(effUserName)) { effUserName = NotesNamingUtils.toCanonicalName(effUserName); tmpDoc.replaceItemValue(NotesConstants.MAIL_FROM_ITEM, effUserName); } } // Contract before sending to be editor-compatible short contractResult = LockUtil.lockHandle(tmpDocAllocations.getNoteHandle(), (hTmpNoteByVal) -> { return NotesCAPI.get().NSFNoteContract(hTmpNoteByVal); }); NotesErrorUtils.checkResult(contractResult); /*spr bban3kzhk9 -- allow sendto AND/OR copyto AND/OR blindcopyto */ /* snis6z2taf et al. -- reinstate ability to MIME encrypt, by flagging any MIME body for the mailer */ short fFlags = flags; short fwMailNoteFlags = wMailNoteFlags; short mailNoteResult = LockUtil.lockHandle(tmpDocAllocations.getNoteHandle(), (hTmpNoteByVal) -> { return NotesCAPI.get().MailNoteJitEx2(null, hTmpNoteByVal, fFlags, null, NotesConstants.MAIL_NO_JIT, fwMailNoteFlags, null, null); }); NotesErrorUtils.checkResult(mailNoteResult); /* must replace certain critical item(s) on original note with their new after * mailing values so that the note will appear in 'Sent' view, etc. * Note: wholesale replacement of all items with new values will result in a * regression problem with spr ajrs39vm4l */ /* here we query the new copy for posted date */ DominoDateTime postedDate = tmpDoc.get(NotesConstants.MAIL_POSTEDDATE_ITEM, DominoDateTime.class, null); removeItem(NotesConstants.MAIL_POSTEDDATE_ITEM); if (postedDate!=null) { replaceItemValue(NotesConstants.MAIL_POSTEDDATE_ITEM, postedDate); } // save the msg? if (isSaveMessageOnSend()) { // Message recall does not work for mails sent from a DIIOP program // need to generate message id for the recall feature to work. // Make sure deleting the existing message id if it is present and re-create new one to match with // the one in local mail.box for the recall feature to work. if (hasItem(NotesConstants.MAIL_ID_ITEM)) { removeItem(NotesConstants.MAIL_ID_ITEM); } DisposableMemory messageId = new DisposableMemory(NotesConstants.MAXPATH+1); short setMsgIdResult = LockUtil.lockHandle(docAllocations.getNoteHandle(), (hNoteByVal) -> { return NotesCAPI.get().MailSetSMTPMessageID(hNoteByVal, null, messageId, (short) (NotesConstants.MAXPATH & 0xffff)); }); NotesErrorUtils.checkResult(setMsgIdResult); String messageIdStr = NotesStringUtils.fromLMBCS(messageId, -1); replaceItemValue(NotesConstants.MAIL_ID_ITEM, messageIdStr); // we save the original note, not the new one save(); } // now we can kill the temp doc and reset tmpDoc.dispose(); return this; } @Override public Document computeWithForm(boolean continueOnError, Form form, final ComputeWithFormCallback callback) { checkDisposed(); int dwFlags = continueOnError ? NotesConstants.CWF_CONTINUE_ON_ERROR : 0; DHANDLE hFormNote; if(form != null) { hFormNote = form.getDocument().getAdapter(DHANDLE.class); } else { hFormNote = null; } if (PlatformUtils.is64Bit()) { NotesCallbacks.b64_CWFErrorProc errorProc = (pCDField, phase, error, hErrorText, wErrorTextSize, ctx) -> { @SuppressWarnings("deprecation") DHANDLE hErrorTextObj = new DHANDLE64(hErrorText); String errorTxt; if (hErrorTextObj.isNull()) { errorTxt = ""; //$NON-NLS-1$ } else { errorTxt = LockUtil.lockHandle(hErrorTextObj, (hErrorTextObjByVal) -> { Pointer errorTextPtr = Mem.OSLockObject(hErrorTextObjByVal); try { // TODO find out where this offset 6 comes from; hErrorText is a handle to // memory containing the text that caused the error. The handle is a handle to // TYPE_TEXT according to C API return NotesStringUtils.fromLMBCS(errorTextPtr.share(6), (wErrorTextSize & 0xffff) - 6); } finally { Mem.OSUnlockObject(hErrorTextObjByVal); } }); } ComputeWithFormPhase phaseEnum = decodeValidationPhase(phase); ComputeWithFormAction action; if (callback == null) { action = ComputeWithFormAction.ABORT; } else { DominoException errorEx = NotesErrorUtils.toNotesError(error).orElse(null); FormField fieldInfo = readFormField(pCDField); action = callback.errorRaised(fieldInfo, phaseEnum, errorTxt, errorEx); } return action == null ? ComputeWithFormAction.ABORT.getShortVal() : action.getShortVal(); }; short result = LockUtil.lockHandles(getAllocations().getNoteHandle(), hFormNote, (hNoteByVal, hFormNoteByVal) -> { return NotesCAPI.get().NSFNoteComputeWithForm(hNoteByVal, hFormNoteByVal, dwFlags, errorProc, null); }); NotesErrorUtils.checkResult(result); } else { NotesCallbacks.b32_CWFErrorProc errorProc; if (PlatformUtils.isWin32()) { errorProc = (pCDField, phase, error, hErrorText, wErrorTextSize, ctx) -> { @SuppressWarnings("deprecation") DHANDLE hErrorTextObj = new DHANDLE32(hErrorText); String errorTxt; if (hErrorTextObj.isNull()) { errorTxt = ""; //$NON-NLS-1$ } else { errorTxt = LockUtil.lockHandle(hErrorTextObj, (hErrorTextObjByVal) -> { Pointer errorTextPtr = Mem.OSLockObject(hErrorTextObjByVal); try { // TODO find out where this offset 6 comes from; hErrorText is a handle to // memory containing the text that caused the error. The handle is a handle to // TYPE_TEXT according to C API return NotesStringUtils.fromLMBCS(errorTextPtr.share(6), (wErrorTextSize & 0xffff) - 6); } finally { Mem.OSUnlockObject(hErrorTextObjByVal); } }); } ComputeWithFormPhase phaseEnum = decodeValidationPhase(phase); ComputeWithFormAction action; if (callback == null) { action = ComputeWithFormAction.ABORT; } else { DominoException errorEx = NotesErrorUtils.toNotesError(error).orElse(null); FormField fieldInfo = readFormField(pCDField); action = callback.errorRaised(fieldInfo, phaseEnum, errorTxt, errorEx); } return action == null ? ComputeWithFormAction.ABORT.getShortVal() : action.getShortVal(); }; } else { errorProc = (pCDField, phase, error, hErrorText, wErrorTextSize, ctx) -> { @SuppressWarnings("deprecation") DHANDLE hErrorTextObj = new DHANDLE32(hErrorText); String errorTxt; if (hErrorTextObj.isNull()) { errorTxt = ""; //$NON-NLS-1$ } else { errorTxt = LockUtil.lockHandle(hErrorTextObj, (hErrorTextObjByVal) -> { Pointer errorTextPtr = Mem.OSLockObject(hErrorTextObjByVal); try { // TODO find out where this offset 6 comes from; hErrorText is a handle to // memory containing the text that caused the error. The handle is a handle to // TYPE_TEXT according to C API return NotesStringUtils.fromLMBCS(errorTextPtr.share(6), (wErrorTextSize & 0xffff) - 6); } finally { Mem.OSUnlockObject(hErrorTextObjByVal); } }); } ComputeWithFormPhase phaseEnum = decodeValidationPhase(phase); ComputeWithFormAction action; if (callback == null) { action = ComputeWithFormAction.ABORT; } else { DominoException errorEx = NotesErrorUtils.toNotesError(error).orElse(null); FormField fieldInfo = readFormField(pCDField); action = callback.errorRaised(fieldInfo, phaseEnum, errorTxt, errorEx); } return action == null ? ComputeWithFormAction.ABORT.getShortVal() : action.getShortVal(); }; } short result = LockUtil.lockHandles(getAllocations().getNoteHandle(), hFormNote, (hNoteByVal, hFormNoteByVal) -> { return NotesCAPI.get().NSFNoteComputeWithForm(hNoteByVal, hFormNoteByVal, dwFlags, errorProc, null); }); NotesErrorUtils.checkResult(result); } return this; } private static FormField readFormField(Pointer pCDField) { ByteBuffer cdBuf = pCDField.getByteBuffer(0, MemoryStructureUtil.sizeOf(CDField.class)); CDField cdFieldStruct = MemoryStructureUtil.forStructure(CDField.class, () -> cdBuf); // Re-read with the now-known length ByteBuffer cdBuf2 = pCDField.getByteBuffer(0, cdFieldStruct.getHeader().getLength()); cdFieldStruct = MemoryStructureWrapperService.get().wrapStructure(CDField.class, cdBuf2); return new FormFieldImpl(Collections.singleton(cdFieldStruct)); } private ComputeWithFormPhase decodeValidationPhase(short phase) { ComputeWithFormPhase phaseEnum = null; switch (phase) { case NotesConstants.CWF_DV_FORMULA: phaseEnum = ComputeWithFormPhase.DEFAULT_VALUE_FORMULA; break; case NotesConstants.CWF_IT_FORMULA: phaseEnum = ComputeWithFormPhase.INPUT_TRANSLATION_FORMULA; break; case NotesConstants.CWF_IV_FORMULA: phaseEnum = ComputeWithFormPhase.INPUT_VALIDATION_FORMULA; break; case NotesConstants.CWF_COMPUTED_FORMULA: // Also CWF_COMPUTED_FORMULA_LOAD phaseEnum = ComputeWithFormPhase.COMPUTED_FIELD_FORMULA; break; case NotesConstants.CWF_DATATYPE_CONVERSION: phaseEnum = ComputeWithFormPhase.DATATYPE_VERIFICATION; break; case NotesConstants.CWF_COMPUTED_FORMULA_SAVE: phaseEnum = ComputeWithFormPhase.COMPUTED_FORMULA_SAVE; break; default: break; } return phaseEnum; } @Override public boolean isUnread() { return getParentDatabase().isDocumentUnread(null, getNoteID()); } @Override public boolean isUnread(String userName) { return getParentDatabase().isDocumentUnread(userName, getNoteID()); } @Override public boolean convertRichTextItem(String itemName, IRichTextConversion... conversions) { return convertRichTextItem(itemName, this, itemName, conversions); } @Override public void convertRFC822Items() { short result = LockUtil.lockHandle(getAllocations().getNoteHandle(), (hNoteByVal) -> { boolean isCanonical = (getFlags() & NotesConstants.NOTE_FLAG_CANONICAL) == NotesConstants.NOTE_FLAG_CANONICAL; return NotesCAPI.get().MIMEConvertRFC822TextItems(hNoteByVal, isCanonical); }); NotesErrorUtils.checkResult(result); } @Override public boolean convertRichTextItem(String itemName, Document targetNote, String targetItemName, IRichTextConversion... conversions) { checkDisposed(); if (conversions==null || conversions.length==0) { return false; } List> navFromNote = getRichTextItem(itemName); List> currNav = navFromNote; JNARichtextWriter tmpRichText = null; for (int i=0; i> nextNav = tmpRichText.closeAndGetRichTextNavigator(); currNav = nextNav; } } if (tmpRichText!=null) { tmpRichText.closeAndCopyToDoc(targetNote, targetItemName); return true; } else { return false; } } @Override public boolean isSoftDeleted() { return (getFlags2() & NotesConstants.NOTE_FLAG2_SOFT_DELETED) == NotesConstants.NOTE_FLAG2_SOFT_DELETED; } @Override public Document setUnread(String userName, boolean unread) { if (unread) { getParentDatabase().updateUnreadDocumentTable(userName, null, Collections.singleton(getNoteID())); } else { getParentDatabase().updateUnreadDocumentTable(userName, Collections.singleton(getNoteID()), null); } return this; } @Override public Document compileLotusScript() { checkDisposed(); JNADatabaseAllocations parentDbAllocations = (JNADatabaseAllocations) getParent().getAdapter(APIObjectAllocations.class); parentDbAllocations.checkDisposed(); final LotusScriptCompilationException[] ex = new LotusScriptCompilationException[1]; short result = LockUtil.lockHandles(parentDbAllocations.getDBHandle(), getAllocations().getNoteHandle(), (hDb, hNote) -> { NotesCallbacks.LSCOMPILEERRPROC callback = (pInfo, pCtx) -> { int version = Short.toUnsignedInt(pInfo.Version); int line = Short.toUnsignedInt(pInfo.Line); String errText = ""; //$NON-NLS-1$ String errFile = ""; //$NON-NLS-1$ if(!Platform.isMac()) { // TODO investigate why these segfault on first pointer access on macOS errText = NotesStringUtils.fromLMBCS(pInfo.pErrText, -1); errFile = NotesStringUtils.fromLMBCS(pInfo.pErrFile, -1); } ex[0] = new LotusScriptCompilationException(errText, errFile, version, line); return INotesErrorConstants.NOERROR; }; if (PlatformUtils.isWin32()) { Win32NotesCallbacks.LSCOMPILEERRPROCWin32 callbackWin32 = (pInfo, pCtx) -> { return callback.invoke(pInfo, pCtx); }; return NotesCAPI.get().NSFNoteLSCompileExt(hDb, hNote, 0, callbackWin32, null); } else { return NotesCAPI.get().NSFNoteLSCompileExt(hDb, hNote, 0, callback, null); } }); if(ex[0] != null) { throw ex[0]; } NotesErrorUtils.checkResult(result); return this; } @Override public int getAsInt(String itemName, int defaultValue) { checkDisposed(); return LockUtil.lockHandle(getAllocations().getNoteHandle(), (hNoteByVal) -> { Memory itemNameLmbcs = NotesStringUtils.toLMBCS(itemName, true); return NotesCAPI.get().NSFItemGetLong(hNoteByVal, itemNameLmbcs, defaultValue); }); } @Override public String getAsText(String itemName, char separator) { checkDisposed(); try(DisposableMemory returnBuf = new DisposableMemory(60 * 1024)) { short txtLengthAsShort = LockUtil.lockHandle(getAllocations().getNoteHandle(), (hNoteByVal) -> { Memory itemNameLmbcs = NotesStringUtils.toLMBCS(itemName, true); return NotesCAPI.get().NSFItemConvertToText(hNoteByVal, itemNameLmbcs, returnBuf, (short)(60 * 1024), separator); }); int txtLength = txtLengthAsShort & 0xffff; if (txtLength==0) { return ""; //$NON-NLS-1$ } else { return NotesStringUtils.fromLMBCS(returnBuf, txtLength); } } } @Override public long size() { checkDisposed(); long[] totalSize = new long[1]; getItems(null, (item) -> { totalSize[0] += item.getValueLength(); if (item.getType() == ItemDataType.TYPE_OBJECT) { List fileDataAsList = item.getValue(); if (!fileDataAsList.isEmpty() && fileDataAsList.get(0) instanceof Attachment) { Attachment att = (Attachment) fileDataAsList.get(0); totalSize[0] += att.getFileSize(); } } return IItemCallback.Action.Continue; }); return totalSize[0]; } @Override public Document appendToTextList(String itemName, String value, boolean allowDuplicates) { checkDisposed(); Memory itemNameMem = NotesStringUtils.toLMBCS(itemName, true); Memory valueMem = NotesStringUtils.toLMBCS(Objects.requireNonNull(value), false); if (valueMem.size() > 0xffff) { throw new IllegalArgumentException("Value exceeds max size of 65535 bytes"); } short valueLen = (short) (valueMem.size() & 0xffff); short result = LockUtil.lockHandle(getAllocations().getNoteHandle(), (hNoteByVal) -> { return NotesCAPI.get().NSFItemAppendTextList(hNoteByVal, itemNameMem, valueMem, valueLen, allowDuplicates); }); NotesErrorUtils.checkResult(result); return this; } @Override public String getNameOfDoc() { String rawName = get("$name", String.class, ""); //$NON-NLS-1$ //$NON-NLS-2$ return JNADatabase .parseLegacyAPINamedNoteName(rawName) .map((v) -> { return v[0]; }) .orElse(""); //$NON-NLS-1$ } @Override public String getUserNameOfDoc() { String rawName = get("$name", String.class, ""); //$NON-NLS-1$ //$NON-NLS-2$ return JNADatabase .parseLegacyAPINamedNoteName(rawName) .map((v) -> { return v[1]; }) .orElse(""); //$NON-NLS-1$ } }