blob: 77e52e4111dc2e0d8585d791533b998a9dd8d22d [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.sis.internal.shapefile.jdbc;
import java.io.File;
import java.nio.BufferUnderflowException;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.util.*;
import java.util.logging.Level;
import org.apache.sis.internal.shapefile.jdbc.resultset.SQLIllegalColumnIndexException;
import org.apache.sis.internal.shapefile.jdbc.resultset.SQLNoSuchFieldException;
import org.opengis.feature.Feature;
/**
* Reader of a Database Binary content by the way of a {@link java.nio.MappedByteBuffer}
*
* @author Marc Le Bihan
* @version 0.5
* @since 0.5
* @module
*/
public class MappedByteReader extends AbstractDbase3ByteReader implements AutoCloseable {
/** List of field descriptors. */
private List<DBase3FieldDescriptor> fieldsDescriptors = new ArrayList<>();
/** Connection properties. */
private Properties info;
/**
* Construct a mapped byte reader on a file.
* @param dbase3File File.
* @param connectionInfos Connection properties, maybe null.
* @throws SQLInvalidDbaseFileFormatException if the database seems to be invalid.
* @throws SQLDbaseFileNotFoundException if the Dbase file has not been found.
*/
public MappedByteReader(File dbase3File, Properties connectionInfos) throws SQLInvalidDbaseFileFormatException, SQLDbaseFileNotFoundException {
super(dbase3File);
this.info = connectionInfos;
// React to special features asked.
if (this.info != null) {
// Sometimes, DBF files have a wrong charset, or more often : none, and you have to specify it.
String recordCharset = (String)this.info.get("record_charset");
if (recordCharset != null) {
Charset cs = Charset.forName(recordCharset);
setCharset(cs);
}
}
loadDescriptor();
}
/**
* Load a row into a feature.
* @param feature Feature to fill.
*/
@Override public void loadRowIntoFeature(Feature feature) {
// TODO: ignore deleted records
getByteBuffer().get(); // denotes whether deleted or current
// read first part of record
for (DBase3FieldDescriptor fd : this.fieldsDescriptors) {
byte[] data = new byte[fd.getLength()];
getByteBuffer().get(data);
int length = data.length;
while (length != 0 && Byte.toUnsignedInt(data[length - 1]) <= ' ') {
length--;
}
String value = new String(data, 0, length);
feature.setPropertyValue(fd.getName(), value);
}
}
/**
* Checks if a next row is available. Warning : it may be a deleted one.
* @return true if a next row is available.
*/
@Override
public boolean nextRowAvailable() {
// 1) Check for remaining bytes.
if (getByteBuffer().hasRemaining() == false) {
return false;
}
// 2) Check that the immediate next byte read isn't the EOF signal.
byte eofCheck = getByteBuffer().get();
boolean isEOF = (eofCheck == 0x1A);
this.log(Level.FINER, "log.delete_status", getRowNum(), eofCheck, isEOF ? "EOF" : "Active");
if (eofCheck == 0x1A) {
return false;
}
else {
// Return one byte back.
int position = getByteBuffer().position();
getByteBuffer().position(position-1);
return true;
}
}
/**
* Returns the record number of the last record red.
* @return The record number.
*/
@Override public int getRowNum() {
int position = getByteBuffer().position();
int recordNumber = (position - Short.toUnsignedInt(this.firstRecordPosition)) / Short.toUnsignedInt(this.recordLength);
return recordNumber;
}
/**
* Read the next row as a set of objects.
* @return Map of field name / object value.
*/
@Override
public Map<String, byte[]> readNextRowAsObjects() {
// TODO: ignore deleted records
/* byte isDeleted = */ getByteBuffer().get(); // denotes whether deleted or current
// read first part of record
HashMap<String, byte[]> fieldsValues = new HashMap<>();
for (DBase3FieldDescriptor fd : this.fieldsDescriptors) {
byte[] data = new byte[fd.getLength()];
getByteBuffer().get(data);
// Trim the bytes right.
int length = data.length;
while (length != 0 && Byte.toUnsignedInt(data[length - 1]) <= ' ') {
length--;
}
if (length != data.length) {
byte[] dataTrimmed = new byte[length];
for(int index=0; index < length; index ++) {
dataTrimmed[index] = data[index];
}
fieldsValues.put(fd.getName(), dataTrimmed);
}
else {
fieldsValues.put(fd.getName(), data);
}
}
return fieldsValues;
}
/**
* Loading the database file content from binary .dbf file.
* @throws SQLInvalidDbaseFileFormatException if descriptor is not readable.
*/
private void loadDescriptor() throws SQLInvalidDbaseFileFormatException {
try {
this.dbaseVersion = getByteBuffer().get();
getByteBuffer().get(this.dbaseLastUpdate);
getByteBuffer().order(ByteOrder.LITTLE_ENDIAN);
this.rowCount = getByteBuffer().getInt();
this.firstRecordPosition = getByteBuffer().getShort();
this.recordLength = getByteBuffer().getShort();
getByteBuffer().order(ByteOrder.BIG_ENDIAN);
getByteBuffer().get(this.reservedFiller1);
this.reservedIncompleteTransaction = getByteBuffer().get();
this.reservedEncryptionFlag = getByteBuffer().get();
getByteBuffer().get(this.reservedFreeRecordThread);
getByteBuffer().get(this.reservedMultiUser);
this.reservedMDXFlag = getByteBuffer().get();
// Translate code page value to a known charset.
this.codePage = getByteBuffer().get();
if (this.charset == null) {
try {
this.charset = toCharset(this.codePage);
}
catch(UnsupportedCharsetException e) {
// Warn the caller that he will have to perform is own conversions.
log(Level.WARNING, "log.no_valid_charset", getFile().getAbsolutePath(), e.getMessage());
}
}
else {
log(Level.INFO, "log.record_charset", this.charset.name());
}
getByteBuffer().get(this.reservedFiller2);
while(getByteBuffer().position() < this.firstRecordPosition - 1) {
DBase3FieldDescriptor fd = new DBase3FieldDescriptor(getByteBuffer());
this.fieldsDescriptors.add(fd);
// loop until you hit the 0Dh field terminator
}
this.descriptorTerminator = getByteBuffer().get();
// If the last character read after the field descriptor isn't 0x0D, the expected mark has not been found and the DBF is corrupted.
if (this.descriptorTerminator != 0x0D) {
String message = format(Level.WARNING, "excp.filedescriptor_problem", getFile().getAbsolutePath(), "Character marking the end of the fields descriptors (0x0D) has not been found.");
throw new SQLInvalidDbaseFileFormatException(message);
}
}
catch(BufferUnderflowException e) {
// This exception doesn't denote a trouble of file opening because the file has been checked before
// the calling of this private function.
// Therefore, an internal structure problem cause maybe a premature End of file or anything else, but the only thing
// we can conclude is : we are not before a device trouble, but a file format trouble.
String message = format(Level.WARNING, "excp.filedescriptor_problem", getFile().getAbsolutePath(), e.getMessage());
throw new SQLInvalidDbaseFileFormatException(message);
}
}
/**
* Returns the fields descriptors in their binary format.
* @return Fields descriptors.
*/
@Override
public List<DBase3FieldDescriptor> getFieldsDescriptors() {
return this.fieldsDescriptors;
}
/**
* Return a field name.
* @param columnIndex Column index.
* @param sql For information, the SQL statement that is attempted.
* @return Field Name.
* @throws SQLIllegalColumnIndexException if the index is out of bounds.
*/
@Override
public String getFieldName(int columnIndex, String sql) throws SQLIllegalColumnIndexException {
return getField(columnIndex, sql).getName();
}
/**
* @see org.apache.sis.internal.shapefile.jdbc.Dbase3ByteReader#getColumnCount()
*/
@Override
public int getColumnCount() {
return this.fieldsDescriptors.size();
}
/**
* Returns the column index for the given column name.
* The default implementation of all methods expecting a column label will invoke this method.
* @param columnLabel The name of the column.
* @param sql For information, the SQL statement that is attempted.
* @return The index of the given column name : first column is 1.
* @throws SQLNoSuchFieldException if there is no field with this name in the query.
*/
@Override
public int findColumn(String columnLabel, String sql) throws SQLNoSuchFieldException {
// If the column name is null, no search is needed.
if (columnLabel == null) {
String message = format(Level.WARNING, "excp.no_such_column_in_resultset", columnLabel, sql, getFile().getName());
throw new SQLNoSuchFieldException(message, sql, getFile(), columnLabel);
}
// Search the field among the fields descriptors.
for(int index=0; index < this.fieldsDescriptors.size(); index ++) {
if (this.fieldsDescriptors.get(index).getName().equals(columnLabel)) {
return index + 1;
}
}
// If we are here, we haven't found our field. Throw an exception.
String message = format(Level.WARNING, "excp.no_such_column_in_resultset", columnLabel, sql, getFile().getName());
throw new SQLNoSuchFieldException(message, sql, getFile(), columnLabel);
}
/**
* Returns the field descriptor of a given ResultSet column index.
* @param columnIndex Column index, first column is 1, second is 2, etc.
* @param sql For information, the SQL statement that is attempted.
* @return Field Descriptor.
* @throws SQLIllegalColumnIndexException if the index is out of bounds.
*/
private DBase3FieldDescriptor getField(int columnIndex, String sql) throws SQLIllegalColumnIndexException {
if (columnIndex < 1 || columnIndex > getColumnCount()) {
String message = format(Level.WARNING, "excp.illegal_column_index", columnIndex, getColumnCount());
throw new SQLIllegalColumnIndexException(message, sql, getFile(), columnIndex);
}
return this.fieldsDescriptors.get(columnIndex-1);
}
}