001/** 002 * Copyright 2015 DuraSpace, Inc. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.fcrepo.serialization; 017 018import org.apache.commons.io.IOUtils; 019import org.fcrepo.kernel.models.FedoraResource; 020import org.springframework.stereotype.Component; 021 022import javax.jcr.ImportUUIDBehavior; 023import javax.jcr.Node; 024import javax.jcr.RepositoryException; 025import javax.jcr.Session; 026import javax.xml.namespace.QName; 027import javax.xml.stream.XMLEventReader; 028import javax.xml.stream.XMLInputFactory; 029import javax.xml.stream.XMLStreamException; 030import javax.xml.stream.events.Attribute; 031import javax.xml.stream.events.StartElement; 032import javax.xml.stream.events.XMLEvent; 033import java.io.File; 034import java.io.FileInputStream; 035import java.io.FileNotFoundException; 036import java.io.FileOutputStream; 037import java.io.IOException; 038import java.io.InputStream; 039import java.io.OutputStream; 040 041import static org.fcrepo.kernel.FedoraJcrTypes.FEDORA_BINARY; 042import static org.fcrepo.kernel.FedoraJcrTypes.FEDORA_NON_RDF_SOURCE_DESCRIPTION; 043 044/** 045 * Serialize a FedoraObject using the modeshape-provided JCR/XML format 046 * 047 * @author cbeer 048 */ 049@Component 050public class JcrXmlSerializer extends BaseFedoraObjectSerializer { 051 052 @Override 053 public String getKey() { 054 return JCR_XML; 055 } 056 057 @Override 058 public String getMediaType() { 059 return "application/xml"; 060 } 061 062 @Override 063 public boolean canSerialize(final FedoraResource resource) { 064 return (!(resource.hasType(FEDORA_BINARY) || resource.hasType(FEDORA_NON_RDF_SOURCE_DESCRIPTION) 065 || resource.isFrozenResource())); 066 } 067 068 @Override 069 /** 070 * Serialize JCR/XML with options for recurse and skipBinary. 071 * @param obj 072 * @param out 073 * @param skipBinary 074 * @param recurse 075 * @throws RepositoryException 076 * @throws IOException 077 */ 078 public void serialize(final FedoraResource obj, 079 final OutputStream out, 080 final boolean skipBinary, 081 final boolean recurse) 082 throws RepositoryException, IOException, InvalidSerializationFormatException { 083 if (obj.hasType(FEDORA_BINARY)) { 084 throw new InvalidSerializationFormatException("Cannot serialize decontextualized binary content."); 085 } 086 if (obj.isFrozenResource()) { 087 throw new InvalidSerializationFormatException("Cannot serialize historic versions."); 088 } 089 final Node node = obj.getNode(); 090 // jcr/xml export system view implemented for noRecurse: 091 // exportSystemView(String absPath, OutputStream out, boolean skipBinary, boolean noRecurse) 092 node.getSession().exportSystemView(obj.getPath(), out, skipBinary, !recurse); 093 } 094 095 @Override 096 public void deserialize(final Session session, final String path, 097 final InputStream stream) throws RepositoryException, IOException, InvalidSerializationFormatException { 098 099 final File temp = File.createTempFile("fcrepo-unsanitized-input", ".xml"); 100 final FileOutputStream fos = new FileOutputStream(temp); 101 try { 102 IOUtils.copy(stream, fos); 103 } finally { 104 IOUtils.closeQuietly(stream); 105 IOUtils.closeQuietly(fos); 106 } 107 validateJCRXML(temp); 108 try (final InputStream tmpInputStream = new TempFileInputStream(temp)) { 109 session.importXML(path, tmpInputStream, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW); 110 } catch (UnsupportedOperationException | IllegalArgumentException e) { 111 // These come from ModeShape when there's various problems in the formatting of the XML 112 // that are not caught by JCRXMLValidatingInputStreamBridge. 113 throw new InvalidSerializationFormatException("Invalid JCR/XML." 114 + (e.getMessage() != null ? " (" + e.getMessage() + ")" : "")); 115 } 116 } 117 118 private void validateJCRXML(final File file) throws InvalidSerializationFormatException, IOException { 119 int depth = 0; 120 try (final FileInputStream fis = new FileInputStream(file)) { 121 final XMLEventReader reader = XMLInputFactory.newFactory().createXMLEventReader(fis); 122 while (reader.hasNext()) { 123 final XMLEvent event = reader.nextEvent(); 124 if (event.isStartElement()) { 125 depth ++; 126 final StartElement startElement = event.asStartElement(); 127 final Attribute nameAttribute = startElement.getAttributeByName( 128 new QName("http://www.jcp.org/jcr/sv/1.0", "name")); 129 if (depth == 1 && nameAttribute != null && "jcr:content".equals(nameAttribute.getValue())) { 130 throw new InvalidSerializationFormatException( 131 "Cannot import JCR/XML starting with content node."); 132 } 133 if (depth == 1 && nameAttribute != null && "jcr:frozenNode".equals(nameAttribute.getValue())) { 134 throw new InvalidSerializationFormatException( 135 "Cannot import historic versions."); 136 } 137 final QName name = startElement.getName(); 138 if (!(name.getNamespaceURI().equals("http://www.jcp.org/jcr/sv/1.0") 139 && (name.getLocalPart().equals("node") || name.getLocalPart().equals("property") 140 || name.getLocalPart().equals("value")))) { 141 throw new InvalidSerializationFormatException( 142 "Unrecognized element \"" + name.toString() + "\", in import XML."); 143 } 144 } else { 145 if (event.isEndElement()) { 146 depth --; 147 } 148 } 149 } 150 reader.close(); 151 } catch (XMLStreamException e) { 152 throw new InvalidSerializationFormatException("Unable to parse XML" 153 + (e.getMessage() != null ? " (" + e.getMessage() + ")." : ".")); 154 } 155 } 156 157 /** 158 * A FileInputStream that deletes the file when closed. 159 */ 160 private static final class TempFileInputStream extends FileInputStream { 161 162 private File f; 163 164 /** 165 * A constructor whose passed file's content is exposed by this 166 * TempFileInputStream, and which will be deleted when this 167 * InputStream is closed. 168 * @param f 169 * @throws FileNotFoundException 170 */ 171 public TempFileInputStream(final File f) throws FileNotFoundException { 172 super(f); 173 this.f = f; 174 } 175 176 @Override 177 public void close() throws IOException { 178 try { 179 super.close(); 180 } finally { 181 if (f != null) { 182 f.delete(); 183 f = null; 184 } 185 } 186 } 187 } 188}