/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2004-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library 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
* Lesser General Public License for more details.
*/
package org.geotools.data.sort;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.RandomAccessFile;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import org.geotools.data.Query;
import org.geotools.data.collection.ListFeatureCollection;
import org.geotools.data.simple.DelegateSimpleFeatureReader;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.data.simple.SimpleFeatureReader;
import org.geotools.factory.Hints;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.filter.sort.SortBy;
import org.opengis.filter.sort.SortOrder;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.io.WKBWriter;
class MergeSortDumper {
static final boolean canSort(SimpleFeatureType schema, SortBy[] sortBy) {
if (sortBy == SortBy.UNSORTED) {
return true;
}
// check all attributes are serializable
for (AttributeDescriptor ad : schema.getAttributeDescriptors()) {
Class<?> binding = ad.getType().getBinding();
if (!Serializable.class.isAssignableFrom(binding)) {
return false;
}
}
// check all sorting attributes are comparable
for (SortBy sb : sortBy) {
if (sb != SortBy.NATURAL_ORDER && sb != SortBy.REVERSE_ORDER) {
AttributeDescriptor ad = schema.getDescriptor(sb.getPropertyName()
.getPropertyName());
Class<?> binding = ad.getType().getBinding();
if (ad == null || !Comparable.class.isAssignableFrom(binding)
|| Geometry.class.isAssignableFrom(binding)) {
return false;
}
}
}
return true;
}
static SimpleFeatureReader getDelegateReader(SimpleFeatureReader reader, Query query)
throws IOException {
Hints hints = query.getHints();
int maxFeatures = 1000;
if (hints != null && hints.get(Hints.MAX_MEMORY_SORT) != null) {
maxFeatures = (Integer) hints.get(Hints.MAX_MEMORY_SORT);
} else if (Hints.getSystemDefault(Hints.MAX_MEMORY_SORT) != null) {
maxFeatures = (Integer) Hints.getSystemDefault(Hints.MAX_MEMORY_SORT);
}
return getDelegateReader(reader, query.getSortBy(), maxFeatures);
}
static SimpleFeatureReader getDelegateReader(SimpleFeatureReader reader, SortBy[] sortBy,
int maxFeatures) throws IOException {
Comparator<SimpleFeature> comparator = getComparator(sortBy);
// easy case, no sorting needed
if (comparator == null) {
return reader;
}
// double check
SimpleFeatureType schema = reader.getFeatureType();
if (!canSort(schema, sortBy)) {
throw new IllegalArgumentException(
"The specified reader cannot be sorted, either the "
+ "sorting properties are not comparable or the attributes are not serializable");
}
int count = 0;
File file = null;
RandomAccessFile raf = null;
List<SimpleFeature> features = new ArrayList<SimpleFeature>();
List<FeatureBlockReader> readers = new ArrayList<FeatureBlockReader>();
boolean cleanFile = true;
try {
// read and store into files as necessary
while (reader.hasNext()) {
SimpleFeature f = reader.next();
features.add(f);
count++;
if (count > maxFeatures) {
Collections.sort(features, comparator);
if (raf == null) {
file = File.createTempFile("sorted", ".features");
file.delete();
raf = new RandomAccessFile(file, "rw");
}
FeatureBlockReader fbr = storeToFile(raf, features, schema);
readers.add(fbr);
count = 0;
features.clear();
}
}
// return the appropriate reader
if (raf == null) {
// simple case, we managed to keep everything in memory, sort and return a
// reader based on the collection contents
Collections.sort(features, comparator);
SimpleFeatureIterator fi = new ListFeatureCollection(schema, features).features();
return new DelegateSimpleFeatureReader(schema, fi);
} else {
// go merge-sort
cleanFile = false;
return new MergeSortReader(schema, raf, file, readers, comparator);
}
} finally {
if (cleanFile && raf != null) {
raf.close();
file.delete();
}
reader.close();
}
}
/**
* Writes the feature attributes to a binary file
*
* @param features
* @return
* @throws IOException
*/
static FeatureBlockReader storeToFile(RandomAccessFile raf, List<SimpleFeature> features,
SimpleFeatureType schema) throws IOException {
long start = raf.getFilePointer();
// write each attribute in the random access file
List<AttributeDescriptor> attributes = schema.getAttributeDescriptors();
for (SimpleFeature sf : features) {
// write feature id
raf.writeUTF(sf.getID());
// write the attributes
for (AttributeDescriptor ad : attributes) {
Object value = sf.getAttribute(ad.getLocalName());
writeAttribute(raf, ad, value);
}
}
return new FeatureBlockReader(raf, start, features.size(), schema);
}
static void writeAttribute(RandomAccessFile raf, AttributeDescriptor ad, Object value)
throws IOException {
if (value == null) {
// null marker
raf.writeBoolean(true);
} else {
// not null, write the contents. This one requires some explanation. We are not
// writing any type metadata in the stream for the types we can optimize (primitives,
// numbers,
// strings and the like). This means we have to be 100% sure the class we're writing is
// actually the one we can optimize for, and not some subclass. Thus, we are authorized
// to use identity comparison instead of isAssignableFrom or equality, when we read back
// it must be as if we did not serialize stuff at all
raf.writeBoolean(false);
Class<?> binding = ad.getType().getBinding();
if (binding == Boolean.class) {
raf.writeBoolean((Boolean) value);
} else if (binding == Byte.class || binding == byte.class) {
raf.writeByte((Byte) value);
} else if (binding == Short.class || binding == short.class) {
raf.writeShort((Short) value);
} else if (binding == Integer.class || binding == int.class) {
raf.writeInt((Integer) value);
} else if (binding == Long.class || binding == long.class) {
raf.writeLong((Long) value);
} else if (binding == Float.class || binding == float.class) {
raf.writeFloat((Float) value);
} else if (binding == Double.class || binding == double.class) {
raf.writeDouble((Double) value);
} else if (binding == String.class) {
raf.writeUTF((String) value);
} else if (binding == java.sql.Date.class || binding == java.sql.Time.class
|| binding == java.sql.Timestamp.class || binding == java.util.Date.class) {
raf.writeLong(((Date) value).getTime());
} else if (Geometry.class.isAssignableFrom(binding)) {
WKBWriter writer = new WKBWriter();
byte[] buffer = writer.write((Geometry) value);
int length = buffer.length;
raf.writeInt(length);
raf.write(buffer);
} else {
// can't optimize, in this case we use an ObjectOutputStream to write out
// full metadata
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(value);
oos.flush();
byte[] bytes = bos.toByteArray();
raf.writeInt(bytes.length);
raf.write(bytes);
}
}
}
/**
* Builds a comparator out of the sortBy list
*
* @param sortBy
* @return
*/
static Comparator<SimpleFeature> getComparator(SortBy[] sortBy) {
// handle the easy cases, no sorting or natural sorting
if (sortBy == SortBy.UNSORTED || sortBy == null) {
return null;
}
// build a list of comparators
List<Comparator<SimpleFeature>> comparators = new ArrayList<Comparator<SimpleFeature>>();
for (SortBy sb : sortBy) {
if (sb == SortBy.NATURAL_ORDER) {
comparators.add(new FidComparator(true));
} else if (sb == SortBy.REVERSE_ORDER) {
comparators.add(new FidComparator(false));
} else {
String name = sb.getPropertyName().getPropertyName();
boolean ascending = sb.getSortOrder() == SortOrder.ASCENDING;
comparators.add(new PropertyComparator(name, ascending));
}
}
// return the final comparator
if (comparators.size() == 1) {
return comparators.get(0);
} else {
return new CompositeComparator(comparators);
}
}
}