/* * Copyright 2013 * * 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: * * http://www.apache.org/licenses/LICENSE-2.0 * * 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 org.openntf.domino.utils; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.Externalizable; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Vector; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import org.openntf.domino.Database; import org.openntf.domino.Document; import org.openntf.domino.DocumentCollection; import org.openntf.domino.Item; import org.openntf.domino.MIMEEntity; import org.openntf.domino.MIMEHeader; import org.openntf.domino.NoteCollection; import org.openntf.domino.RichTextItem; import org.openntf.domino.Session; import org.openntf.domino.Stream; import org.openntf.domino.exceptions.DataNotCompatibleException; import org.openntf.domino.exceptions.MIMEConversionException; import org.openntf.domino.utils.DominoUtils.LoaderObjectInputStream; /** * Utility enum as a carrier for Document-centric static properties and methods. * */ public enum Documents { ; public static final String ITEMNAME_ATTACHMENTS = "attachments"; public static final String ITEMNAME_BODY = "Body"; public static final String ITEMNAME_DESCRIPTION = "description"; public static final String ITEMNAME_FILEPATH = "filepath"; public static final String ITEMNAME_FORM = "Form"; public static final String ITEMNAME_MEMBERS = "Members"; public static final String ITEMNAME_RECORDID = "recordID"; public static final String ITEMNAME_SERVER = "server"; public static final String ITEMNAME_SUBJECT = "subject"; public static final String ITEMNAME_UNIVERSALID = "universalID"; private static AtomicInteger RESTORE_ERR_COUNT = new AtomicInteger(0); /** The Constant log_. */ private final static Logger log_ = Logger.getLogger("org.openntf.domino"); /** The Constant logBackup_. */ // private final static Logger logBackup_ = Logger.getLogger("com.ibm.xsp.domino"); public static enum Flags { HasAttachments, HasEmbedded, IsDeleted, IsEncrypted, IsEncryptOnSend, IsNewNote, IsProfile, IsResponse, IsSaveMessageOnSend, IsSentByAgent, IsSignOnSend, IsValid; @Override public String toString() { return this.getDeclaringClass() + "." + this.getClass() + ":" + this.name(); } }; /* * ************************************************************************** * ************************************************************************** * * MIMEBean methods * * ************************************************************************** * ************************************************************************** */ /** * Restore state. * * @param doc * the doc * @param itemName * the item name * @param entity * the MIMEentity to use, may be null. If specified, it must match itemName * * @return the restored object * @throws Throwable * the throwable */ @SuppressWarnings("unchecked") public static Object restoreState(final Document doc, final String itemName, final MIMEEntity entity) throws Exception { Session session = doc.getAncestorSession(); Object result = null; Stream mimeStream = session.createStream(); Class<?> chkClass = null; String allHeaders = entity.getHeaders(); MIMEHeader javaClassHeader = entity.getNthHeader("X-Java-Class"); if (javaClassHeader != null) { String className = javaClassHeader.getHeaderVal(); chkClass = DominoUtils.getClass(className); if (chkClass == null) { log_.log(Level.SEVERE, "Unable to load class " + className + " from currentThread classLoader" + " so object deserialization is likely to fail..."); } } entity.getContentAsBytes(mimeStream); mimeStream.setPosition(0); // ByteArrayOutputStream streamOut = new ByteArrayOutputStream(); // mimeStream.getContents(streamOut); // mimeStream.recycle(); // byte[] stateBytes = streamOut.toByteArray(); // ByteArrayInputStream byteStream = new ByteArrayInputStream(stateBytes); InputStream is = new Streams.MIMEBufferedInputStream(mimeStream); ObjectInputStream objectStream; if (allHeaders == null) { System.out.println("No headers available. Testing gzip by experimentation..."); try { GZIPInputStream zipStream = new GZIPInputStream(is); objectStream = new LoaderObjectInputStream(zipStream); } catch (Exception ioe) { objectStream = new LoaderObjectInputStream(is); } } else if (allHeaders.toLowerCase().contains("content-encoding: gzip")) { // GZIPInputStream zipStream = new GZIPInputStream(byteStream); GZIPInputStream zipStream = new GZIPInputStream(is); objectStream = new LoaderObjectInputStream(zipStream); } else { objectStream = new LoaderObjectInputStream(is); } // There are three potential storage forms: Externalizable, Serializable, and StateHolder, distinguished by type or header if ("x-java-externalized-object".equals(entity.getContentSubType())) { Class<Externalizable> externalizableClass = (Class<Externalizable>) DominoUtils.getClass(entity.getNthHeader("X-Java-Class") .getHeaderVal()); Externalizable restored = externalizableClass.newInstance(); restored.readExternal(objectStream); result = restored; } else { Object restored = null; try { restored = objectStream.readObject(); } catch (Throwable t) { int curCount = RESTORE_ERR_COUNT.incrementAndGet(); if (curCount < 20) { System.err.println("Unable to restore an object from item " + itemName + " with expect class " + chkClass.getName() + " due to a " + t.getClass().getSimpleName() + " for the " + curCount + " time. Message: " + t.getMessage()); t.printStackTrace(); } } // But wait! It might be a StateHolder object or Collection! MIMEHeader storageScheme = entity.getNthHeader("X-Storage-Scheme"); MIMEHeader originalJavaClass = entity.getNthHeader("X-Original-Java-Class"); if (storageScheme != null && "StateHolder".equals(storageScheme.getHeaderVal())) { Class<?> facesContextClass = DominoUtils.getClass("javax.faces.context.FacesContext"); Method getCurrentInstance = facesContextClass.getMethod("getCurrentInstance"); Class<?> stateHoldingClass = DominoUtils.getClass(originalJavaClass.getHeaderVal()); Method restoreStateMethod = stateHoldingClass.getMethod("restoreState", facesContextClass, Object.class); result = stateHoldingClass.newInstance(); restoreStateMethod.invoke(result, getCurrentInstance.invoke(null), restored); } else if (originalJavaClass != null && "org.openntf.domino.DocumentCollection".equals(originalJavaClass.getHeaderVal())) { // Maybe this can be sped up by not actually getting the documents try { String[] unids = (String[]) restored; Database db = doc.getParentDatabase(); DocumentCollection docCollection = db.createDocumentCollection(); for (String unid : unids) { docCollection.addDocument(db.getDocumentByUNID(unid)); } result = docCollection; } catch (Exception e) { e.printStackTrace(); } } else if (originalJavaClass != null && "org.openntf.domino.NoteCollection".equals(originalJavaClass.getHeaderVal())) { String[] unids = (String[]) restored; Database db = doc.getParentDatabase(); NoteCollection noteCollection = db.createNoteCollection(false); for (String unid : unids) { noteCollection.add(db.getDocumentByUNID(unid)); } result = noteCollection; } else { result = restored; } } // entity.recycle(); if (!doc.closeMIMEEntities(false, itemName)) { // log_.log(Level.WARNING, "closeMIMEEntities returned false for item " + itemName + " on doc " + doc.getNoteID() + " in db " // + doc.getAncestorDatabase().getApiPath()); } return result; } /** * Restore state. * * @param doc * the doc * @param itemName * the item name * @return the serializable * @throws Throwable * the throwable */ public static Object restoreState(final Document doc, final String itemName) throws Exception { return restoreState(doc, itemName, null); } /** * Save state. * * @param object * the object * @param doc * the doc * @param itemName * the item name * @throws Throwable * the throwable */ public static void saveState(final Serializable object, final Document doc, final String itemName) throws Exception { Documents.saveState(object, doc, itemName, false, null); } // private static Map<String, Integer> diagCount = new HashMap<String, Integer>(); /** * Save state. * * @param object * the object * @param doc * the doc * @param itemName * the item name * @param compress * the compress * @param headers * the headers * @throws Throwable * the throwable */ public static void saveState(final Serializable object, final Document doc, final String itemName, boolean compress, final Map<String, String> headers) throws Exception { if (object == null) { log_.log(Level.INFO, "Ignoring attempt to save MIMEBean value of null"); return; } Session session = doc.getAncestorSession(); boolean convertMime = session.isConvertMime(); session.setConvertMime(false); if (compress) { // Check whether it is already a zipped byte[]; if so, don't zip it once more if (object.getClass().getName().equals("[B")) { // Then it's a byte[] byte[] b = (byte[]) object; if (b.length < 50 || // ZIP header + footer take 28 bytes, so in this case zipping doesn't pay (b[0] == (byte) GZIPInputStream.GZIP_MAGIC && b[1] == (byte) (GZIPInputStream.GZIP_MAGIC >> 8))) { compress = false; } } } ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); ObjectOutputStream objectStream = compress ? new ObjectOutputStream(new GZIPOutputStream(byteStream)) : new ObjectOutputStream( byteStream); String contentType = null; // Prefer externalization if available if (object instanceof Externalizable) { ((Externalizable) object).writeExternal(objectStream); contentType = "application/x-java-externalized-object"; } else { // System.out.println("TEMP DEBUG Writing a " + object.getClass().getName() + " to a MIME Bean"); objectStream.writeObject(object); contentType = "application/x-java-serialized-object"; } objectStream.flush(); objectStream.close(); Stream mimeStream = session.createStream(); MIMEEntity previousState = doc.getMIMEEntity(itemName); MIMEEntity entity = null; if (previousState == null) { Item itemChk = doc.getFirstItem(itemName); while (itemChk != null) { if (itemChk.isNames() || itemChk.isReaders() || itemChk.isAuthors()) { throw new DataNotCompatibleException("Cannot overwrite item '" + itemName + "' with serialized data in NoteID " + doc.getNoteID() + ", because it is a Name/Reader/Author item."); } itemChk.remove(); itemChk = doc.getFirstItem(itemName); } entity = doc.createMIMEEntity(itemName); } else { entity = previousState; } try { MIMEHeader javaClass = entity.getNthHeader("X-Java-Class"); MIMEHeader contentEncoding = entity.getNthHeader("Content-Encoding"); if (javaClass == null) { javaClass = entity.createHeader("X-Java-Class"); } else { // long jcid = org.openntf.domino.impl.Base.getDelegateId((org.openntf.domino.impl.Base) javaClass); // if (jcid < 1) { // System.out.println("EXISTING javaClassid: " + jcid); // System.out.println("Item: " + itemName + " in document " + doc.getUniversalID() + " (" + doc.getNoteID() // + ") update count: " + diagCount.get(diagKey)); // } } try { javaClass.setHeaderVal(object.getClass().getName()); } catch (Throwable t) { t.printStackTrace(); } if (compress) { if (contentEncoding == null) { contentEncoding = entity.createHeader("Content-Encoding"); } contentEncoding.setHeaderVal("gzip"); // contentEncoding.recycle(); } else { if (contentEncoding != null) { contentEncoding.remove(); // contentEncoding.recycle(); } } // javaClass.recycle(); if (headers != null) { for (Map.Entry<String, String> entry : headers.entrySet()) { MIMEHeader paramHeader = entity.getNthHeader(entry.getKey()); if (paramHeader == null) { paramHeader = entity.createHeader(entry.getKey()); } paramHeader.setHeaderVal(entry.getValue()); // paramHeader.recycle(); } } byte[] bytes = byteStream.toByteArray(); ByteArrayInputStream byteIn = new ByteArrayInputStream(bytes); mimeStream.setContents(byteIn); entity.setContentFromBytes(mimeStream, contentType, MIMEEntity.ENC_NONE); } finally { // entity.recycle(); // mimeStream.recycle(); // entity = null; //NTF - why set to null? We're properly closing the entities now. // previousState = null; // why set to null? if (!doc.closeMIMEEntities(true, itemName)) { // log_.log(Level.WARNING, "closeMIMEEntities returned false for item " + itemName + " on doc " + doc.getNoteID() + " in db " // + doc.getAncestorDatabase().getApiPath() + " during a saveState call. This may result in data loss!", // new Throwable()); } if (convertMime) { session.setConvertMime(true); } } } /** * Gets the MIME Item value * * @param document * Document from which to get the MIME Value * @param itemname * Name of the item containing the MIME entity * * @return Value of the MIME item, if it exists. Null otherwise. */ public static Object getItemValueMIME(final Document document, final String itemname) { String noteID = null; try { if (null == document) { throw new IllegalArgumentException("Document is null"); } if (Strings.isBlankString(itemname)) { throw new IllegalArgumentException("Itemname is blank or null"); } noteID = document.getNoteID(); Session session = document.getAncestorSession(); boolean convertMime = session.isConvertMIME(); session.setConvertMIME(false); MIMEEntity entity = document.getMIMEEntity(itemname); Object result = null; if (entity != null) { try { result = Documents.getItemValueMIME(document, itemname, entity); } finally { document.closeMIMEEntities(false, itemname); } } session.setConvertMIME(convertMime); return result; } catch (Throwable t) { DominoUtils.handleException(new MIMEConversionException("Unable to getItemValueMIME for item name " + itemname + " on document " + noteID, t)); } return null; } /** * Gets the MIME Item value * * @param document * Document from which to get the MIME Value * @param itemname * Name of the item containing the MIME entity * @param entity * MIMEEntity from which to retrive the MIME value. * * @return Value of the MIME item, if it exists. Null otherwise. */ public static Object getItemValueMIME(final Document document, final String itemname, MIMEEntity entity) { String noteID = null; boolean convertMime = false; boolean mustClose = false; try { if (null == document) { throw new IllegalArgumentException("Document is null"); } if (Strings.isBlankString(itemname)) { throw new IllegalArgumentException("Itemname is blank or null"); } noteID = document.getNoteID(); Session session = document.getAncestorSession(); convertMime = session.isConvertMIME(); if (convertMime) { session.setConvertMIME(false); } if (entity == null) { entity = document.getMIMEEntity(itemname); mustClose = true; } if (entity == null) { return null; } MIMEHeader contentType = entity.getNthHeader("Content-Type"); String headerval = (null == contentType) ? "" : contentType.getHeaderVal(); if ("application/x-java-serialized-object".equals(headerval) || "application/x-java-externalized-object".equals(headerval)) { // entity is a MIMEBean return Documents.restoreState(document, itemname, entity); } } catch (Throwable t) { DominoUtils.handleException(new MIMEConversionException("Unable to getItemValueMIME for item name " + itemname + " on document " + noteID + " [Caught " + t.getClass().getName() + ": " + t.getMessage() + "]", t)); } finally { if (entity != null && mustClose) { document.closeMIMEEntities(false, itemname); } if (convertMime) { Session session = document.getAncestorSession(); session.setConvertMIME(true); } } return null; } public static boolean isHasRecordID(final Document document) { try { if (null == document) { throw new IllegalArgumentException("Document is null"); } return (document.hasItem(ITEMNAME_RECORDID) && (!Strings.isBlankString(document.getItemValueString(ITEMNAME_RECORDID)))); } catch (Exception e) { DominoUtils.handleException(e); } return false; } /** * Returns whether the given field in the given document contains a value. * * @param document * The document. * @param fieldName * The fieldname of the field to test. * @return <code>true</code> if a field in a document contains a value, else <code>false</code>. */ public static boolean isSet(final Document document, final String fieldName) { if (!document.hasItem(fieldName)) return false; Vector<Object> value = document.getItemValue(fieldName); if (value == null) return false; if (value.size() == 0) return false; if (value.size() > 1) return true; // size = 1 return !Strings.isBlankString(value.get(0).toString()); } /** * Returns the {@link RichTextItem} stored in the given {@link Document}. * * @param document * The document. * @param fieldName * The name of the RichTextItem which should be returned. * @param createItem * <code>True</code> to create the RichTextItem if it does not already exist or is not type RichTextItem. * @return The Item as RichTextItem or <code>null</code>, if it does not exist or is not a RichTextItem. */ public static RichTextItem getRichTextItem(final Document document, final String fieldName, final boolean createItem) { Item tmpItem = document.getFirstItem(fieldName); if (tmpItem instanceof RichTextItem) { return (RichTextItem) tmpItem; } if (!createItem) { return null; } else { if (tmpItem != null) { document.removeItem(fieldName); } return document.createRichTextItem(fieldName); } } public static Map<String, List<Object>> getItemTable(final Document doc, final CharSequence... itemnames) { if (doc == null || itemnames == null) return null; Map<String, List<Object>> result = new LinkedHashMap<String, List<Object>>(); for (CharSequence itemname : itemnames) { if (doc.hasItem(itemname.toString())) { Vector<Object> v = doc.getItemValue(itemname.toString()); result.put(itemname.toString(), v); } } return result; } public static void setItemTable(final Document doc, final Map<String, List<Object>> table) { if (doc == null || table == null) return; for (String key : table.keySet()) { doc.replaceItemValue(key, table.get(key)); } } public static void setItemTablePivot(final Document doc, final List<Map<String, Object>> pivot) { //TODO NTF Map<String, List<Object>> unpivot = new LinkedHashMap<String, List<Object>>(); for (int i = 0; i < pivot.size(); i++) { Map<String, Object> curMap = pivot.get(i); for (String key : curMap.keySet()) { List<Object> curList = unpivot.get(key); if (curList == null) { curList = new ArrayList<Object>(pivot.size()); unpivot.put(key, curList); } curList.set(i, curMap.get(key)); } } setItemTable(doc, unpivot); } public static List<Map<String, Object>> getItemTablePivot(final Document doc, final CharSequence... itemnames) { if (doc == null || itemnames == null) return null; Map<String, List<Object>> table = getItemTable(doc, itemnames); if (table == null) return null; Set<String> keys = table.keySet(); int maxSize = 0; for (String key : keys) { List<Object> curValue = table.get(key); if (curValue.size() > maxSize) maxSize = curValue.size(); } List<Map<String, Object>> result = new ArrayList<Map<String, Object>>(maxSize); for (int i = 0; i < maxSize; i++) { Map<String, Object> map = new LinkedHashMap<String, Object>(); result.set(i, map); for (String key : keys) { List<Object> curValue = table.get(key); if (curValue.size() > i) { map.put(key, curValue.get(i)); } else { map.put(key, null); } } } return result; } }