com.hcl.domino.jna.internal.NotesStringUtils Maven / Gradle / Ivy
/*
* ==========================================================================
* 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