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

com.hcl.domino.jna.internal.NotesStringUtils 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.internal;

import static java.text.MessageFormat.format;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Formatter;
import java.util.List;

import com.hcl.domino.DominoException;
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.jna.internal.capi.INotesCAPI;
import com.hcl.domino.jna.internal.capi.NotesCAPI;
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.ptr.PointerByReference;

/**
 * String conversion functions between Java and LMBCS
 * 
 * @author Karsten Lehmann
 */
public class NotesStringUtils {
	private static final String PREF_USEOSLINEBREAK = "NotesStringUtils.useOSLineDelimiter"; //$NON-NLS-1$
	
	//use simple cache for string-lmbcs conversion of short string
	private static final boolean USE_STRING2LMBCS_CACHE = true;
	//max length of each string-lmbcs cache entry in characters
	private static final int MAX_STRING2LMBCS_KEY_LENGTH = 500;
	
	private static final int MAX_STRING2LMBCS_SIZE_BYTES = 1000000;
	
	private static LRUStringLMBCSCache m_string2LMBCSCache_NullTerminated_LinefeedLinebreaks = new LRUStringLMBCSCache(MAX_STRING2LMBCS_SIZE_BYTES);
	private static LRUStringLMBCSCache m_string2LMBCSCache_NotNullTerminated_LinefeedLinebreaks = new LRUStringLMBCSCache(MAX_STRING2LMBCS_SIZE_BYTES);
	
	private static LRUStringLMBCSCache m_string2LMBCSCache_NullTerminated_NullLinebreaks = new LRUStringLMBCSCache(MAX_STRING2LMBCS_SIZE_BYTES);
	private static LRUStringLMBCSCache m_string2LMBCSCache_NotNullTerminated_NullLinebreaks = new LRUStringLMBCSCache(MAX_STRING2LMBCS_SIZE_BYTES);

	private static LRUStringLMBCSCache m_string2LMBCSCache_NullTerminated_OriginalLinebreaks = new LRUStringLMBCSCache(MAX_STRING2LMBCS_SIZE_BYTES);
	private static LRUStringLMBCSCache m_string2LMBCSCache_NotNullTerminated_OriginalLinebreaks = new LRUStringLMBCSCache(MAX_STRING2LMBCS_SIZE_BYTES);
	
	private static final Charset charsetUTF8 = StandardCharsets.UTF_8;

	public static void flushCache() {
		m_string2LMBCSCache_NullTerminated_LinefeedLinebreaks.clear();
		m_string2LMBCSCache_NotNullTerminated_LinefeedLinebreaks.clear();
		m_string2LMBCSCache_NullTerminated_NullLinebreaks.clear();
		m_string2LMBCSCache_NotNullTerminated_NullLinebreaks.clear();
		m_string2LMBCSCache_NullTerminated_OriginalLinebreaks.clear();
		m_string2LMBCSCache_NotNullTerminated_OriginalLinebreaks.clear();
	}
	
