/* * � Copyright IBM Corp. 2012 * * 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 com.ibm.domino.commons.json; import static com.ibm.domino.commons.json.JsonConstants.BOUNDARY_PROP; import static com.ibm.domino.commons.json.JsonConstants.CONTENT_DISPOSITION_PROP; import static com.ibm.domino.commons.json.JsonConstants.CONTENT_ID_PROP; import static com.ibm.domino.commons.json.JsonConstants.CONTENT_TRANSFER_ENCODING_PROP; import static com.ibm.domino.commons.json.JsonConstants.CONTENT_TYPE_PROP; import static com.ibm.domino.commons.json.JsonConstants.DATA_PROP; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import lotus.domino.Document; import lotus.domino.MIMEEntity; import lotus.domino.MIMEHeader; import lotus.domino.NotesException; import lotus.domino.Session; import lotus.domino.Stream; import com.ibm.commons.util.StringUtil; import com.ibm.commons.util.io.json.JsonException; import com.ibm.commons.util.io.json.JsonJavaObject; import com.ibm.commons.util.io.json.JsonObject; import com.ibm.commons.util.io.json.parser.ParseException; import com.ibm.domino.commons.internal.Logger; /** * Adapts a MIMEEntity to a JsonObject. */ public class JsonMimeEntityAdapter implements JsonObject { private static final String CONTENT_TYPE_HEADER = "Content-Type"; //$NON-NLS-1$ private static final String CONTENT_ID_HEADER = "Content-ID"; //$NON-NLS-1$ private static final String CONTENT_DISPOSITION_HEADER = "Content-Disposition"; //$NON-NLS-1$ private static final String CONTENT_TRANSFER_ENCODING_HEADER = "Content-Transfer-Encoding"; //$NON-NLS-1$ private static final String BOUNDARY_EQUALS = "boundary="; //$NON-NLS-1$ private static final String MULTIPART = "multipart"; //$NON-NLS-1$ private static final String ENCODING_BASE64 = "base64"; //$NON-NLS-1$ private static final String ENCODING_7BIT = "7bit"; //$NON-NLS-1$ private static final String ENCODING_8BIT = "8bit"; //$NON-NLS-1$ private static final String ENCODING_BINARY = "binary"; //$NON-NLS-1$ private static final String ENCODING_QUOTED_PRINTABLE = "quoted-printable"; //$NON-NLS-1$ MIMEEntity _entity = null; private String[] _propertyNames; private ParserContext _context; private JsonObject _objectCache; /** * Inner class for holding the parser context. * * <p>A parser context is constructed once and shared by all MIME entities in an array. */ public static class ParserContext { private MIMEEntity _rootEntity; private Document _document; private String _itemName; private JsonMimeEntityAdapter _currentEntityAdapter; private Map<String, MIMEEntity> _entityMap = new HashMap<String, MIMEEntity>(); public ParserContext(Document document, String itemName) { _document = document; _itemName = itemName; } public JsonMimeEntityAdapter getCurrentEntityAdapter() { return _currentEntityAdapter; } public void setCurrentEntityAdapter(JsonMimeEntityAdapter currentEntityAdapter) { _currentEntityAdapter = currentEntityAdapter; } public Document getDocument() { return _document; } public String getItemName() { return _itemName; } public MIMEEntity getRootEntity() { return _rootEntity; } public void setRootEntity(MIMEEntity rootEntity) { _rootEntity = rootEntity; } public void addEntity(String key, MIMEEntity entity) { _entityMap.put(key, entity); } public MIMEEntity getEntity(String key) { return _entityMap.get(key); } } /** * Constructor used when generating JSON output. * * @param entity */ public JsonMimeEntityAdapter(MIMEEntity entity) { _entity = entity; } /** * Constructor used when parsing JSON input. * * @param context */ public JsonMimeEntityAdapter(ParserContext context, JsonJavaObject jsonObject) { _context = context; _objectCache = jsonObject; } /** * Recursively adds entity adapters to a flat list (depth first) * * @param adapters * @param entity * @throws NotesException */ public static void addEntityAdapter(List<JsonMimeEntityAdapter> adapters, MIMEEntity entity) throws NotesException { // Add this entity JsonMimeEntityAdapter adapter = new JsonMimeEntityAdapter(entity); adapters.add(adapter); // Add children MIMEEntity child = entity.getFirstChildEntity(); if ( child != null ) { addEntityAdapter(adapters, child); // Add siblings MIMEEntity sibling = child.getNextSibling(); while ( sibling != null ) { addEntityAdapter(adapters, sibling); sibling = sibling.getNextSibling(); } } } public Iterator<String> getJsonProperties() { return new Iterator<String>() { private int _index = 0; public boolean hasNext() { String properties[] = getProperties(); return _index < properties.length ; } public String next() { String properties[] = getProperties(); return properties[_index++]; } public void remove() { // The JSON IO classes shouldn't call remove } private String[] getProperties() { if ( _propertyNames != null ) { return _propertyNames; } List<String> properties = new ArrayList<String>(); try { // Base64 encode the data (if necessary). We do this first // because it can change the headers int encoding = _entity.getEncoding(); if (encoding == MIMEEntity.ENC_IDENTITY_BINARY) { _entity.encodeContent(MIMEEntity.ENC_BASE64); } // Assume there is always a content type header properties.add(CONTENT_TYPE_PROP); // The remaining properties depend on the entity MIMEHeader header = null; header = _entity.getNthHeader(CONTENT_ID_HEADER); if (header != null) { properties.add(CONTENT_ID_PROP); } header = _entity.getNthHeader(CONTENT_DISPOSITION_HEADER); if (header != null) { properties.add(CONTENT_DISPOSITION_PROP); } header = _entity.getNthHeader(CONTENT_TRANSFER_ENCODING_HEADER); if (header != null) { properties.add(CONTENT_TRANSFER_ENCODING_PROP); } // TODO: Review this. The content can be quite large. // Assigning it to a string here could result in multiple // copies on the heap. String content = _entity.getContentAsText(); if (StringUtil.isNotEmpty(content)) { properties.add(DATA_PROP); } String boundaryStart = _entity.getBoundaryStart(); if (StringUtil.isNotEmpty(boundaryStart)) { properties.add(BOUNDARY_PROP); } } catch (NotesException e) { Logger.get().warnp(this, "getProperties",//$NON-NLS-1$ e, "Unhandled exception getting the list of JSON property names");// $NLW-JsonMimeEntityAdapter_UnhandledExceptionInGetProperties-1$ } // Convert to array String[] array = new String[properties.size()]; Iterator<String> iterator = properties.iterator(); for ( int i = 0; iterator.hasNext(); i++ ) { array[i] = iterator.next(); } // Cache the array for next time _propertyNames = array; return _propertyNames; } }; } public Object getJsonProperty(String property) { Object value = null; try { MIMEHeader header = null; if ( CONTENT_TYPE_PROP.equals(property) ) { header = _entity.getNthHeader(CONTENT_TYPE_HEADER); } else if ( CONTENT_ID_PROP.equals(property) ) { header = _entity.getNthHeader(CONTENT_ID_HEADER); } else if ( CONTENT_DISPOSITION_PROP.equals(property) ) { header = _entity.getNthHeader(CONTENT_DISPOSITION_HEADER); } else if ( CONTENT_TRANSFER_ENCODING_PROP.equals(property) ) { header = _entity.getNthHeader(CONTENT_TRANSFER_ENCODING_HEADER); } if ( header != null ) { value = header.getHeaderValAndParams().trim(); } else if ( DATA_PROP.equals(property) ) { String content = _entity.getContentAsText(); header = _entity.getNthHeader(CONTENT_TYPE_HEADER); if ( header != null && header.getHeaderVal().toLowerCase().contains(MULTIPART) ) { value = content.trim(); } else { value = content; } } else if ( BOUNDARY_PROP.equals(property) ) { String boundaryStart = _entity.getBoundaryStart(); value = boundaryStart.trim(); } if ( value == null ) { value = "null"; //$NON-NLS-1$ } } catch (NotesException e) { Logger.get().warnp(this, "getJsonProperty",//$NON-NLS-1$ e, "Unhandled exception getting a JSON property value");// $NLW-JsonMimeEntityAdapter_UnhandledExceptionInGetJsonProperty-1$ } return value; } public void putJsonProperty(String property, Object value) { // Delegate to the cache _objectCache.putJsonProperty(property, value); } /** * When parsing the JSON representation of a MIME entity, we temporarily cache the JSON properties * in memory. Call this method to flush the entire entity to the document. * * @param lastEntity */ public void flushJsonProperties(boolean lastEntity) throws JsonException { Document document = _context.getDocument(); String itemName = _context.getItemName(); MIMEEntity entity = null; try { // Create the MIMEEntity if ( _context.getRootEntity() == null ) { if (document.hasItem(itemName)) { document.removeItem(itemName); } entity = document.createMIMEEntity(itemName); _context.setRootEntity(entity); } else { // Whose child are we? MIMEEntity parentEntity = null; String boundary = (String)_objectCache.getJsonProperty(BOUNDARY_PROP); if ( !StringUtil.isEmpty(boundary) ) { parentEntity = _context.getEntity(boundary); } if ( parentEntity == null ) { /* entity = _context.getRootEntity().createChildEntity(); */ throw new JsonException(new ParseException(boundary)); } else { entity = parentEntity.createChildEntity(); } } // Add this entity to the map String contentType = (String)_objectCache.getJsonProperty(CONTENT_TYPE_PROP); String key = getKeyFromContentType(contentType); if ( key != null ) { _context.addEntity(key, entity); } // Write the headers addHeader(CONTENT_TYPE_PROP, CONTENT_TYPE_HEADER, entity); addHeader(CONTENT_DISPOSITION_PROP, CONTENT_DISPOSITION_HEADER, entity); addHeader(CONTENT_ID_PROP, CONTENT_ID_HEADER, entity); addHeader(CONTENT_TRANSFER_ENCODING_PROP, CONTENT_TRANSFER_ENCODING_HEADER, entity); // Write the data String data = (String)_objectCache.getJsonProperty(DATA_PROP); if (!StringUtil.isEmpty(data)) { int encoding = MIMEEntity.ENC_NONE; String contentEncoding = (String)_objectCache.getJsonProperty(CONTENT_TRANSFER_ENCODING_PROP); if (!StringUtil.isEmpty(contentEncoding)) { if(contentEncoding.contains(ENCODING_BASE64)) encoding = MIMEEntity.ENC_BASE64; else if(contentEncoding.contains(ENCODING_7BIT)) encoding = MIMEEntity.ENC_IDENTITY_7BIT; else if(contentEncoding.contains(ENCODING_8BIT)) encoding = MIMEEntity.ENC_IDENTITY_8BIT; else if(contentEncoding.contains(ENCODING_BINARY)) encoding = MIMEEntity.ENC_IDENTITY_BINARY; else if(contentEncoding.contains(ENCODING_QUOTED_PRINTABLE)) encoding = MIMEEntity.ENC_QUOTED_PRINTABLE; } Session session = document.getParentDatabase().getParent(); Stream stream = session.createStream(); stream.writeText(data); entity.setContentFromText(stream, contentType, encoding); stream.close(); stream.recycle(); } // Dereference the cached object so it can be GC'd _objectCache = null; // Clean up for the last entity in the array if ( lastEntity ) { document.closeMIMEEntities(true, itemName); _context.getRootEntity().recycle(); } } catch (NotesException e) { throw new JsonException(e); } } private String getKeyFromContentType(String contentType) { String key = null; if (contentType != null && contentType.toLowerCase().contains(MULTIPART) && contentType.toLowerCase().contains(BOUNDARY_EQUALS) ) { key = contentType.substring(contentType.toLowerCase().indexOf(BOUNDARY_EQUALS) + BOUNDARY_EQUALS.length()); } if ( key != null ) { if (key.startsWith("\"")) { //$NON-NLS-1$ key = key.substring(1); } if (key.endsWith("\"")) { //$NON-NLS-1$ key = key.substring(0, key.length()-1); } if (key.length() > 0) { key = "--" + key; //$NON-NLS-1$ } } return key; } private void addHeader(String property, String header, MIMEEntity entity) throws NotesException { String value = (String)_objectCache.getJsonProperty(property); if (!StringUtil.isEmpty(value)) { MIMEHeader mimeHeader = entity.createHeader(header); mimeHeader.setHeaderVal(value); } } }