blob: d8b138496a23909be1132c3810cbbe70a51c805f [file] [log] [blame]
/*
* 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.openjpa.xmlstore;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.io.Writer;
import java.lang.reflect.Constructor;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.security.AccessController;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import javax.xml.parsers.SAXParser;
import org.apache.openjpa.enhance.PCRegistry;
import org.apache.openjpa.lib.util.Base16Encoder;
import org.apache.openjpa.lib.util.J2DoPrivHelper;
import org.apache.openjpa.lib.xml.XMLFactory;
import org.apache.openjpa.lib.xml.XMLWriter;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.FieldMetaData;
import org.apache.openjpa.meta.JavaTypes;
import org.apache.openjpa.util.Id;
import org.apache.openjpa.util.InternalException;
import org.apache.openjpa.util.OpenJPAException;
import org.apache.openjpa.util.StoreException;
import org.apache.openjpa.util.UnsupportedException;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
/**
* Stores {@link ObjectData} objects by serializing a collection
* of them into and out of an XML file.
*/
public class XMLFileHandler {
private final XMLConfiguration _conf;
/**
* Constructor; supply configuration.
*/
public XMLFileHandler(XMLConfiguration conf) {
_conf = conf;
}
/**
* Loads all instances of <code>meta</code> into a list of objects.
* The given <code>meta</code> must represent a least-derived
* persistence-capable type.
*/
public Collection load(ClassMetaData meta) {
File f = getFile(meta);
if (!(AccessController.doPrivileged(
J2DoPrivHelper.existsAction(f))).booleanValue() ||
(AccessController.doPrivileged(
J2DoPrivHelper.lengthAction(f))).longValue() == 0)
return Collections.EMPTY_SET;
try {
return read(f);
} catch (OpenJPAException ke) {
throw ke;
} catch (Exception e) {
throw new StoreException(e);
}
}
/**
* Read a collection of {@link ObjectData}s from the contents of the
* given file.
*/
private Collection read(File f)
throws Exception {
// parse the file and return the objects it contains
SAXParser parser = XMLFactory.getSAXParser(false, false);
ObjectDataHandler handler = new ObjectDataHandler(_conf);
parser.parse(f, handler);
return handler.getExtent();
}
/**
* Returns a {@link File} object that <code>meta</code> lives
* in. This implementation creates a filename from the full class
* name of the type identified by <code>meta</code>, and returns
* a {@link File} object that has this filename and whose base
* directory is the URL identified by the <code>ConnectionURL</code>
* configuration property.
*/
private File getFile(ClassMetaData meta) {
if (_conf.getConnectionURL() == null) {
throw new InternalException("Invalid ConnectionURL");
}
File baseDir = new File(_conf.getConnectionURL());
return new File(baseDir, meta.getDescribedType().getName());
}
/**
* Stores all instances in <code>datas</code> into the appropriate file,
* as dictated by <code>meta</code>.
*
* @param meta the least-derived type of the instances being stored
* @param datas a collection of {@link ObjectData} instances, each
* of which represents an object of type <code>meta</code>
*/
public void store(ClassMetaData meta, Collection datas) {
if (meta.getPCSuperclass() != null)
throw new InternalException();
File f = getFile(meta);
if (!(AccessController.doPrivileged(
J2DoPrivHelper.existsAction(f.getParentFile()))).booleanValue())
AccessController.doPrivileged(
J2DoPrivHelper.mkdirsAction(f.getParentFile()));
FileWriter fw = null;
try {
fw = new FileWriter(f);
write(datas, fw);
} catch (OpenJPAException ke) {
throw ke;
} catch (Exception e) {
throw new StoreException(e);
} finally {
if (fw != null)
try {
fw.close();
} catch (IOException ioe) {
}
}
}
/**
* Write the given collection of {@link ObjectData}s to the given file.
*/
private void write(Collection datas, FileWriter fw)
throws Exception {
// create an XML pretty printer to write out the objects
Writer out = new XMLWriter(fw);
// start the file; the root node is an "extent"
out.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
out.write("<extent>");
// run through each object in the collection
for (Iterator itr = datas.iterator(); itr.hasNext();) {
ObjectData obj = (ObjectData) itr.next();
ClassMetaData meta = obj.getMetaData();
// write out the "object" element start
out.write("<object class=\"");
out.write(meta.getDescribedType().getName());
out.write("\" oid=\"");
out.write(obj.getId().toString());
out.write("\" version=\"");
out.write(obj.getVersion().toString());
out.write("\">");
// run through each field writing out the value
FieldMetaData[] fmds = meta.getFields();
for (int i = 0; i < fmds.length; i++) {
if (fmds[i].getManagement() != FieldMetaData.MANAGE_PERSISTENT)
continue;
out.write("<field name=\"");
out.write(fmds[i].getName());
out.write("\">");
// write out the field data depending upon type
switch (fmds[i].getTypeCode()) {
case JavaTypes.COLLECTION:
case JavaTypes.ARRAY:
Collection c = (Collection) obj.getField(i);
if (c == null)
break;
// write out each of the elements
int elemType = fmds[i].getElement().getTypeCode();
for (Iterator ci = c.iterator(); ci.hasNext();) {
out.write("<element>");
writeDataValue(out, elemType, ci.next());
out.write("</element>");
}
break;
case JavaTypes.MAP:
Map m = (Map) obj.getField(i);
if (m == null)
break;
// write out each of the map entries
Collection entries = m.entrySet();
int keyType = fmds[i].getKey().getTypeCode();
int valueType = fmds[i].getElement().getTypeCode();
for (Iterator ei = entries.iterator(); ei.hasNext();) {
Map.Entry e = (Map.Entry) ei.next();
out.write("<key>");
writeDataValue(out, keyType, e.getKey());
out.write("</key>");
out.write("<value>");
writeDataValue(out, valueType, e.getValue());
out.write("</value>");
}
break;
default:
writeDataValue(out, fmds[i].getTypeCode(),
obj.getField(i));
}
out.write("</field>");
}
out.write("</object>");
}
out.write("</extent>");
}
/**
* Write out the data value. This method writes nulls as "null",
* serializes (using Java serialization and base16 encoding) out non-
* primitives/boxed primitives and non-persistent types, and writes
* primitives/boxed primitives and oids using their toString.
*/
public void writeDataValue(Writer out, int type, Object val)
throws IOException {
// write nulls as "null"
if (val == null) {
out.write("null");
return;
}
switch (type) {
case JavaTypes.OBJECT:
case JavaTypes.OID:
if (!(val instanceof Serializable))
throw new UnsupportedException(
"Cannot store non-serializable,"
+ " non-persistence-capable value: " + val);
// serialize out the object and encode the result with base16
ByteArrayOutputStream baos = new ByteArrayOutputStream(8192);
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(val);
oos.close();
out.write(Base16Encoder.encode(baos.toByteArray()));
break;
case JavaTypes.CHAR:
case JavaTypes.CHAR_OBJ:
// quote chars so we can distinguish whitespace chars; special
// case for \0
char c = ((Character) val).charValue();
out.write("'");
if (c == '\0')
out.write("0x0");
else
out.write(XMLEncoder.encode(val.toString()));
out.write("'");
break;
case JavaTypes.STRING:
// quote strings so we can distinguish leading and trailing
// whitespace
out.write("\"");
out.write(XMLEncoder.encode(val.toString()));
out.write("\"");
break;
case JavaTypes.PC:
case JavaTypes.PC_UNTYPED:
// write the type of oid object + ':' + oid string
out.write(val.getClass().getName());
out.write(':');
out.write(XMLEncoder.encode(val.toString()));
break;
default:
// must be a number of simple type; no need to encode
out.write(val.toString());
}
}
/**
* Used to reconstruct {@link ObjectData} instances from SAX events.
*/
private static class ObjectDataHandler
extends DefaultHandler {
private static final Class[] ARGS = new Class[]{ String.class };
private final XMLConfiguration _conf;
private final Collection _extent = new ArrayList();
// parse state
private ObjectData _object;
private FieldMetaData _fmd;
private Object _fieldVal;
private Object _keyVal;
private StringBuffer _buf;
/**
* Constructor; supply configuration.
*/
public ObjectDataHandler(XMLConfiguration conf) {
_conf = conf;
}
/**
* Return the results of the parsing.
*/
public Collection getExtent() {
return _extent;
}
@Override
public void startElement(String uri, String localName, String qName,
Attributes attrs)
throws SAXException {
try {
startElement(qName, attrs);
} catch (RuntimeException re) {
throw re;
} catch (SAXException se) {
throw se;
} catch (Exception e) {
throw new SAXException(e);
}
}
private void startElement(String qName, Attributes attrs)
throws Exception {
if ("object".equals(qName)) { // object
// get the metadata for the type we're reading
String type = attrs.getValue("class");
ClassMetaData meta = _conf.getMetaDataRepositoryInstance().
getMetaData(classForName(type), null, true);
// construct the oid object
Object oid;
if (meta.getIdentityType() == ClassMetaData.ID_DATASTORE)
oid = new Id(attrs.getValue("oid"), _conf, null);
else
oid = PCRegistry.newObjectId(meta.getDescribedType(),
attrs.getValue("oid"));
// create an ObjectData that will contain the information
// for this instance, and set the version
_object = new ObjectData(oid, meta);
_object.setVersion(new Long(attrs.getValue("version")));
}
else if ("field".equals(qName)) { // field
// start parsing a field element: for container types,
// initialize the container; for other types, initialize a
// buffer
_fmd = _object.getMetaData().getField(attrs.getValue("name"));
switch (_fmd.getTypeCode()) {
case JavaTypes.COLLECTION:
case JavaTypes.ARRAY:
_fieldVal = new ArrayList();
break;
case JavaTypes.MAP:
_fieldVal = new HashMap();
break;
default:
_buf = new StringBuffer();
}
}
else if ("element".equals(qName) ||
"key".equals(qName) ||
"value".equals(qName) ) { // field
// initialize a buffer for the element value
_buf = new StringBuffer();
}
}
@Override
public void endElement(String uri, String localName, String qName)
throws SAXException {
try {
endElement(qName);
} catch (RuntimeException re) {
throw re;
} catch (SAXException se) {
throw se;
} catch (Exception e) {
throw new SAXException(e);
}
}
private void endElement(String qName)
throws Exception {
Object val;
if ("object".equals(qName)) {
// add the object to our results
_extent.add(_object);
}
else if ("field".equals(qName)) {
switch (_fmd.getTypeCode()) {
case JavaTypes.COLLECTION:
case JavaTypes.ARRAY:
case JavaTypes.MAP:
// field value already constructed
break;
default:
// construct the field value from text within the
// element
_fieldVal = fromXMLString(_fmd.getTypeCode(),
_fmd.getTypeMetaData(), _buf.toString());
}
// set the field value into the object being parsed
_object.setField(_fmd.getIndex(), _fieldVal);
}
else if ("element".equals(qName)) {
// cache element value
val = fromXMLString(_fmd.getElement().getTypeCode(),
_fmd.getElement().getTypeMetaData(), _buf.toString());
((Collection) _fieldVal).add(val);
}
else if ("key".equals(qName)) {
// cache key value
_keyVal = fromXMLString(_fmd.getKey().getTypeCode(),
_fmd.getKey().getTypeMetaData(), _buf.toString());
}
else if ("value".equals(qName)) {
// create value and put cached key and value into map
val = fromXMLString(_fmd.getElement().getTypeCode(),
_fmd.getElement().getTypeMetaData(), _buf.toString());
Map map = (Map) _fieldVal;
map.put(_keyVal, val);
}
// don't cache text between elements
_buf = null;
}
@Override
public void characters(char[] ch, int start, int length) {
if (_buf != null)
_buf.append(ch, start, length);
}
/**
* Recreate a field value from its XML string.
*/
public Object fromXMLString(int type, ClassMetaData rel, String str)
throws Exception {
str = str.trim();
if (str.equals("null"))
return null;
switch (type) {
case JavaTypes.BOOLEAN:
case JavaTypes.BOOLEAN_OBJ:
return Boolean.valueOf(str);
case JavaTypes.BYTE:
case JavaTypes.BYTE_OBJ:
return new Byte(str);
case JavaTypes.CHAR:
case JavaTypes.CHAR_OBJ:
// strip quotes; special case for 0x0
str = str.substring(1, str.length() - 1);
if (str.equals("0x0"))
return new Character('\0');
return new Character(XMLEncoder.decode(str).charAt(0));
case JavaTypes.DOUBLE:
case JavaTypes.DOUBLE_OBJ:
return new Double(str);
case JavaTypes.FLOAT:
case JavaTypes.FLOAT_OBJ:
return new Float(str);
case JavaTypes.INT:
case JavaTypes.INT_OBJ:
return new Integer(str);
case JavaTypes.LONG:
case JavaTypes.LONG_OBJ:
return new Long(str);
case JavaTypes.SHORT:
case JavaTypes.SHORT_OBJ:
return new Short(str);
case JavaTypes.NUMBER:
case JavaTypes.BIGDECIMAL:
return new BigDecimal(str);
case JavaTypes.BIGINTEGER:
return new BigInteger(str);
case JavaTypes.STRING:
// strip quotes
str = str.substring(1, str.length() - 1);
return XMLEncoder.decode(str);
case JavaTypes.OBJECT:
case JavaTypes.OID:
// convert the chars into bytes, and run them through an
// ObjectInputStream in order to get the serialized object
byte[] bytes = Base16Encoder.decode(str);
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais);
Object data = ois.readObject();
ois.close();
return data;
case JavaTypes.DATE:
return new Date(str);
case JavaTypes.PC:
case JavaTypes.PC_UNTYPED:
// parse out oid class name and value
int idx = str.indexOf(':');
Class idClass = classForName(str.substring(0, idx));
String idStr = XMLEncoder.decode(str.substring(idx + 1));
Constructor cons = idClass.getConstructor(ARGS);
return cons.newInstance(new Object[]{ idStr });
case JavaTypes.LOCALE:
int under1 = str.indexOf('_');
if (under1 == -1)
return (new Locale(str, ""));
int under2 = str.indexOf('_', under1 + 1);
if (under2 == -1)
return new Locale(str.substring(0, under1),
str.substring(under1 + 1));
String lang = str.substring(0, under1);
String country = str.substring(under1 + 1, under2);
String variant = str.substring(under2 + 1);
return new Locale(lang, country, variant);
default:
throw new InternalException();
}
}
/**
* Return the class for the specified name.
*/
private Class classForName(String name)
throws Exception {
ClassLoader loader = _conf.getClassResolverInstance().
getClassLoader(getClass(), null);
return Class.forName(name, true, loader);
}
}
/**
* Utility methods for encoding and decoding XML strings.
*/
private static class XMLEncoder {
/**
* Encode the given string as XML text.
*/
public static String encode(String s) {
StringBuffer buf = null;
for (int i = 0; i < s.length(); i++) {
switch (s.charAt(i)) {
case '<':
buf = initializeBuffer(buf, s, i);
buf.append("&lt;");
break;
case '>':
buf = initializeBuffer(buf, s, i);
buf.append("&gt;");
break;
case '&':
buf = initializeBuffer(buf, s, i);
buf.append("&amp;");
break;
default:
if (buf != null)
buf.append(s.charAt(i));
}
}
if (buf != null)
return buf.toString();
return s;
}
/**
* Decode the given XML string.
*/
public static String decode(String s) {
StringBuffer buf = null;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '&' && s.length() > i + 3) {
if ((s.charAt(i + 1) == 'l' || s.charAt(i + 1) == 'g')
&& s.charAt(i + 2) == 't' && s.charAt(i + 3) == ';') {
// &lt; or &gt;
buf = initializeBuffer(buf, s, i);
c = (s.charAt(i) == 'l') ? '<' : '>';
i += 3;
} else if (s.length() > i + 4 && s.charAt(i + 1) == 'a'
&& s.charAt(i + 2) == 'm' && s.charAt(i + 3) == 'p'
&& s.charAt(i + 4) == ';') {
// &amp;
buf = initializeBuffer(buf, s, i);
c = '&';
i += 4;
}
}
if (buf != null)
buf.append(c);
}
if (buf != null)
return buf.toString();
return s;
}
/**
* Create and initialize a buffer for the encoded/decoded string if
* needed.
*/
private static StringBuffer initializeBuffer(StringBuffer buf,
String s, int i) {
if (buf == null) {
buf = new StringBuffer();
if (i > 0)
buf.append (s.substring (0, i));
}
return buf;
}
}
}