	/**
	 * Method to control the LMBCS / Java String conversion for newline characters. By default
	 * we insert \r\n on Windows and \n on other platforms like IBM does.
* * @param b true to use \r\n as newline on Windows, false to use \n everywhere */ @SuppressWarnings("unused") private static void setUseOSLineDelimiter(boolean b) { if (isUseOSLineDelimiter() != b) { // NotesGC.setCustomValue(PREF_USEOSLINEBREAK, Boolean.valueOf(b)); //remove all cached values that contain newlines List keysWithNull = m_string2LMBCSCache_NullTerminated_LinefeedLinebreaks.getKeys(); for (String currKey : keysWithNull) { if (currKey.indexOf('\n') != -1) { m_string2LMBCSCache_NullTerminated_LinefeedLinebreaks.remove(currKey); } } keysWithNull = m_string2LMBCSCache_NullTerminated_NullLinebreaks.getKeys(); for (String currKey : keysWithNull) { if (currKey.indexOf('\n') != -1) { m_string2LMBCSCache_NullTerminated_NullLinebreaks.remove(currKey); } } keysWithNull = m_string2LMBCSCache_NullTerminated_OriginalLinebreaks.getKeys(); for (String currKey : keysWithNull) { if (currKey.indexOf('\n') != -1) { m_string2LMBCSCache_NullTerminated_OriginalLinebreaks.remove(currKey); } } List keysWithoutNull = m_string2LMBCSCache_NotNullTerminated_LinefeedLinebreaks.getKeys(); for (String currKey : keysWithoutNull) { if (currKey.indexOf('\n') != -1) { m_string2LMBCSCache_NotNullTerminated_LinefeedLinebreaks.remove(currKey); } } keysWithoutNull = m_string2LMBCSCache_NotNullTerminated_NullLinebreaks.getKeys(); for (String currKey : keysWithoutNull) { if (currKey.indexOf('\n') != -1) { m_string2LMBCSCache_NotNullTerminated_NullLinebreaks.remove(currKey); } } keysWithoutNull = m_string2LMBCSCache_NotNullTerminated_OriginalLinebreaks.getKeys(); for (String currKey : keysWithoutNull) { if (currKey.indexOf('\n') != -1) { m_string2LMBCSCache_NotNullTerminated_OriginalLinebreaks.remove(currKey); } } } } /** * Returns whether an OS specific newline is used when converting between LMBCS and Java String. * By default we insert \r\n on Windows and \n on other platforms like IBM does. * * @return true to use \r\n as newline on Windows, false to use \n everywhere */ public static boolean isUseOSLineDelimiter() { return true; // check if this has any use /* Boolean b = (Boolean) NotesGC.getCustomValue(PREF_USEOSLINEBREAK); if (b==null) return Boolean.TRUE; else return b.booleanValue(); */ } /** * Scans the byte array for null values * * @param bytes array of bytes * @return number of bytes before null byte in array */ public static int getNullTerminatedLength(byte[] bytes) { if (bytes == null) { return 0; } int textLen = bytes.length; //search for terminating null character for (int i=0; i fromLMBCSStringList(Pointer inPtr, int numEntries) { List stringList = new ArrayList<>(); Pointer ptrStartOfString = inPtr; for (int i=0; i lines = new ArrayList<>(); INotesCAPI api = NotesCAPI.get(); //output buffer shared across loop runs for each line DisposableMemory outBufUTF8 = null; try { for (int i=0; i0) { if (lineBreakConversion == LineBreakConversion.NULL) { //replace line breaks with null characters bOut.write(0); } else if (lineBreakConversion == LineBreakConversion.LINEFEED) { //replace line breaks (e.g. \r\n on Windows) with \n bOut.write('\n'); } else { //should not happen throw new IllegalArgumentException(format("Unexpected line break conversion: {0}", lineBreakConversion)); } } if (lines[i].length() == 0) { continue; } //check if string only contains ascii characters that map 1:1 to LMBCS; //in this case we can skip the OSTranslate call boolean isPureAscii = true; for (int x=0; x= 0x80) { isPureAscii = false; break; } } byte[] lineDataAsUTF8 = lines[i].getBytes(charsetUTF8); if (isPureAscii) { try { bOut.write(lineDataAsUTF8); } catch (IOException e) { throw new DominoException(0, "Error writing to temporary byte stream", e); } } else { int worstCaseLMBCSLength = 3 * lines[i].length(); if (inputBufUTF8!=null && inputBufUTF8.size() < lineDataAsUTF8.length) { inputBufUTF8.close(); inputBufUTF8 = null; } if (inputBufUTF8==null) { inputBufUTF8 = new DisposableMemory(lineDataAsUTF8.length); } inputBufUTF8.write(0, lineDataAsUTF8, 0, lineDataAsUTF8.length); if (outputBufLMBCS!=null && outputBufLMBCS.size() < worstCaseLMBCSLength) { outputBufLMBCS.close(); outputBufLMBCS = null; } if (outputBufLMBCS==null) { outputBufLMBCS = new DisposableMemory(worstCaseLMBCSLength); } do { int retOutBufLength = api.OSTranslate32( NotesConstants.OS_TRANSLATE_UTF8_TO_LMBCS, inputBufUTF8, lineDataAsUTF8.length, outputBufLMBCS, (int) outputBufLMBCS.size()); if (retOutBufLength==outputBufLMBCS.size()) { // output buffer not large enough, increase it and retry (not expected to happen because of // our worst case computation) long oldOutBufSize = outputBufLMBCS.size(); long newOutBufSize = (long) (((double) oldOutBufSize)*2); outputBufLMBCS.close(); outputBufLMBCS = new DisposableMemory(newOutBufSize); continue; } else if (retOutBufLength>0) { //success byte[] data = outputBufLMBCS.getByteArray(0, retOutBufLength); try { bOut.write(data); } catch (IOException e) { throw new DominoException(0, "Error writing to temporary byte stream", e); } break; } } while (true); } } } finally { if (inputBufUTF8!=null) { inputBufUTF8.close(); } if (outputBufLMBCS!=null) { outputBufLMBCS.close(); } } if (addNull) { int limit = bOut.size(); Memory m; if (noCache) { m = new DisposableMemory(limit + 1); } else { m = new ReadOnlyMemory(limit + 1); } byte[] data = bOut.toByteArray(); m.write(0, data, 0, data.length); m.setByte(limit, (byte) 0); if (!noCache) { ((ReadOnlyMemory)m).seal(); if (USE_STRING2LMBCS_CACHE && inStr.length()<=MAX_STRING2LMBCS_KEY_LENGTH) { if (cacheToUse!=null) { cacheToUse.put(inStr, m); } } } return m; } else { Memory m; if (noCache) { m = new DisposableMemory(bOut.size()); } else { m = new ReadOnlyMemory(bOut.size()); } byte[] data = bOut.toByteArray(); m.write(0, data, 0, data.length); if (!noCache) { ((ReadOnlyMemory)m).seal(); if (USE_STRING2LMBCS_CACHE && inStr.length()<=MAX_STRING2LMBCS_KEY_LENGTH) { if (cacheToUse!=null) { cacheToUse.put(inStr, m); } } } return m; } } /** * Converts the provided string collection into an adjacent series of null-terminated * LMBCS strings in memory. * * @param strings the {@link Collection} of strings to convert * @return a {@link Memory} object pointing to the strings in memory */ public static Memory toLMBCS(Collection strings) { if(strings == null || strings.isEmpty()) { return toLMBCS("", true); //$NON-NLS-1$ } int count = strings.size(); List allocated = new ArrayList<>(count); for(String s : strings) { allocated.add(toLMBCS(s, true)); } // Total size of converted LMBCS long totalSize = allocated.stream().mapToLong(Memory::size).sum(); Memory result = new Memory(totalSize); long offset = 0; for(int i = 0; i < count; i++) { int size = (int)allocated.get(i).size(); byte[] lmbcs = allocated.get(i).getByteArray(0, size); result.write(offset, lmbcs, 0, size); offset += size; } return result; } /** * Converts bytes in memory to a UNID * * @param innardsFile innards of file part * @param innardsNote innards of note part * @return unid */ public static String toUNID(long innardsFile, long innardsNote) { Formatter formatter = new Formatter(); formatter.format("%016x", innardsFile); //$NON-NLS-1$ formatter.format("%016x", innardsNote); //$NON-NLS-1$ String unid = formatter.toString().toUpperCase(); formatter.close(); return unid; } /** * Reads a UNID from memory * * @param ptr memory * @return UNID as string */ public static String pointerToUnid(Pointer ptr) { Formatter formatter = new Formatter(); ByteBuffer data = ptr.getByteBuffer(0, 16).order(ByteOrder.LITTLE_ENDIAN); formatter.format("%016x", data.getLong()); //$NON-NLS-1$ formatter.format("%016x", data.getLong()); //$NON-NLS-1$ String unidStr = formatter.toString().toUpperCase(); formatter.close(); return unidStr; } /** * Writes a UNID string to memory * * @param unidStr UNID string * @param target target memory */ public static void unidToPointer(String unidStr, Pointer target) { try { int fileInnards1 = (int) (Long.parseLong(unidStr.substring(0,8), 16) & 0xffffffff); int fileInnards0 = (int) (Long.parseLong(unidStr.substring(8,16), 16) & 0xffffffff); int noteInnards1 = (int) (Long.parseLong(unidStr.substring(16,24), 16) & 0xffffffff); int noteInnards0 = (int) (Long.parseLong(unidStr.substring(24,32), 16) & 0xffffffff); target.setInt(0, fileInnards0); target.share(4).setInt(0, fileInnards1); target.share(8).setInt(0, noteInnards0); target.share(12).setInt(0, noteInnards1); } catch (Exception e) { throw new DominoException(0, format("Could not convert UNID to memory: {0}", unidStr), e); } } /** * This function takes a port name, a server name, and file path relative to the Domino or * Notes data directory and creates a full network path specification for a Domino database * file.
*
* To open a Domino database on a server, use this function to create the full path specification, * and pass this specification as input to NSFDbOpen or NSFDbOpenExtended. * * @param portName network port name or NULL to allow Domino or Notes to use the "most available" port to the given server * @param serverName Name of the server (either in abbreviated format, canonical format or as common name) or "" for local * @param fileName filename of the Domino database you with to access, relative to the data directory * @return fully qualified network path */ public static String osPathNetConstruct(String portName, String serverName, String fileName) { Memory portNameMem = toLMBCS(portName, true); Memory serverNameMem = toLMBCS(serverName, true); Memory fileNameMem = toLMBCS(fileName, true); try(DisposableMemory retPathMem = new DisposableMemory(NotesConstants.MAXPATH)) { short result = NotesCAPI.get().OSPathNetConstruct(portNameMem, serverNameMem, fileNameMem, retPathMem); NotesErrorUtils.checkResult(result); return fromLMBCS(retPathMem, getNullTerminatedLength(retPathMem)); } } /** * Given a fully-qualified network path to a Domino database file, this function breaks it * into its port name, server name, and filename components.
* If the fully qualified path contains just the port name and/or server name components, * then they will be the only ones returned.
*
* Expanded database filepath syntax:
*
* {Port} NetworkSeparator {servername} Serversuffix {filename}
* COM! {NetworkSeparator} NOTESBETA {ServerSuffix} NOTEFILE\APICOMMS.NSF
*
* Note: the NetworkSeparator and ServerSuffix are not system independent. To maintain the * portability of your code, it is recommended that you make no explicit use of them * anywhere in your programs. * * @param pathName expanded path specification of a Domino database file * @return String array of portname, servername, filename */ public static String[] osPathNetParse(String pathName) { try(DisposableMemory retPortNameMem = new DisposableMemory(NotesConstants.MAXPATH); DisposableMemory retServerNameMem = new DisposableMemory(NotesConstants.MAXPATH); DisposableMemory retFileNameMem = new DisposableMemory(NotesConstants.MAXPATH)) { Memory pathNameMem = toLMBCS(pathName, true); short result = NotesCAPI.get().OSPathNetParse(pathNameMem, retPortNameMem, retServerNameMem, retFileNameMem); NotesErrorUtils.checkResult(result); String portName = fromLMBCS(retPortNameMem, getNullTerminatedLength(retPortNameMem)); String serverName = fromLMBCS(retServerNameMem, getNullTerminatedLength(retServerNameMem)); String fileName = fromLMBCS(retFileNameMem, getNullTerminatedLength(retFileNameMem)); return new String[] {portName, serverName, fileName}; } } /** * Converts an innards array to hex format, e.g. used for replica ids * * @param innards innards array with two elements * @return replica id (16 character hex string) */ public static String innardsToReplicaId(int[] innards) { return StringUtil.pad(Integer.toHexString(innards[1]).toUpperCase(), 8, '0', false) + StringUtil.pad(Integer.toHexString(innards[0]).toUpperCase(), 8, '0', false); } /** * Converts a replica id to an innards array * * @param replicaId replica id * @return innards array with two elements */ public static int[] replicaIdToInnards(String replicaId) { if (replicaId.contains(":")) { //$NON-NLS-1$ replicaId = replicaId.replace(":", ""); //$NON-NLS-1$ //$NON-NLS-2$ } if (replicaId.length() != 16) { throw new IllegalArgumentException("Replica ID is expected to have 16 hex characters or 8:8 format"); } int[] innards = new int[2]; innards[1] = (int) (Long.parseLong(replicaId.substring(0,8), 16) & 0xffffffff); innards[0] = (int) (Long.parseLong(replicaId.substring(8), 16) & 0xffffffff); return innards; } private static class LRUStringLMBCSCache extends SizeLimitedLRUCache { public LRUStringLMBCSCache(int maxSizeUnits) { super(maxSizeUnits); } @Override protected int computeSize(String key, Memory value) { return key.length()*2 + (int) value.size(); } } /** * Converts a Java string to LMBCS and splits it into chunks of the specified * max length. * * @param txt text to convert and split * @param addNull true to add a null terminator * @param replaceLinebreaks true to replace linebreaks with \0 * @param chunkSize max size of chunks (might be less for LMBCS with multibyte sequences), must be greater than 4 * @return list of chunks */ public static List splitAsLMBCS(String txt, boolean addNull, boolean replaceLinebreaks, int chunkSize) { Memory mem = toLMBCS(txt, addNull, replaceLinebreaks); return splitLMBCS(mem, mem.size(), chunkSize); } /** * Splits an LMBCS string into chunks of the specified max length * * @param txtPtr pointer to LMBCS string * @param txtSize length of string * @param chunkSize max size of chunks (might be less for LMBCS with multibyte sequences), must be greater than 4 * @return list of chunks */ public static List splitLMBCS(Pointer txtPtr, long txtSize, int chunkSize) { if (chunkSize<5) { throw new IllegalArgumentException("Chunk size must be greater than 4"); } if (txtSize < chunkSize) { return Arrays.asList(txtPtr.getByteBuffer(0, txtSize)); } Pointer nlsInfoPtr = NotesCAPI.get().OSGetLMBCSCLS(); List chunks = new ArrayList<>(); long offset = 0; ByteBuffer wholeByteBuf = txtPtr.getByteBuffer(offset, txtSize); while (true) { long nextOffset = offset + chunkSize + 1; if (nextOffset > txtSize) { //add last chunk wholeByteBuf.position((int) offset); ByteBuffer lastByteBuf = wholeByteBuf.slice(); lastByteBuf.position(0); lastByteBuf.limit((int) (txtSize - offset)); chunks.add(lastByteBuf); break; } //end NLS_goto_prev_whole_char at current offset Pointer ppStart = txtPtr.share(offset); //set pointer to offset where we want to split PointerByReference ppString = new PointerByReference(); ppString.setValue(txtPtr.share(nextOffset)); //make sure we are at the beginning of the character for multibyte NotesCAPI.get().NLS_goto_prev_whole_char(ppString, ppStart, nlsInfoPtr); long correctedNextOffset = Pointer.nativeValue(ppString.getValue()) - Pointer.nativeValue(txtPtr); long actualChunkSize = correctedNextOffset - offset; if (actualChunkSize==0) { break; } //extract chunk data from bytebuffer wholeByteBuf.position((int) offset); ByteBuffer chunkByteBuf = wholeByteBuf.slice(); chunkByteBuf.position(0); chunkByteBuf.limit((int) actualChunkSize); chunks.add(chunkByteBuf); //start next scan at corrected offset offset = correctedNextOffset; } return chunks; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy