| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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.apache.chemistry.opencmis.server.impl.atompub; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.math.BigInteger; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| |
| import javax.xml.namespace.QName; |
| import javax.xml.stream.XMLStreamException; |
| import javax.xml.stream.XMLStreamReader; |
| import javax.xml.stream.XMLStreamWriter; |
| |
| import org.apache.chemistry.opencmis.commons.PropertyIds; |
| import org.apache.chemistry.opencmis.commons.data.Acl; |
| import org.apache.chemistry.opencmis.commons.data.ContentStream; |
| import org.apache.chemistry.opencmis.commons.data.ObjectData; |
| import org.apache.chemistry.opencmis.commons.data.Properties; |
| import org.apache.chemistry.opencmis.commons.data.PropertyData; |
| import org.apache.chemistry.opencmis.commons.data.PropertyId; |
| import org.apache.chemistry.opencmis.commons.data.PropertyString; |
| import org.apache.chemistry.opencmis.commons.definitions.TypeDefinition; |
| import org.apache.chemistry.opencmis.commons.exceptions.CmisInvalidArgumentException; |
| import org.apache.chemistry.opencmis.commons.exceptions.CmisNotSupportedException; |
| import org.apache.chemistry.opencmis.commons.impl.Base64; |
| import org.apache.chemistry.opencmis.commons.impl.IOUtils; |
| import org.apache.chemistry.opencmis.commons.impl.XMLConstants; |
| import org.apache.chemistry.opencmis.commons.impl.XMLConstraints; |
| import org.apache.chemistry.opencmis.commons.impl.XMLConverter; |
| import org.apache.chemistry.opencmis.commons.impl.XMLUtils; |
| import org.apache.chemistry.opencmis.commons.impl.dataobjects.BulkUpdateImpl; |
| import org.apache.chemistry.opencmis.commons.impl.dataobjects.ContentStreamImpl; |
| import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertiesImpl; |
| import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyStringImpl; |
| import org.apache.chemistry.opencmis.commons.server.TempStoreOutputStream; |
| import org.apache.chemistry.opencmis.server.shared.CappedInputStream; |
| import org.apache.chemistry.opencmis.server.shared.TempStoreOutputStreamFactory; |
| |
| /** |
| * Parser for Atom Entries. |
| */ |
| public final class AtomEntryParser { |
| |
| private static final long MAX_STREAM_LENGTH = 10 * 1024 * 1024; |
| |
| private static final String TAG_ENTRY = "entry"; |
| private static final String TAG_TITLE = "title"; |
| private static final String TAG_OBJECT = "object"; |
| private static final String TAG_CONTENT = "content"; |
| private static final String TAG_BASE64 = "base64"; |
| private static final String TAG_MEDIATYPE = "mediatype"; |
| private static final String TAG_FILENAME = "filename"; |
| private static final String TAG_TYPE = "type"; |
| private static final String TAG_BULK_UPDATE = "bulkUpdate"; |
| |
| private static final String ATTR_SRC = "src"; |
| private static final String ATTR_TYPE = "type"; |
| |
| private boolean ignoreAtomContentSrc; |
| |
| private CappedInputStream cappedStream; |
| |
| private final TempStoreOutputStreamFactory streamFactory; |
| |
| private ObjectData object; |
| private ContentStreamImpl atomContentStream; |
| private ContentStreamImpl cmisContentStream; |
| private TypeDefinition typeDef; |
| private BulkUpdateImpl bulkUpdate; |
| |
| /** |
| * Constructor. |
| */ |
| public AtomEntryParser(TempStoreOutputStreamFactory streamFactory) { |
| this.streamFactory = streamFactory; |
| } |
| |
| /** |
| * Constructor that immediately parses the given stream. |
| */ |
| public AtomEntryParser(InputStream stream, TempStoreOutputStreamFactory streamFactory) throws XMLStreamException, |
| IOException { |
| this(streamFactory); |
| parse(stream); |
| } |
| |
| /** |
| * Sets the flag controlling whether atom content src (external content) is |
| * ignored. This flag is false by default (not ignored). |
| */ |
| public void setIgnoreAtomContentSrc(boolean ignoreAtomContentSrc) { |
| this.ignoreAtomContentSrc = ignoreAtomContentSrc; |
| } |
| |
| /** |
| * Returns the object. |
| */ |
| public ObjectData getObject() { |
| return object; |
| } |
| |
| /** |
| * Returns the properties of the object. |
| */ |
| public Properties getProperties() { |
| return (object == null ? null : object.getProperties()); |
| } |
| |
| /** |
| * Returns the Id of the object. |
| */ |
| public String getId() { |
| Properties properties = getProperties(); |
| if (properties == null) { |
| return null; |
| } |
| |
| Map<String, PropertyData<?>> propertiesMap = properties.getProperties(); |
| if (propertiesMap == null) { |
| return null; |
| } |
| |
| PropertyData<?> property = propertiesMap.get(PropertyIds.OBJECT_ID); |
| if (property instanceof PropertyId) { |
| return ((PropertyId) property).getFirstValue(); |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Returns the ACL of the object. |
| */ |
| public Acl getAcl() { |
| return (object == null ? null : object.getAcl()); |
| } |
| |
| /** |
| * Returns the policy id list of the object. |
| */ |
| public List<String> getPolicyIds() { |
| if ((object == null) || (object.getPolicyIds() == null)) { |
| return null; |
| } |
| |
| return object.getPolicyIds().getPolicyIds(); |
| } |
| |
| /** |
| * Returns the content stream. |
| */ |
| public ContentStream getContentStream() { |
| return (cmisContentStream == null ? atomContentStream : cmisContentStream); |
| } |
| |
| /** |
| * Returns the type definition. |
| */ |
| public TypeDefinition getTypeDefinition() { |
| return typeDef; |
| } |
| |
| /** |
| * Returns the bulk update data. |
| */ |
| public BulkUpdateImpl getBulkUpdate() { |
| return bulkUpdate; |
| } |
| |
| /** |
| * Parses the stream. |
| */ |
| public void parse(InputStream stream) throws XMLStreamException, IOException { |
| release(); |
| |
| if (stream == null) { |
| return; |
| } |
| |
| cappedStream = new CappedInputStream(stream, MAX_STREAM_LENGTH); |
| XMLStreamReader parser = XMLUtils.createParser(cappedStream); |
| |
| try { |
| while (true) { |
| int event = parser.getEventType(); |
| if (event == XMLStreamReader.START_ELEMENT) { |
| QName name = parser.getName(); |
| |
| if (XMLConstants.NAMESPACE_ATOM.equals(name.getNamespaceURI()) |
| && (TAG_ENTRY.equals(name.getLocalPart()))) { |
| parseEntry(parser); |
| break; |
| } else { |
| throw new CmisInvalidArgumentException("XML is not an Atom entry!"); |
| } |
| } |
| |
| if (!XMLUtils.next(parser)) { |
| break; |
| } |
| } |
| } catch (XMLStreamException xse) { |
| release(); |
| throw xse; |
| } catch (IOException ioe) { |
| release(); |
| throw ioe; |
| } catch (RuntimeException re) { |
| release(); |
| throw re; |
| } finally { |
| parser.close(); |
| } |
| } |
| |
| /** |
| * Releases all resources. |
| */ |
| public void release() { |
| object = null; |
| typeDef = null; |
| bulkUpdate = null; |
| closeAtomContentStream(); |
| closeCmisContentStream(); |
| } |
| |
| /** |
| * Closes the Atom content stream. |
| */ |
| private void closeAtomContentStream() { |
| IOUtils.closeQuietly(atomContentStream); |
| atomContentStream = null; |
| } |
| |
| /** |
| * Closes the CMIS content stream. |
| */ |
| private void closeCmisContentStream() { |
| IOUtils.closeQuietly(cmisContentStream); |
| cmisContentStream = null; |
| } |
| |
| /** |
| * Parses an Atom entry. |
| */ |
| private void parseEntry(XMLStreamReader parser) throws XMLStreamException, IOException { |
| String atomTitle = null; |
| |
| XMLUtils.next(parser); |
| |
| // walk through all tags in entry |
| while (true) { |
| int event = parser.getEventType(); |
| if (event == XMLStreamReader.START_ELEMENT) { |
| QName name = parser.getName(); |
| |
| if (XMLConstants.NAMESPACE_RESTATOM.equals(name.getNamespaceURI())) { |
| if (TAG_OBJECT.equals(name.getLocalPart())) { |
| parseObject(parser); |
| } else if (TAG_TYPE.equals(name.getLocalPart())) { |
| parseTypeDefinition(parser); |
| } else if (TAG_BULK_UPDATE.equals(name.getLocalPart())) { |
| parseBulkUpdate(parser); |
| } else if (TAG_CONTENT.equals(name.getLocalPart())) { |
| parseCmisContent(parser); |
| } else { |
| XMLUtils.skip(parser); |
| } |
| } else if (XMLConstants.NAMESPACE_ATOM.equals(name.getNamespaceURI())) { |
| if (TAG_CONTENT.equals(name.getLocalPart())) { |
| parseAtomContent(parser); |
| } else if (TAG_TITLE.equals(name.getLocalPart())) { |
| atomTitle = XMLUtils.readText(parser, XMLConstraints.MAX_STRING_LENGTH); |
| } else { |
| XMLUtils.skip(parser); |
| } |
| } else { |
| XMLUtils.skip(parser); |
| } |
| } else if (event == XMLStreamReader.END_ELEMENT) { |
| break; |
| } else { |
| if (!XMLUtils.next(parser)) { |
| break; |
| } |
| } |
| } |
| |
| // overwrite cmis:name with Atom title |
| if ((object != null) && (object.getProperties() != null) && (atomTitle != null) && (atomTitle.length() > 0)) { |
| PropertyString nameProperty = new PropertyStringImpl(PropertyIds.NAME, atomTitle); |
| ((PropertiesImpl) object.getProperties()).replaceProperty(nameProperty); |
| } |
| } |
| |
| /** |
| * Parses a CMIS object. |
| */ |
| private void parseObject(XMLStreamReader parser) throws XMLStreamException { |
| object = XMLConverter.convertObject(parser); |
| } |
| |
| /** |
| * Parses a CMIS type. |
| */ |
| private void parseTypeDefinition(XMLStreamReader parser) throws XMLStreamException { |
| typeDef = XMLConverter.convertTypeDefinition(parser); |
| } |
| |
| /** |
| * Parses a bluk update. |
| */ |
| private void parseBulkUpdate(XMLStreamReader parser) throws XMLStreamException { |
| bulkUpdate = XMLConverter.convertBulkUpdate(parser); |
| } |
| |
| /** |
| * Extract the content stream. |
| * |
| * @throws XMLStreamException |
| * @throws IOException |
| */ |
| private void parseAtomContent(XMLStreamReader parser) throws XMLStreamException, IOException { |
| if (atomContentStream != null) { |
| closeAtomContentStream(); |
| throw new CmisInvalidArgumentException("More than one content provided!"); |
| } |
| |
| if (cmisContentStream != null) { |
| // CMIS content takes precedence (see CMIS spec) |
| XMLUtils.skip(parser); |
| return; |
| } |
| |
| atomContentStream = new ContentStreamImpl(); |
| |
| // read attributes |
| String type = "text"; |
| String mimeType = "text/plain"; |
| for (int i = 0; i < parser.getAttributeCount(); i++) { |
| QName attrName = parser.getAttributeName(i); |
| if (ATTR_TYPE.equals(attrName.getLocalPart())) { |
| if (parser.getAttributeValue(i) != null) { |
| type = parser.getAttributeValue(i).trim().toLowerCase(Locale.ENGLISH); |
| } |
| } else if (ATTR_SRC.equals(attrName.getLocalPart())) { |
| if (ignoreAtomContentSrc) { |
| atomContentStream = null; |
| XMLUtils.skip(parser); |
| return; |
| } |
| throw new CmisNotSupportedException("External content not supported!"); |
| } |
| } |
| |
| TempStoreOutputStream tsos = null; |
| if (type.equals("text")) { |
| mimeType = "text/plain"; |
| tsos = readContentBytes(parser, mimeType); |
| } else if (type.equals("html")) { |
| mimeType = "text/html"; |
| tsos = readContentBytes(parser, mimeType); |
| } else if (type.equals("xhtml")) { |
| mimeType = "application/xhtml+xml"; |
| tsos = copy(parser, mimeType); |
| } else if (type.endsWith("/xml") || type.endsWith("+xml")) { |
| mimeType = type; |
| tsos = copy(parser, mimeType); |
| } else if (type.startsWith("text/")) { |
| mimeType = type; |
| tsos = readContentBytes(parser, mimeType); |
| } else { |
| mimeType = type; |
| tsos = readBase64(parser, mimeType, null); |
| } |
| |
| atomContentStream.setMimeType(mimeType); |
| |
| if (tsos != null) { |
| try { |
| atomContentStream.setStream(tsos.getInputStream()); |
| atomContentStream.setLength(BigInteger.valueOf(tsos.getLength())); |
| } catch (IOException e) { |
| tsos.destroy(e); |
| throw e; |
| } |
| } |
| } |
| |
| /** |
| * Extract the content stream. |
| */ |
| private void parseCmisContent(XMLStreamReader parser) throws XMLStreamException, IOException { |
| closeAtomContentStream(); |
| if (cmisContentStream != null) { |
| closeCmisContentStream(); |
| throw new CmisInvalidArgumentException("More than one content provided!"); |
| } |
| |
| cmisContentStream = new ContentStreamImpl(); |
| |
| XMLUtils.next(parser); |
| |
| // walk through all tags in content |
| while (true) { |
| int event = parser.getEventType(); |
| if (event == XMLStreamReader.START_ELEMENT) { |
| QName name = parser.getName(); |
| |
| if (XMLConstants.NAMESPACE_RESTATOM.equals(name.getNamespaceURI())) { |
| if (TAG_MEDIATYPE.equals(name.getLocalPart())) { |
| cmisContentStream.setMimeType(XMLUtils.readText(parser, XMLConstraints.MAX_STRING_LENGTH)); |
| } else if (TAG_BASE64.equals(name.getLocalPart())) { |
| TempStoreOutputStream tsos = readBase64(parser, cmisContentStream.getMimeType(), |
| cmisContentStream.getFileName()); |
| try { |
| cmisContentStream.setStream(tsos.getInputStream()); |
| cmisContentStream.setLength(BigInteger.valueOf(tsos.getLength())); |
| } catch (IOException e) { |
| tsos.destroy(e); |
| throw e; |
| } |
| } else { |
| XMLUtils.skip(parser); |
| } |
| } else if (XMLConstants.NAMESPACE_APACHE_CHEMISTRY.equals(name.getNamespaceURI())) { |
| if (TAG_FILENAME.equals(name.getLocalPart())) { |
| cmisContentStream.setFileName(XMLUtils.readText(parser, XMLConstraints.MAX_STRING_LENGTH)); |
| } else { |
| XMLUtils.skip(parser); |
| } |
| } else { |
| XMLUtils.skip(parser); |
| } |
| } else if (event == XMLStreamReader.END_ELEMENT) { |
| break; |
| } else { |
| if (!XMLUtils.next(parser)) { |
| break; |
| } |
| } |
| } |
| |
| XMLUtils.next(parser); |
| } |
| |
| /** |
| * Parses a tag that contains content bytes. |
| */ |
| private TempStoreOutputStream readContentBytes(XMLStreamReader parser, String mimeType) throws XMLStreamException, |
| IOException { |
| TempStoreOutputStream bufferStream = streamFactory.newOutputStream(); |
| bufferStream.setMimeType(mimeType); |
| |
| XMLUtils.next(parser); |
| |
| try { |
| while (true) { |
| int event = parser.getEventType(); |
| if (event == XMLStreamReader.END_ELEMENT) { |
| break; |
| } else if (event == XMLStreamReader.CHARACTERS) { |
| String s = parser.getText(); |
| if (s != null) { |
| byte[] bytes = IOUtils.toUTF8Bytes(s); |
| bufferStream.write(bytes); |
| cappedStream.deductBytes(bytes.length); |
| } |
| } else if (event == XMLStreamReader.START_ELEMENT) { |
| bufferStream.destroy(null); |
| throw new CmisInvalidArgumentException("Unexpected tag: " + parser.getName()); |
| } |
| |
| if (!XMLUtils.next(parser)) { |
| break; |
| } |
| } |
| } catch (XMLStreamException xse) { |
| // remove temp file |
| bufferStream.destroy(xse); |
| throw xse; |
| } catch (IOException ioe) { |
| // remove temp file |
| bufferStream.destroy(ioe); |
| throw ioe; |
| } |
| |
| XMLUtils.next(parser); |
| |
| return bufferStream; |
| } |
| |
| /** |
| * Parses a tag that contains base64 encoded content. |
| */ |
| private TempStoreOutputStream readBase64(XMLStreamReader parser, String mimeType, String filename) |
| throws XMLStreamException, IOException { |
| TempStoreOutputStream bufferStream = streamFactory.newOutputStream(); |
| bufferStream.setMimeType(mimeType); |
| bufferStream.setFileName(filename); |
| Base64.OutputStream b64stream = new Base64.OutputStream(bufferStream, Base64.DECODE); |
| |
| XMLUtils.next(parser); |
| |
| try { |
| while (true) { |
| int event = parser.getEventType(); |
| if (event == XMLStreamReader.END_ELEMENT) { |
| break; |
| } else if (event == XMLStreamReader.CHARACTERS) { |
| int len = parser.getTextLength(); |
| if (len > 0) { |
| char[] chars = parser.getTextCharacters(); |
| int offset = parser.getTextStart(); |
| for (int i = 0; i < len; i++) { |
| // it's base64/ASCII |
| b64stream.write(chars[offset + i]); |
| } |
| cappedStream.deductBytes(len); |
| } |
| } else if (event == XMLStreamReader.START_ELEMENT) { |
| b64stream.close(); |
| bufferStream.destroy(null); |
| throw new CmisInvalidArgumentException("Unexpected tag: " + parser.getName()); |
| } |
| |
| if (!XMLUtils.next(parser)) { |
| break; |
| } |
| } |
| |
| b64stream.close(); |
| } catch (XMLStreamException xse) { |
| // remove temp file |
| bufferStream.destroy(xse); |
| throw xse; |
| } catch (IOException ioe) { |
| // remove temp file |
| bufferStream.destroy(ioe); |
| throw ioe; |
| } |
| |
| XMLUtils.next(parser); |
| |
| return bufferStream; |
| } |
| |
| /** |
| * Copies a subtree into a stream. |
| */ |
| private TempStoreOutputStream copy(XMLStreamReader parser, String mimeType) throws XMLStreamException, IOException { |
| // create a writer |
| TempStoreOutputStream bufferStream = streamFactory.newOutputStream(); |
| bufferStream.setMimeType(mimeType); |
| |
| try { |
| XMLStreamWriter writer = XMLUtils.createWriter(bufferStream); |
| |
| writer.writeStartDocument(); |
| |
| // copy subtree |
| int level = 1; |
| while (XMLUtils.next(parser)) { |
| int event = parser.getEventType(); |
| if (event == XMLStreamReader.START_ELEMENT) { |
| copyStartElement(parser, writer); |
| level++; |
| } else if (event == XMLStreamReader.CHARACTERS) { |
| writer.writeCharacters(parser.getText()); |
| } else if (event == XMLStreamReader.COMMENT) { |
| writer.writeComment(parser.getText()); |
| } else if (event == XMLStreamReader.CDATA) { |
| writer.writeCData(parser.getText()); |
| } else if (event == XMLStreamReader.END_ELEMENT) { |
| level--; |
| if (level == 0) { |
| break; |
| } |
| writer.writeEndElement(); |
| } else { |
| break; |
| } |
| } |
| |
| writer.writeEndDocument(); |
| writer.flush(); |
| |
| bufferStream.close(); |
| } catch (XMLStreamException xse) { |
| // remove temp file |
| bufferStream.destroy(xse); |
| throw xse; |
| } catch (IOException ioe) { |
| // remove temp file |
| bufferStream.destroy(ioe); |
| throw ioe; |
| } |
| |
| XMLUtils.next(parser); |
| |
| return bufferStream; |
| } |
| |
| /** |
| * Copies a XML start element. |
| */ |
| private static void copyStartElement(XMLStreamReader parser, XMLStreamWriter writer) throws XMLStreamException { |
| String namespaceUri = parser.getNamespaceURI(); |
| String prefix = parser.getPrefix(); |
| String localName = parser.getLocalName(); |
| |
| // write start element |
| if (namespaceUri != null) { |
| if ((prefix == null) || (prefix.length() == 0)) { |
| writer.writeStartElement(localName); |
| } else { |
| writer.writeStartElement(prefix, localName, namespaceUri); |
| } |
| } else { |
| writer.writeStartElement(localName); |
| } |
| |
| // set namespaces |
| for (int i = 0; i < parser.getNamespaceCount(); i++) { |
| addNamespace(writer, parser.getNamespacePrefix(i), parser.getNamespaceURI(i)); |
| } |
| addNamespaceIfMissing(writer, prefix, namespaceUri); |
| |
| // write attributes |
| for (int i = 0; i < parser.getAttributeCount(); i++) { |
| String attrNamespaceUri = parser.getAttributeNamespace(i); |
| String attrPrefix = parser.getAttributePrefix(i); |
| String attrName = parser.getAttributeLocalName(i); |
| String attrValue = parser.getAttributeValue(i); |
| |
| if ((attrNamespaceUri == null) || (attrNamespaceUri.trim().length() == 0)) { |
| writer.writeAttribute(attrName, attrValue); |
| } else if ((attrPrefix == null) || (attrPrefix.trim().length() == 0)) { |
| writer.writeAttribute(attrNamespaceUri, attrName, attrValue); |
| } else { |
| addNamespaceIfMissing(writer, attrPrefix, attrNamespaceUri); |
| writer.writeAttribute(attrPrefix, attrNamespaceUri, attrName, attrValue); |
| } |
| } |
| } |
| |
| /** |
| * Checks if the given prefix is assigned to the given namespace. |
| */ |
| @SuppressWarnings("unchecked") |
| private static void addNamespaceIfMissing(XMLStreamWriter writer, String prefix, String namespaceUri) |
| throws XMLStreamException { |
| if ((namespaceUri == null) || (namespaceUri.trim().length() == 0)) { |
| return; |
| } |
| |
| if (prefix == null) { |
| prefix = ""; |
| } |
| |
| Iterator<String> iter = writer.getNamespaceContext().getPrefixes(namespaceUri); |
| if (iter == null) { |
| return; |
| } |
| |
| while (iter.hasNext()) { |
| String p = iter.next(); |
| if ((p != null) && (p.equals(prefix))) { |
| return; |
| } |
| } |
| |
| addNamespace(writer, prefix, namespaceUri); |
| } |
| |
| /** |
| * Adds a namespace to a XML element. |
| */ |
| private static void addNamespace(XMLStreamWriter writer, String prefix, String namespaceUri) |
| throws XMLStreamException { |
| if ((prefix == null) || (prefix.trim().length() == 0)) { |
| writer.setDefaultNamespace(namespaceUri); |
| writer.writeDefaultNamespace(namespaceUri); |
| } else { |
| writer.setPrefix(prefix, namespaceUri); |
| writer.writeNamespace(prefix, namespaceUri); |
| } |
| } |
| } |