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

com.hcl.domino.jna.html.JNARichtextHTMLConverter 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.html;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import com.hcl.domino.DominoClient;
import com.hcl.domino.DominoException;
import com.hcl.domino.commons.gc.APIObjectAllocations;
import com.hcl.domino.commons.html.CommandId;
import com.hcl.domino.commons.html.IHtmlApiReference;
import com.hcl.domino.commons.html.IHtmlApiUrlTargetComponent;
import com.hcl.domino.commons.html.ReferenceType;
import com.hcl.domino.commons.html.TargetType;
import com.hcl.domino.commons.util.NotesErrorUtils;
import com.hcl.domino.commons.util.PlatformUtils;
import com.hcl.domino.commons.util.StringUtil;
import com.hcl.domino.data.Database;
import com.hcl.domino.data.Database.Action;
import com.hcl.domino.data.Document;
import com.hcl.domino.exception.ObjectDisposedException;
import com.hcl.domino.html.EmbeddedImage;
import com.hcl.domino.html.EmbeddedImage.HTMLImageReader;
import com.hcl.domino.html.HtmlConversionResult;
import com.hcl.domino.html.HtmlConvertOption;
import com.hcl.domino.html.RichTextHTMLConverter;
import com.hcl.domino.jna.JNADominoClient;
import com.hcl.domino.jna.data.JNADatabase;
import com.hcl.domino.jna.data.JNADocument;
import com.hcl.domino.jna.internal.DisposableMemory;
import com.hcl.domino.jna.internal.JNANotesConstants;
import com.hcl.domino.jna.internal.Mem;
import com.hcl.domino.jna.internal.NotesStringUtils;
import com.hcl.domino.jna.internal.capi.NotesCAPI;
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.LockUtil;
import com.hcl.domino.jna.internal.structs.HtmlAPIReference32Struct;
import com.hcl.domino.jna.internal.structs.HtmlAPIReference64Struct;
import com.hcl.domino.jna.internal.structs.HtmlApi_UrlTargetComponentStruct;
import com.hcl.domino.jna.internal.structs.NoteIdStruct;
import com.hcl.domino.jna.internal.structs.NotesUniversalNoteIdStruct;
import com.hcl.domino.misc.DominoClientDescendant;
import com.hcl.domino.misc.NotesConstants;
import com.sun.jna.Memory;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import com.sun.jna.StringArray;
import com.sun.jna.ptr.IntByReference;

/**
 * Implementation of {@link RichTextHTMLConverter} to render a document
 * or single item as HTML.
 * 
 * @author Karsten Lehmann
 */
public class JNARichtextHTMLConverter implements RichTextHTMLConverter, DominoClientDescendant {
	private JNADominoClient m_client;
	
	public JNARichtextHTMLConverter(JNADominoClient client) {
		m_client = client;
	}
	
	@Override
	public DominoClient getParentDominoClient() {
		return m_client;
	}
	
	@Override
	public Builder renderItem(Document doc, String itemName) {
		
		if (StringUtil.isEmpty(itemName)) {
			throw new NullPointerException("Item name cannot be empty");
		}
		
		return new JNAHtmlConverterBuilder(doc, itemName);
	}

	@Override
	public Builder render(Document doc) {
		return new JNAHtmlConverterBuilder(doc, null);
	}
	
	@Override
	public Builder render(Database database) {
		return new JNAHtmlConverterBuilder(database);
	}
	
	private class JNAHtmlConverterBuilder implements Builder {
		private final Set options = new LinkedHashSet<>();
		private final Database database;
		private final Document doc;
		private final String itemName;
		private String userAgent;
		
		private JNAHtmlConverterBuilder(Database database) {
			this.database = database;
			this.doc = null;
			this.itemName = null;
		}
		
		private JNAHtmlConverterBuilder(Document doc, String itemName) {
			this.database = doc.getParentDatabase();
			this.doc = doc;
			this.itemName = itemName;
		}

		@Override
		public Builder option(HtmlConvertOption option, String value) {
			Objects.requireNonNull(option, "option cannot be null");
			options.add(option.toOption(value));
			return this;
		}

		@Override
		public Builder option(String option, String value) {
			Objects.requireNonNull(option, "option cannot be null");
			if(option.isEmpty()) {
				throw new IllegalArgumentException("option cannot be empty");
			}
			options.add(option + "=" + StringUtil.toString(value)); //$NON-NLS-1$
			return this;
		}

		@Override
		public Builder options(Collection options) {
			if(options != null && !options.isEmpty()) {
				this.options.addAll(options);
			}
			return this;
		}

