Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.hcl.domino.jnx.jsonb.DocumentJsonbSerializer 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.jnx.jsonb;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Formatter;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.Spliterator;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.function.BiFunction;
import java.util.stream.StreamSupport;
import org.apache.commons.io.IOUtils;
import com.hcl.domino.DominoException;
import com.hcl.domino.commons.json.AbstractJsonSerializer;
import com.hcl.domino.commons.json.JsonUtil;
import com.hcl.domino.data.Document;
import com.hcl.domino.data.DocumentClass;
import com.hcl.domino.data.DominoDateRange;
import com.hcl.domino.data.DominoDateTime;
import com.hcl.domino.data.DominoOriginatorId;
import com.hcl.domino.data.DominoTimeType;
import com.hcl.domino.data.ItemDataType;
import com.hcl.domino.exception.EntryNotFoundInIndexException;
import com.hcl.domino.exception.ItemNotFoundException;
import com.hcl.domino.html.HtmlConversionResult;
import com.hcl.domino.html.HtmlConvertOption;
import com.hcl.domino.html.RichTextHTMLConverter;
import com.hcl.domino.json.DateRangeFormat;
import com.hcl.domino.json.JsonSerializer;
import com.hcl.domino.mime.MimeReader.ReadMimeDataType;
import jakarta.json.bind.serializer.JsonbSerializer;
import jakarta.json.bind.serializer.SerializationContext;
import jakarta.json.stream.JsonGenerator;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
public class DocumentJsonbSerializer implements JsonbSerializer {
public static class Builder {
private Collection skippedItemNames;
private Collection includedItemNames;
private Collection excludedTypes;
private boolean lowercaseProperties;
private boolean includeMetadata;
private Collection booleanItemNames;
private Collection booleanTrueValues;
private DateRangeFormat dateRangeFormat = DateRangeFormat.ISO;
private Map htmlConvertOptions;
protected Map> customProcessors = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
private boolean metaOnly;
private Builder() {
}
/**
* Sets the item names that should be considered boolean values when serialized
* to JSON.
*
* Items of type {@link ItemDataType#TYPE_TEXT TEXT},
* {@link ItemDataType#TYPE_NUMBER NUMBER}, and
* {@link ItemDataType#TYPE_RFC822_TEXT RFC822_TEXT}, as well as
* single-value items of type {@link ItemDataType#TYPE_TEXT_LIST TEXT_LIST} or
* {@link ItemDataType#TYPE_NUMBER_RANGE NUMBER_RANGE} are evaluated for boolean
* conversion.
*
*
* @param booleanItemNames the names of items to serialize as boolean
* @return this builder
*/
public Builder booleanItemNames(final Collection booleanItemNames) {
this.booleanItemNames = booleanItemNames;
return this;
}
/**
* Sets the values used to determine {@code true} and {@code false} when
* serializing items configured
* via {@link #booleanItemNames(Collection)}.
*
* All values not specified here will be considered {@code false}.
*
*
* @param trueValues a collection of objects to compare to resolve as
* {@code true}
* @return this builder
*/
public Builder booleanTrueValues(final Collection trueValues) {
this.booleanTrueValues = trueValues;
return this;
}
/**
* Constructs a new {@link DocumentJsonbSerializer} based on the configuration
* of this builder.
*
* @return the newly-constructed serializer
*/
public DocumentJsonbSerializer build() {
final DocumentJsonbSerializer result = new DocumentJsonbSerializer();
result.skippedItemNames = JsonbUtil.toSet(this.skippedItemNames);
result.includedItemNames = JsonbUtil.toSet(this.includedItemNames);
if (this.excludedTypes != null) {
result.excludedTypes = EnumSet.copyOf(this.excludedTypes);
}
result.lowercaseProperties = this.lowercaseProperties;
result.includeMetadata = this.includeMetadata;
result.booleanItemNames = JsonUtil.toInsensitiveSet(this.booleanItemNames);
result.booleanTrueValues = this.booleanTrueValues == null ? Collections.emptySet() : this.booleanTrueValues;
result.dateRangeFormat = this.dateRangeFormat;
result.htmlConvertOptions = this.htmlConvertOptions;
result.customProcessors = this.customProcessors;
result.metaOnly = metaOnly;
return result;
}
/**
* Sets the custom processors for this serializer, which will be called when
* encountering
* item names found in the map.
*
* @param customProcessors a {@link Map} of {@link BiFunction} processors
* @return this builder
* @since 1.0.28
*/
public Builder customProcessors(final Map> customProcessors) {
this.customProcessors = customProcessors;
return this;
}
/**
* Sets the format for date/time range values.
*
* @param format the {@link DateRangeFormat} type to use
* @return this builder
*/
public Builder dateRangeFormat(final DateRangeFormat format) {
this.dateRangeFormat = format == null ? DateRangeFormat.ISO : format;
return this;
}
/**
* Exclude items by name.
*
* @param skippedItemNames a {@link Collection} of case-insensitive item names,
* or {@code null} to not
* exclude any items
* @return this builder
*/
public Builder excludeItems(final Collection skippedItemNames) {
this.skippedItemNames = skippedItemNames;
return this;
}
/**
* Exclude item types.
*
* @param excludedTypes a {@link Collection} of item types to exclude, or
* {@code null} to not exclude any
* item types
* @return this builder
*/
public Builder excludeTypes(final Collection excludedTypes) {
this.excludedTypes = excludedTypes;
return this;
}
/**
* Include only specific items by name.
*
* Note: {@link #excludeItems} is still applied if both are specified.
*
*
* @param includedItemNames a {@link Collection} of case-insensitive item names,
* or {@code null} to not
* include only specific items
* @return this builder
*/
public Builder includeItems(final Collection includedItemNames) {
this.includedItemNames = includedItemNames;
return this;
}
/**
* Sets whether to include a metadata object in the output JSON, using the
* {@value JsonSerializer#PROP_METADATA} property.
* {@code false} by default.
*
* @param includeMetadata whether to include a document-metadata object
* @return this builder
*/
public Builder includeMetadata(final boolean includeMetadata) {
this.includeMetadata = includeMetadata;
return this;
}
/**
* Sets whether property names in the emitted JSON should be lowercased. When
* {@code false} (the default),
* emitted properties will match the capitalization of the first of each named
* item in the source
* document.
*
* @param lowercaseProperties whether to lowercase property names
* @return this builder
*/
public Builder lowercaseProperties(final boolean lowercaseProperties) {
this.lowercaseProperties = lowercaseProperties;
return this;
}
/**
* Sets the options to use when converting rich text to HTML.
*
* Setting this overrides the default behavior of
* {@link HtmlConvertOption#XMLCompatibleHTML XMLCompatibleHTML=1}
*
* .
*
* @param richTextHtmlOptions the map of options to set
* @return this builder
* @since 1.0.27
*/
public Builder richTextHtmlOptions(final Map richTextHtmlOptions) {
this.htmlConvertOptions = richTextHtmlOptions;
return this;
}
/**
* Sets whether or not the serializer will emit only the {@code @meta} object
* and its attributes, as opposed to also including the full document contents.
*
* @param metaOnly {@code true} to emit only metadata; {@code false} (the default)
* to emit both meta and document data
* @return this serializer
* @since 1.11.0
*/
public Builder metaOnly(boolean metaOnly) {
this.metaOnly = metaOnly;
return this;
}
}
/**
* Creates a new serializer configuration builder.
*
* @return a new serializer builder
*/
public static Builder newBuilder() {
return new Builder();
}
/**
* Creates a new serializer with the default configuration.
*
* @return the newly-constructed serializer
*/
public static DocumentJsonbSerializer newSerializer() {
return new Builder().build();
}
private Collection skippedItemNames;
private Collection includedItemNames;
private Collection excludedTypes;
private boolean lowercaseProperties;
private boolean includeMetadata;
private Collection booleanItemNames;
private Collection booleanTrueValues;
private DateRangeFormat dateRangeFormat;
Map htmlConvertOptions;
Map> customProcessors;
private boolean metaOnly;
private DocumentJsonbSerializer() {
}
@Override
public void serialize(final Document obj, final JsonGenerator generator, final SerializationContext ctx) {
final Set handledItems = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
handledItems.addAll(JsonSerializer.DEFAULT_EXCLUDED_ITEMS);
if (this.includedItemNames != null) {
handledItems.removeAll(this.includedItemNames);
}
if (this.skippedItemNames != null) {
handledItems.addAll(this.skippedItemNames);
}
generator.writeStartObject();
if (this.includeMetadata) {
generator.writeStartObject(JsonSerializer.PROP_METADATA);
generator.write(JsonSerializer.PROP_META_NOTEID, obj.getNoteID());
generator.write(JsonSerializer.PROP_META_UNID, obj.getUNID());
generator.write(JsonSerializer.PROP_META_CREATED, JsonUtil.toIsoString(obj.getCreated()));
generator.write(JsonSerializer.PROP_META_LASTMODIFIED, JsonUtil.toIsoString(obj.getLastModified()));
generator.write(JsonSerializer.PROP_META_LASTACCESSED, JsonUtil.toIsoString(obj.getLastAccessed()));
generator.write(JsonSerializer.PROP_META_LASTMODIFIEDINFILE, JsonUtil.toIsoString(obj.getModifiedInThisFile()));
generator.write(JsonSerializer.PROP_META_ADDEDTOFILE, JsonUtil.toIsoString(obj.getAddedToFile()));
generator.writeStartArray(JsonSerializer.PROP_META_NOTECLASS);
for (final DocumentClass c : obj.getDocumentClass()) {
generator.write(c.name());
}
generator.writeEnd();
generator.write(JsonSerializer.PROP_META_UNREAD, obj.isUnread());
generator.write(JsonSerializer.PROP_META_EDITABLE, obj.isEditable());
{
DominoOriginatorId oid = obj.getOID();
int[] seqTime = oid.getSequenceTime().getAdapter(int[].class);
try(Formatter formatter = new Formatter()) {
formatter.format("%08x", oid.getSequence()); //$NON-NLS-1$
formatter.format("%08x", seqTime[0]); //$NON-NLS-1$
formatter.format("%08x", seqTime[1]); //$NON-NLS-1$
generator.write(JsonSerializer.PROP_META_REVISION, formatter.toString().toUpperCase());
}
}
if(obj.isResponse()) {
generator.write(JsonSerializer.PROP_META_PARENTUNID, obj.getParentDocumentUNID());
}
Optional threadId = obj.getThreadID();
if(threadId.isPresent()) {
generator.write(JsonSerializer.PROP_META_THREADID, threadId.get());
}
if(obj.isSigned()) {
String signer;
try {
signer = obj.getSigner();
} catch(DominoException e) {
// Likely in many cases - no cross cert, mismatched pub key, etc.
signer = e.getLocalizedMessage();
}
generator.write(JsonSerializer.PROP_META_SIGNER, signer);
}
generator.writeEnd();
}
if(!this.metaOnly) {
obj.forEachItem((item, loop) -> {
final String itemName = item.getName();
if (itemName != null && !handledItems.contains(itemName)) {
handledItems.add(itemName);
if (this.includedItemNames != null && !this.includedItemNames.contains(itemName)) {
// Skip
return;
}
final ItemDataType type = item.getType();
if ((this.excludedTypes != null && this.excludedTypes.contains(type)) || AbstractJsonSerializer.isExcludedField(itemName)) {
return;
}
final String propName = this.lowercaseProperties ? itemName.toLowerCase() : itemName;
if (this.customProcessors != null && this.customProcessors.containsKey(itemName)) {
final Object val = this.customProcessors.get(itemName).apply(obj, itemName);
this.writeArbitraryValue(generator, ctx, propName, val);
return;
}
switch (type) {
case TYPE_NUMBER: {
final double value = item.get(double.class, 0d);
if (this.booleanItemNames.contains(propName)) {
final boolean val = AbstractJsonSerializer.matchesBooleanValues(value, this.booleanTrueValues);
generator.write(propName, val);
} else {
generator.write(propName, value);
}
break;
}
case TYPE_NUMBER_RANGE: {
final List vals = item.getAsList(Double.class, Collections.emptyList());
if (this.booleanItemNames.contains(propName)) {
if (vals.size() == 1) {
final boolean val = AbstractJsonSerializer.matchesBooleanValues(vals.get(0), this.booleanTrueValues);
generator.write(propName, val);
} else {
generator.write(propName, false);
}
} else {
generator.writeStartArray(propName);
for (final Double val : vals) {
generator.write(val);
}
generator.writeEnd();
}
break;
}
case TYPE_RFC822_TEXT:
case TYPE_TEXT: {
final String val = item.get(String.class, null);
if (this.booleanItemNames.contains(propName)) {
final boolean boolVal = AbstractJsonSerializer.matchesBooleanValues(val, this.booleanTrueValues);
generator.write(propName, boolVal);
} else {
if (val == null) {
generator.writeNull(propName);
} else {
generator.write(propName, val);
}
}
break;
}
case TYPE_TEXT_LIST: {
final List vals = item.getAsList(String.class, Collections.emptyList());
if (this.booleanItemNames.contains(propName)) {
if (vals.size() == 1) {
final boolean val = AbstractJsonSerializer.matchesBooleanValues(vals.get(0), this.booleanTrueValues);
generator.write(propName, val);
} else {
generator.write(propName, false);
}
} else {
generator.writeStartArray(propName);
for (final String val : vals) {
generator.write(val);
}
generator.writeEnd();
}
break;
}
case TYPE_TIME: {
final DominoTimeType val = item.get(DominoTimeType.class, null);
this.writeTimeProperty(generator, propName, val);
break;
}
case TYPE_TIME_RANGE: {
final List vals = item.getAsList(DominoTimeType.class, Collections.emptyList());
if (vals.size() == 1) {
this.writeTimeProperty(generator, propName, vals.get(0));
} else {
generator.writeStartArray(propName);
for (final DominoTimeType val : vals) {
if (val == null) {
generator.writeNull();
} else {
if (val instanceof DominoDateTime) {
generator.write(JsonUtil.toIsoString((DominoDateTime) val));
} else {
switch (this.dateRangeFormat) {
case OBJECT:
generator.writeStartObject();
generator.write(JsonSerializer.PROP_RANGE_FROM,
JsonUtil.toIsoString(((DominoDateRange) val).getStartDateTime()));
generator.write(JsonSerializer.PROP_RANGE_TO,
JsonUtil.toIsoString(((DominoDateRange) val).getEndDateTime()));
generator.writeEnd();
break;
case ISO:
default:
generator.write(JsonUtil.toIsoString((DominoDateRange) val));
break;
}
}
}
}
generator.writeEnd();
}
break;
}
case TYPE_COMPOSITE:
try {
final RichTextHTMLConverter.Builder builder = obj.getParentDatabase()
.getParentDominoClient()
.getRichTextHtmlConverter()
.renderItem(obj, propName);
if (this.htmlConvertOptions == null || this.htmlConvertOptions.isEmpty()) {
builder.option(HtmlConvertOption.XMLCompatibleHTML, "1"); //$NON-NLS-1$
} else {
this.htmlConvertOptions.forEach(builder::option);
}
final HtmlConversionResult conv = builder.convert();
generator.write(propName, conv.getHtml());
} catch (ItemNotFoundException | EntryNotFoundInIndexException e) {
// Occurs with design notes
generator.write(propName, ""); //$NON-NLS-1$
} catch (final DominoException e) {
switch (e.getId()) {
case 14941:
case 14944:
// Un-messaged error codes observed with design notes
generator.write(propName, ""); //$NON-NLS-1$
break;
default:
throw e;
}
}
break;
case TYPE_MIME_PART:
// TODO read inline?
// TODO rationalize multiple body types
final MimeMessage mime = obj.getParentDatabase()
.getParentDominoClient()
.getMimeReader()
.readMIME(obj, propName, EnumSet.of(ReadMimeDataType.MIMEHEADERS));
String content;
try (InputStream is = mime.getInputStream()) {
content = IOUtils.toString(is, StandardCharsets.UTF_8);
} catch (IOException | MessagingException e) {
throw new RuntimeException(e);
}
if (content == null) {
generator.writeNull(propName);
} else {
generator.write(propName, content.toString());
}
break;
case TYPE_HTML:
// TODO this is probably a specialized value, but the underlying API could
// handle converting to string
break;
case TYPE_USERDATA:
// TODO Base64? Custom adapters?
break;
case TYPE_FORMULA:
case TYPE_ERROR:
case TYPE_NOTEREF_LIST:
// TODO convert to string?
break;
case TYPE_ACTION:
case TYPE_ASSISTANT_INFO:
case TYPE_CALENDAR_FORMAT:
case TYPE_COLLATION:
case TYPE_HIGHLIGHTS:
case TYPE_ICON:
case TYPE_INVALID_OR_UNKNOWN:
case TYPE_LSOBJECT:
case TYPE_NOTELINK_LIST:
case TYPE_OBJECT:
case TYPE_QUERY:
case TYPE_SCHED_LIST:
case TYPE_SEAL:
case TYPE_SEAL2:
case TYPE_SEALDATA:
case TYPE_SEAL_LIST:
case TYPE_SIGNATURE:
case TYPE_UNAVAILABLE:
case TYPE_USERID:
case TYPE_VIEWMAP_DATASET:
case TYPE_VIEWMAP_LAYOUT:
case TYPE_VIEW_FORMAT:
case TYPE_WORKSHEET_DATA:
default:
break;
}
}
});
}
generator.writeEnd();
}
private void writeArbitraryValue(final JsonGenerator generator, SerializationContext ctx, final String propName, final Object val) {
if (val == null) {
if (propName == null) {
generator.writeNull();
} else {
generator.writeNull(propName);
}
} else if (val instanceof Short || val instanceof Integer || val instanceof Long) {
if (propName == null) {
generator.write(((Number) val).longValue());
} else {
generator.write(propName, ((Number) val).longValue());
}
} else if (val instanceof Number) {
if (propName == null) {
generator.write(((Number) val).doubleValue());
} else {
generator.write(propName, ((Number) val).doubleValue());
}
} else if (val instanceof Character || val instanceof CharSequence) {
if (propName == null) {
generator.write(val.toString());
} else {
generator.write(propName, val.toString());
}
} else if (val instanceof Boolean) {
if (propName == null) {
generator.write((Boolean) val);
} else {
generator.write(propName, (Boolean) val);
}
} else if (val instanceof Map) {
if (propName == null) {
generator.writeStartObject();
} else {
generator.writeStartObject(propName);
}
((Map, ?>) val).forEach((key, value) -> {
if (key == null) {
throw new IllegalArgumentException("Unable to serialize null key in map");
}
this.writeArbitraryValue(generator, ctx, key.toString(), value);
});
generator.writeEnd();
} else if (val instanceof Iterable) {
if (propName == null) {
generator.writeStartArray();
} else {
generator.writeStartArray(propName);
}
final Spliterator> iter = ((Iterable>) val).spliterator();
StreamSupport.stream(iter, false).forEach(value -> this.writeArbitraryValue(generator, ctx, null, value));
generator.writeEnd();
} else {
// Pass this along to the context to let it try
ctx.serialize(propName, val, generator);
}
}
private void writeTimeProperty(final JsonGenerator generator, final String propName, final DominoTimeType val) {
if (val == null) {
generator.writeNull(propName);
} else {
if (val instanceof DominoDateTime) {
generator.write(propName, JsonUtil.toIsoString((DominoDateTime) val));
} else {
switch (this.dateRangeFormat) {
case OBJECT:
generator.writeStartObject(propName);
generator.write(JsonSerializer.PROP_RANGE_FROM, JsonUtil.toIsoString(((DominoDateRange) val).getStartDateTime()));
generator.write(JsonSerializer.PROP_RANGE_TO, JsonUtil.toIsoString(((DominoDateRange) val).getEndDateTime()));
generator.writeEnd();
break;
case ISO:
default:
generator.write(propName, JsonUtil.toIsoString((DominoDateRange) val));
}
}
}
}
}