/*
* Copyright 2009-2016 Tilmann Zaeschke. All rights reserved.
*
* This file is part of ZooDB.
*
* ZooDB is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ZooDB is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ZooDB. If not, see <http://www.gnu.org/licenses/>.
*
* See the README and COPYING files for further information.
*/
package org.zoodb.internal.model1p;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import org.zoodb.api.impl.ZooPC;
import org.zoodb.internal.DataSerializer;
import org.zoodb.internal.DataSink;
import org.zoodb.internal.GenericObject;
import org.zoodb.internal.SerializerTools;
import org.zoodb.internal.ZooClassDef;
import org.zoodb.internal.ZooFieldDef;
import org.zoodb.internal.client.AbstractCache;
import org.zoodb.internal.server.ObjectWriter;
import org.zoodb.internal.server.index.BitTools;
import org.zoodb.internal.server.index.LongLongIndex;
import org.zoodb.internal.server.index.SchemaIndex.SchemaIndexEntry;
import org.zoodb.internal.util.DBLogger;
import org.zoodb.internal.util.Util;
/**
* A data sink serializes objects of a given class. It can be backed either by a file- or
* in-memory-storage, or in future by a network channel through which data is sent to a server.
*
* Each sink handles objects of one class only. Therefore sinks can be associated with
* ZooClassDefs and PCContext instances.
*
* TODO
* get the schema indices only once, then update them in case schema/indices evolve.
*
* @author Tilmann Zaschke
*/
public class DataSink1P implements DataSink {
private static final int BUFFER_SIZE = 1000;
private final Node1P node;
private final ZooClassDef cls;
private final DataSerializer ds;
private final ObjectWriter ow;
private final ZooPC[] buffer = new ZooPC[BUFFER_SIZE];
private int bufferCnt = 0;
private final GenericObject[] bufferGO = new GenericObject[BUFFER_SIZE];
private int bufferGOCnt = 0;
private boolean isStarted = false;
private final ArrayList<Pair>[] fieldUpdateBuffer;
private static class Pair {
private final ZooPC pc;
private final long value;
public Pair(ZooPC pc, long value) {
this.pc = pc;
this.value = value;
}
}
@SuppressWarnings("unchecked")
public DataSink1P(Node1P node, AbstractCache cache, ZooClassDef cls, ObjectWriter out) {
this.node = node;
this.cls = cls;
this.fieldUpdateBuffer = new ArrayList[cls.getAllFields().length];
this.ds = new DataSerializer(out, cache, node);
this.ow = out;
}
private void preWrite() {
if (!isStarted) {
ow.newPage();
isStarted = true;
}
}
/* (non-Javadoc)
* @see org.zoodb.internal.model1p.DataSink#write(org.zoodb.api.impl.ZooPC)
*/
@Override
public void write(ZooPC obj) {
if (obj.getClass() == GenericObject.class) {
writeGeneric((GenericObject) obj);
return;
}
preWrite();
//write object
ds.writeObject(obj, cls);
//updated index
//This is buffered to reduce look-ups to find field indices.
buffer[bufferCnt++] = obj;
if (bufferCnt == BUFFER_SIZE) {
flushBuffer();
}
}
@Override
public void writeGeneric(GenericObject obj) {
preWrite();
//write object
ds.writeObject(obj, cls);
//updated index
//This is buffered to reduce look-ups to find field indices.
bufferGO[bufferGOCnt++] = obj;
if (bufferGOCnt == BUFFER_SIZE) {
flushBuffer();
}
}
@Override
public void reset() {
if (isStarted) {
ow.flush(); //TODO reset?
//To avoid memory leaks...
Arrays.fill(buffer, null);
bufferCnt = 0;
Arrays.fill(fieldUpdateBuffer, null);
if (bufferGOCnt > 0) {
Arrays.fill(bufferGO, null);
bufferGOCnt = 0;
}
isStarted = false;
}
}
/* (non-Javadoc)
* @see org.zoodb.internal.model1p.DataSink#flush()
*/
@Override
public void flush() {
if (isStarted) {
flushBuffer();
ow.flush();
//To avoid memory leaks...
Arrays.fill(buffer, null);
isStarted = false;
}
}
private void flushBuffer() {
updateFieldIndices();
bufferCnt = 0;
if (bufferGOCnt > 0) {
updateFieldIndicesGO();
bufferGOCnt = 0;
}
//Now perform all index updates that previously failed (because of unique index collisions).
for (int i = 0; i < fieldUpdateBuffer.length; i++) {
ArrayList<Pair> a = fieldUpdateBuffer[i];
if (a != null) {
ZooFieldDef field = cls.getAllFields()[i];
if (!field.isIndexed()) {
continue;
}
SchemaIndexEntry schemaTop = node.getSchemaIE(field.getDeclaringType());
LongLongIndex fieldInd = (LongLongIndex) schemaTop.getIndex(field);
for (Pair p: a) {
//This should now work, all objects have been removed
//Refreshing is also not an issue, we already have the index-value
if (field.isString()) {
//TODO this does not work for GOs... Actually, it might, because
//GOs extend ZooPC... for ZooPC see Test_091 -> Issue 55
String str = getString(p.pc, field);
//ignore 'null' ?!?!? Why? No reason, just a definition we make here...
if (str != null) {
Iterator<ZooPC> it =
node.readObjectFromIndex(field, p.value, p.value, true);
while (it.hasNext()) {
ZooPC o2 = it.next();
String s2 = getString(o2, field);
if (str.equals(s2)) {
long oid2 = o2.jdoZooGetOid();
throw DBLogger.newUser("Unique index clash by value of field "
+ field.getName() + "=" + p.value + " of new object "
+ Util.oidToString(p.pc.jdoZooGetOid()) + " with "
+ Util.oidToString(oid2));
}
}
}
fieldInd.insertLong(p.value, p.pc.jdoZooGetOid());
} else if (!fieldInd.insertLongIfNotSet(p.value, p.pc.jdoZooGetOid())) {
long oid2 = fieldInd.iterator(p.value, p.value).next().getValue();
throw DBLogger.newUser("Unique index clash by value of field "
+ field.getName() + "=" + p.value + " of new object "
+ Util.oidToString(p.pc.jdoZooGetOid()) + " with "
+ Util.oidToString(oid2));
}
}
fieldUpdateBuffer[i] = null;
}
}
}
private String getString(ZooPC pc, ZooFieldDef field) {
if (pc instanceof GenericObject) {
GenericObject go = (GenericObject) pc;
return (String) go.getField(field);
} else {
try {
return (String) field.getJavaField().get(pc);
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
private void updateFieldIndices() {
final ZooPC[] buffer = this.buffer;
final int bufferCnt = this.bufferCnt;
//update field indices
//We hook into the makeDirty call to store the previous value of the field such that we
//can remove it efficiently from the index.
//Or is there another way, maybe by updating an (or the) index?
int iInd = -1;
int iField = -1;
for (ZooFieldDef field: cls.getAllFields()) {
iField++;
if (!field.isIndexed()) {
continue;
}
iInd++;
//For now we define that an index is shared by all classes and sub-classes that have
//a matching field. So there is only one index which is defined in the top-most class
SchemaIndexEntry schemaTop = node.getSchemaIE(field.getDeclaringType());
LongLongIndex fieldInd = (LongLongIndex) schemaTop.getIndex(field);
try {
Field jField = field.getJavaField();
for (int i = 0; i < bufferCnt; i++) {
ZooPC co = buffer[i];
final long l;
String str = null;
if (field.isString()) {
str = (String)jField.get(co);
l = BitTools.toSortableLong(str);
} else if (field.isPersistentType()) {
ZooPC pc = (ZooPC)jField.get(co);
l = BitTools.toSortableLong(pc);
} else {
l = SerializerTools.primitiveFieldToLong(co, jField, field.getPrimitiveType());
}
if (!co.jdoZooIsNew()) {
long lOld = co.jdoZooGetBackup().getA()[iInd];
//Only update if value did not change
if (lOld == l) {
if (field.isString()) {
String str2 = (String) co.jdoZooGetBackup().getB()[iInd];
if ((str==null && str2 == null) || str.equals(str2)) {
//no update here...
continue;
}
} else {
//no update here...
continue;
}
}
fieldInd.removeLong(lOld, co.jdoZooGetOid());
}
if (field.isIndexUnique()) {
if (field.isString()) {
//always buffer string updates, because verifying collisions is costly
bufferIndexUpdate(iField, co, l);
} else {
if (!fieldInd.insertLongIfNotSet(l, co.jdoZooGetOid())) {
bufferIndexUpdate(iField, co, l);
}
}
} else {
fieldInd.insertLong(l, co.jdoZooGetOid());
}
}
} catch (SecurityException e) {
throw DBLogger.newFatal("Error accessing field: " + field.getName(), e);
} catch (IllegalArgumentException e) {
throw DBLogger.newFatal("Error accessing field: " + field.getName(), e);
} catch (IllegalAccessException e) {
throw DBLogger.newFatal("Error accessing field: " + field.getName(), e);
}
}
}
private void bufferIndexUpdate(int iField, ZooPC pc, long l) {
if (fieldUpdateBuffer[iField] == null) {
fieldUpdateBuffer[iField] = new ArrayList<Pair>();
}
fieldUpdateBuffer[iField].add(new Pair(pc, l));
}
private void updateFieldIndicesGO() {
final GenericObject[] buffer = this.bufferGO;
final int bufferCnt = this.bufferGOCnt;
//update field indices
//We hook into the makeDirty call to store the previous value of the field such that we
//can remove it efficiently from the index.
//Or is there another way, maybe by updating an (or the) index?
int iInd = -1;
int iField = -1;
for (ZooFieldDef field: cls.getAllFields()) {
iField++;
if (!field.isIndexed()) {
continue;
}
iInd++;
//For now we define that an index is shared by all classes and sub-classes that have
//a matching field. So there is only one index which is defined in the top-most class
SchemaIndexEntry schemaTop = node.getSchemaIE(field.getDeclaringType());
LongLongIndex fieldInd = (LongLongIndex) schemaTop.getIndex(field);
try {
for (int i = 0; i < bufferCnt; i++) {
GenericObject co = buffer[i];
final long l;
String str = null;
if (field.isString()) {
l = (Long)co.getFieldRaw(iField);
str = (String) co.getField(field);
} else if (field.isPersistentType()) {
Object oid = co.getFieldRaw(field.getFieldPos());
l = (oid == null ? BitTools.NULL : (long)oid);
} else {
Object primO = co.getFieldRaw(iField);
l = SerializerTools.primitiveToLong(primO, field.getPrimitiveType());
}
if (!co.jdoZooIsNew()) {
long lOld = co.jdoZooGetBackup().getA()[iInd];
//Only update if value did not change
if (lOld == l) {
if (field.isString()) {
String str2 = (String) co.jdoZooGetBackup().getB()[iInd];
if ((str==null && str2 == null) || str.equals(str2)) {
//no update here...
continue;
}
} else {
//no update here...
continue;
}
}
fieldInd.removeLong(lOld, co.getOid());
}
if (field.isIndexUnique()) {
if (!fieldInd.insertLongIfNotSet(l, co.getOid())) {
bufferIndexUpdate(iField, co, l);
}
} else if (field.isString()) {
//always buffer string updates, because verifying collisions is costly
bufferIndexUpdate(iField, co, l);
} else {
fieldInd.insertLong(l, co.getOid());
}
}
} catch (IllegalArgumentException e) {
throw DBLogger.newFatal("Error accessing field: " + field.getName(), e);
}
}
}
}