		@Override
		public Builder options(Collection options, String value) {
			if(options != null && !options.isEmpty()) {
				for (HtmlConvertOption currOption : options) {
					this.options.add(currOption.toOption(value));
				}
			}
			return this;
		}
		
		@Override
		public Builder userAgent(String userAgent) {
			this.userAgent = userAgent;
			return this;
		}

		@Override
		public HtmlConversionResult convert() {
			return internalRenderDocumentOrItemToHTML(this, (Set) null, (Map>) null);
		}
		
	}

	/**
	 * Internal method doing the HTML conversion work
	 * 
	 * @param config the {@link JNAHtmlConverterBuilder} object used to configure the conversion
	 * @param refTypeFilter optional filter for ref types to be returned or null for no filter
	 * @param targetTypeFilter optional filter for target types to be returned or null for no filter
	 * @return conversion result
	 */
	private JNAHtmlConversionResult internalRenderDocumentOrItemToHTML(JNAHtmlConverterBuilder config, Set refTypeFilter,
			Map> targetTypeFilter) {
		Document doc = config.doc;
		
		if (doc != null && !(doc instanceof JNADocument)) {
			throw new IllegalArgumentException("Document must be a JNADocument");
		}
		
		DHANDLE noteHandle = null;
		if(doc instanceof JNADocument) {
			@SuppressWarnings("resource")
			JNADocument jnaDoc = (JNADocument) doc;
			if (jnaDoc.isDisposed()) {
				throw new ObjectDisposedException(jnaDoc);
			}
			if (jnaDoc.isEncrypted()) {
				throw new DominoException("The document is encrypted. Please decrypt the document first before rendering it as HTML.");
			}
			JNADocumentAllocations jnaDocAllocations = (JNADocumentAllocations) doc.getAdapter(APIObjectAllocations.class);
			noteHandle = jnaDocAllocations.getNoteHandle();
		}
		
		if(!(config.database instanceof JNADatabase)) {
			throw new IllegalArgumentException("Database must be a JNADatabase");
		}
		
		JNADatabase jnaDb = (JNADatabase) config.database;
		if (jnaDb.isDisposed()) {
			throw new ObjectDisposedException(jnaDb);
		}
		JNADatabaseAllocations jnaDbAllocations = (JNADatabaseAllocations) jnaDb.getAdapter(APIObjectAllocations.class);
		
		IntByReference phHTML = new IntByReference();
		phHTML.setValue(0);
		
		short result = NotesCAPI.get().HTMLCreateConverter(phHTML);
		NotesErrorUtils.checkResult(result);
		
		int hHTML = phHTML.getValue();
		
		try {
			Collection options = config.options;
			if (options != null && !options.isEmpty()) {
				result = NotesCAPI.get().HTMLSetHTMLOptions(hHTML, new StringArray(options.toArray(new String[options.size()])));
				NotesErrorUtils.checkResult(result);
			}
			String userAgent = config.userAgent;
			if(StringUtil.isNotEmpty(userAgent)) {
				Memory userAgentMem = NotesStringUtils.toLMBCS(userAgent, true);
				NotesCAPI.get().HTMLSetProperty(hHTML, 4, userAgentMem); // TODO add enumeration for the properties
			}

			String itemName = config.itemName;
			Memory itemNameMem = NotesStringUtils.toLMBCS(itemName, true);
			
			int totalLen = LockUtil.lockHandles(jnaDbAllocations.getDBHandle(), noteHandle,
					(hDbByVal, hNoteByVal) -> {
						
				short convertResult;
				if (itemName==null) {
					convertResult = NotesCAPI.get().HTMLConvertNote(hHTML, hDbByVal, hNoteByVal, 0, null);
				}
				else {
					convertResult = NotesCAPI.get().HTMLConvertItem(hHTML, hDbByVal, hNoteByVal, itemNameMem);
				}
				NotesErrorUtils.checkResult(convertResult);
				
				try(DisposableMemory tLenMem = new DisposableMemory(4)) {
    				short getPropResult = NotesCAPI.get().HTMLGetProperty(hHTML, NotesConstants.HTMLAPI_PROP_TEXTLENGTH, tLenMem);
    				NotesErrorUtils.checkResult(getPropResult);
    				
    				return tLenMem.getInt(0);
				}
			});
			
			IntByReference len = new IntByReference();
			int startOffset=0;
			int bufSize = 4000;
			int iLen = bufSize;
			
			byte[] bufArr = new byte[bufSize];
			
			ByteArrayOutputStream htmlTextLMBCSOut = new ByteArrayOutputStream();
			
			try(DisposableMemory textMem = new DisposableMemory(bufSize+1)) {
				while (result==0 && iLen>0 && startOffset 0) {
						textMem.read(0, bufArr, 0, iLen);
						htmlTextLMBCSOut.write(bufArr, 0, iLen);

						startOffset += iLen;
					}
				}
			}

			String htmlText = NotesStringUtils.fromLMBCS(htmlTextLMBCSOut.toByteArray());
			
			
			int iRefCount;
            try(DisposableMemory refCount = new DisposableMemory(4)) {
              result=NotesCAPI.get().HTMLGetProperty(hHTML, NotesConstants.HTMLAPI_PROP_NUMREFS, refCount);
              NotesErrorUtils.checkResult(result);
              iRefCount = refCount.getInt(0);
            }

			List references = new ArrayList<>();
			
			for (int i=0; i> targets = new ArrayList<>(nTargets);
    						
    						for (int t=0; t targetTypeFilterForRefType = targetTypeFilter==null ? null : targetTypeFilter.get(refType);
    							
    							if (targetTypeFilterForRefType==null || targetTypeFilterForRefType.contains(targetType)) {
    								switch (currTarget.ReferenceType) {
    								case NotesConstants.URT_Name:
    									currTarget.Value.setType(Pointer.class);
    									currTarget.Value.read();
    									String name = NotesStringUtils.fromLMBCS(currTarget.Value.name, -1);
    									targets.add(new HtmlApiUrlTargetComponent<>(targetType, String.class, name));
    									break;
    								case NotesConstants.URT_NoteId:
    									currTarget.Value.setType(NoteIdStruct.class);
    									currTarget.Value.read();
    									NoteIdStruct noteIdStruct = currTarget.Value.nid;
    									int iNoteId = noteIdStruct.nid;
    									targets.add(new HtmlApiUrlTargetComponent<>(targetType, Integer.class, iNoteId));
    									break;
    								case NotesConstants.URT_Unid:
    									currTarget.Value.setType(NotesUniversalNoteIdStruct.class);
    									currTarget.Value.read();
    									NotesUniversalNoteIdStruct unidStruct = currTarget.Value.unid;
    									unidStruct.read();
    									String unid = unidStruct.toString();
    									targets.add(new HtmlApiUrlTargetComponent<>(targetType, String.class, unid));
    									break;
    								case NotesConstants.URT_None:
    									targets.add(new HtmlApiUrlTargetComponent<>(targetType, Object.class, null));
    									break;
    								case NotesConstants.URT_RepId:
    									//TODO find out how to decode this one
    									break;
    								case NotesConstants.URT_Special:
    									//TODO find out how to decode this one
    									break;
    								default:
    			                      //TODO: is there anything to do
    								}
    							}
    						}
    						
    						IHtmlApiReference newRef = new HTMLApiReference(refType, refText, fragment,
    								cmdId, targets);
    						references.add(newRef);
    					}
    				}
    				finally {
    					if (hRef!=0) {
    						Mem.OSMemoryUnlock(hRef);
    						Mem.OSMemoryFree(hRef);
    					}
    				}
				}
			}
			
			return new JNAHtmlConversionResult(doc, htmlText, references, options);
		}
		finally {
			if (hHTML!=0) {
				result = NotesCAPI.get().HTMLDestroyConverter(hHTML);
			}
			NotesErrorUtils.checkResult(result);
		}
	}

	private static class HtmlApiUrlTargetComponent implements IHtmlApiUrlTargetComponent {
		private TargetType m_type;
		private Class m_valueClazz;
		private T m_value;
		
		private HtmlApiUrlTargetComponent(TargetType type, Class valueClazz, T value) {
			m_type = type;
			m_valueClazz = valueClazz;
			m_value = value;
		}
		
		@Override
		public TargetType getType() {
			return m_type;
		}

		@Override
		public Class getValueClass() {
			return m_valueClazz;
		}

		@Override
		public T getValue() {
			return m_value;
		}
	}
	
	private static class HTMLApiReference implements IHtmlApiReference {
		private ReferenceType m_type;
		private String m_refText;
		private String m_fragment;
		private CommandId m_commandId;
		private List> m_targets;
		private Map> m_targetByType;
		
		private HTMLApiReference(ReferenceType type, String refText, String fragment, CommandId commandId,
				List> targets) {
			m_type = type;
			m_refText = refText;
			m_fragment = fragment;
			m_commandId = commandId;
			m_targets = targets;
		}
		
		@Override
		public ReferenceType getType() {
			return m_type;
		}

		@Override
		public String getReferenceText() {
			return m_refText;
		}

		@Override
		public String getFragment() {
			return m_fragment;
		}

		@Override
		public CommandId getCommandId() {
			return m_commandId;
		}

		@Override
		public List> getTargets() {
			return m_targets;
		}

		@Override
		public IHtmlApiUrlTargetComponent getTargetByType(TargetType type) {
			if (m_targetByType==null) {
				m_targetByType = new HashMap<>();
				if (m_targets!=null && !m_targets.isEmpty()) {
					for (IHtmlApiUrlTargetComponent currTarget : m_targets) {
						m_targetByType.put(currTarget.getType(), currTarget);
					}
				}
			}
			return m_targetByType.get(type);
		}
	}
	
	/**
	 * Implementation of {@link IHtmlConversionResult} that contains the HTML conversion result
	 * 
	 * @author Karsten Lehmann
	 */
	private class JNAHtmlConversionResult implements HtmlConversionResult {
		private Document m_doc;
		private String m_html;
		private List m_references;
		private Collection m_options;
		
		private JNAHtmlConversionResult(Document doc, String html, List references, Collection options) {
			m_doc = doc;
			m_html = html;
			m_references = references;
			m_options = options;
		}
		
		@Override
		public  T getAdapter(Class clazz) {
			return null;
		}
		
		@Override
		public String getHtml() {
			return m_html;
		}

		@SuppressWarnings("unused")
		public List getReferences() {
			return m_references;
		}
		
		private EmbeddedImage createImageRef(final String refText, final String fieldName, final int itemIndex,
				final int itemOffset, final String format) {
			return new EmbeddedImage() {
				
				@Override
				public void readImage(HTMLImageReader callback) {
					convertHtmlElement(m_doc, this, callback);
				}
				
				@Override
				public void writeImage(Path path) throws IOException {
					if (Files.exists(path)) {
						Files.delete(path);
					}
						
					final IOException[] ex = new IOException[1];
					try(OutputStream fOut = Files.newOutputStream(path, StandardOpenOption.TRUNCATE_EXISTING)) {
						convertHtmlElement(m_doc, this, new HTMLImageReader() {

							@Override
							public int setSize(int size) {
								return 0;
							}

							@Override
							public Action read(byte[] data) {
								try {
									fOut.write(data);
									return Action.Continue;
								} catch (IOException e) {
									ex[0] = e;
									return Action.Stop;
								}
							}
						});
						
						if (ex[0]!=null) {
							throw ex[0];
						}
					}
				}
				
				@Override
				public void writeImage(final OutputStream out) throws IOException {
					final IOException[] ex = new IOException[1];

					convertHtmlElement(m_doc, this, new HTMLImageReader() {

						@Override
						public int setSize(int size) {
							return 0;
						}

						@Override
						public Action read(byte[] data) {
							try {
								out.write(data);
								return Action.Continue;
							} catch (IOException e) {
								ex[0] = e;
								return Action.Stop;
							}
						}
					});
					
					if (ex[0]!=null) {
						throw ex[0];
					}
					
					out.flush();
				}
				
				@Override
				public String getImageSrcAttr() {
					return refText;
				}
				
				@Override
				public Collection getOptions() {
					return m_options;
				}
				
				@Override
				public int getItemOffset() {
					return itemOffset;
				}
				
				@Override
				public String getItemName() {
					return fieldName;
				}
				
				@Override
				public int getItemIndex() {
					return itemIndex;
				}
				
				@Override
				public String getFormat() {
					return format;
				}
			};
		}
		
		@Override
		public List getImages() {
			List imageRefs = new ArrayList<>();
			
			for (IHtmlApiReference currRef : m_references) {
				if (currRef.getType() == ReferenceType.IMG) {
					String refText = currRef.getReferenceText();
					String format = "gif"; //$NON-NLS-1$
					int iFormatPos = refText.indexOf("FieldElemFormat="); //$NON-NLS-1$
					if (iFormatPos!=-1) {
						String remainder = refText.substring(iFormatPos + "FieldElemFormat=".length()); //$NON-NLS-1$
						int iNextDelim = remainder.indexOf('&');
						if (iNextDelim==-1) {
							format = remainder;
						}
						else {
							format = remainder.substring(0, iNextDelim);
						}
					}
					
					IHtmlApiUrlTargetComponent fieldOffsetTarget = currRef.getTargetByType(TargetType.FIELDOFFSET);
					if (fieldOffsetTarget!=null) {
						Object fieldOffsetObj = fieldOffsetTarget.getValue();
						if (fieldOffsetObj instanceof String) {
							String fieldOffset = (String) fieldOffsetObj;
							// 1.3E -> index=1, offset=63
							int iPos = fieldOffset.indexOf('.');
							if (iPos!=-1) {
								String indexStr = fieldOffset.substring(0, iPos);
								String offsetStr = fieldOffset.substring(iPos+1);
								
								int itemIndex = Integer.parseInt(indexStr, 10); // this one is in decimal format...
								int itemOffset = Integer.parseInt(offsetStr, 16); // this one is in hex format...
								
								IHtmlApiUrlTargetComponent fieldTarget = currRef.getTargetByType(TargetType.FIELD);
								if (fieldTarget!=null) {
									Object fieldNameObj = fieldTarget.getValue();
									String fieldName = (fieldNameObj instanceof String) ? (String) fieldNameObj : null;
									
									EmbeddedImage newImgRef = createImageRef(refText, fieldName, itemIndex, itemOffset, format);
									imageRefs.add(newImgRef);
								}
							}
							
						}
					}
				}
			}
			
			return imageRefs;
		};
		
	}

	/**
	 * Convenience method to read the binary data of a {@link EmbeddedImage}
	 * 
	 * @param doc document
	 * @param image image reference
	 * @param callback callback to receive the data
	 */
	private void convertHtmlElement(Document doc, EmbeddedImage image, HTMLImageReader callback) {
		String itemName = image.getItemName();
		int itemIndex = image.getItemIndex();
		int itemOffset = image.getItemOffset();
		Collection options = image.getOptions();
		
		readEmbeddedImage(doc, itemName, options, itemIndex, itemOffset, callback);
	}
	
	@Override
	public void readEmbeddedImage(Document doc, String itemName, Collection options, int itemIndex, int itemOffset, HTMLImageReader callback) {
		if (!(doc instanceof JNADocument)) {
			throw new IllegalArgumentException("Document must be a JNADocument");
		}
		JNADocument jnaDoc = (JNADocument) doc;
		if (jnaDoc.isDisposed()) {
			throw new DominoException("Document is diposed");
		}
		if (jnaDoc.isEncrypted()) {
			throw new DominoException("The document is encrypted. Please decrypt the document first before accessing embedded images.");
		}

		JNADocumentAllocations jnaDocAllocations = (JNADocumentAllocations) jnaDoc.getAdapter(APIObjectAllocations.class);
		
		JNADatabase jnaDb = (JNADatabase) jnaDoc.getParentDatabase();
		if (jnaDb.isDisposed()) {
			throw new ObjectDisposedException(jnaDb);
		}
		JNADatabaseAllocations jnaDbAllocations = (JNADatabaseAllocations) jnaDb.getAdapter(APIObjectAllocations.class);
		
		IntByReference phHTML = new IntByReference();
		
		short result = NotesCAPI.get().HTMLCreateConverter(phHTML);
		NotesErrorUtils.checkResult(result);
		
		int hHTML = phHTML.getValue();
		
		try {
			if (options != null && !options.isEmpty()) {
				result = NotesCAPI.get().HTMLSetHTMLOptions(hHTML, new StringArray(options.toArray(new String[options.size()])));
				NotesErrorUtils.checkResult(result);
			}

			Memory itemNameMem = NotesStringUtils.toLMBCS(itemName, true);

			
			int totalLen = LockUtil.lockHandles(jnaDbAllocations.getDBHandle(), jnaDocAllocations.getNoteHandle(), (hDbByVal, hNoteByVal) -> {
				short convertResult = NotesCAPI.get().HTMLConvertElement(hHTML, hDbByVal, hNoteByVal, itemNameMem, itemIndex, itemOffset);
				NotesErrorUtils.checkResult(convertResult);
				
				try(DisposableMemory tLenMem = new DisposableMemory(4)) {
    				short getPropResult = NotesCAPI.get().HTMLGetProperty(hHTML, NotesConstants.HTMLAPI_PROP_TEXTLENGTH, tLenMem);
    				NotesErrorUtils.checkResult(getPropResult);
    				
    				return tLenMem.getInt(0);
				}
			});
			int skip = callback.setSize(totalLen);
			

			if (skip > totalLen) {
				throw new IllegalArgumentException(MessageFormat.format("Skip value cannot be greater than size: {0} > {1}", skip, totalLen));
			}
			
			IntByReference len = new IntByReference();
			len.setValue(NotesConstants.MAXPATH);
			int startOffset=skip;
			try(DisposableMemory bufMem = new DisposableMemory(NotesConstants.MAXPATH+1)) {
    			while (result==0 && len.getValue()>0 && startOffset




© 2015 - 2025 Weber Informatics LLC | Privacy Policy