package com.revolsys.record.io.format.directory;
import java.io.File;
import java.io.FileFilter;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import javax.annotation.PostConstruct;
import com.revolsys.collection.iterator.AbstractIterator;
import com.revolsys.io.FileUtil;
import com.revolsys.io.PathName;
import com.revolsys.io.PathUtil;
import com.revolsys.io.filter.ExtensionFilenameFilter;
import com.revolsys.properties.ObjectWithProperties;
import com.revolsys.record.Record;
import com.revolsys.record.io.RecordReader;
import com.revolsys.record.io.RecordWriter;
import com.revolsys.record.query.Query;
import com.revolsys.record.schema.AbstractRecordStore;
import com.revolsys.record.schema.FieldDefinition;
import com.revolsys.record.schema.RecordDefinition;
import com.revolsys.record.schema.RecordDefinitionImpl;
import com.revolsys.record.schema.RecordStore;
import com.revolsys.record.schema.RecordStoreSchema;
import com.revolsys.record.schema.RecordStoreSchemaElement;
import com.revolsys.spring.resource.FileSystemResource;
import com.revolsys.spring.resource.Resource;
public class DirectoryRecordStore extends AbstractRecordStore {
private boolean createMissingRecordStore = true;
private boolean createMissingTables = true;
private File directory;
private List<String> fileExtensions;
private final Map<RecordDefinition, Resource> resourcesByRecordDefinition = new HashMap<>();
private final Map<Resource, String> typePathByResource = new HashMap<>();
private final Map<String, RecordWriter> writers = new HashMap<>();
public DirectoryRecordStore(final File directory, final Collection<String> fileExtensions) {
this.directory = directory;
this.fileExtensions = new ArrayList<>(fileExtensions);
}
public DirectoryRecordStore(final File directory, final String... fileExtensions) {
this(directory, Arrays.asList(fileExtensions));
}
public DirectoryRecordStore(final Path directory, final String... fileExtensions) {
this(directory.toFile(), Arrays.asList(fileExtensions));
}
@Override
public void close() {
this.directory = null;
if (this.writers != null) {
for (final RecordWriter writer : this.writers.values()) {
if (writer != null) {
writer.close();
}
}
this.writers.clear();
}
super.close();
}
public void closeWriters(final String typeName) {
final RecordWriter writer = this.writers.remove(typeName);
FileUtil.closeSilent(writer);
}
@Override
public boolean deleteRecord(final Record record) {
final RecordDefinition recordDefinition = record.getRecordDefinition();
final RecordStore recordStore = recordDefinition.getRecordStore();
if (recordStore == this) {
throw new UnsupportedOperationException("Deleting records not supported");
} else {
return false;
}
}
public File getDirectory() {
return this.directory;
}
public String getFileExtension() {
return getFileExtensions().get(0);
}
public List<String> getFileExtensions() {
return this.fileExtensions;
}
@Override
public int getRecordCount(final Query query) {
throw new UnsupportedOperationException();
}
@Override
public RecordDefinition getRecordDefinition(final RecordDefinition recordDefinition) {
final RecordDefinition storeRecordDefinition = super.getRecordDefinition(recordDefinition);
if (storeRecordDefinition == null && this.createMissingTables) {
final PathName typePath = recordDefinition.getPathName();
final PathName schemaPath = typePath.getParent();
RecordStoreSchema schema = getSchema(schemaPath);
if (schema == null && this.createMissingTables) {
final RecordStoreSchema rootSchema = getRootSchema();
schema = rootSchema.newSchema(schemaPath);
}
final File schemaDirectory = new File(this.directory, schemaPath.getPath());
if (!schemaDirectory.exists()) {
schemaDirectory.mkdirs();
}
final RecordDefinitionImpl newRecordDefinition = new RecordDefinitionImpl(schema, typePath);
for (final FieldDefinition field : recordDefinition.getFields()) {
final FieldDefinition newField = new FieldDefinition(field);
newRecordDefinition.addField(newField);
}
schema.addElement(newRecordDefinition);
return newRecordDefinition;
}
return storeRecordDefinition;
}
@Override
public RecordReader getRecords(final PathName path) {
final RecordDefinition recordDefinition = getRecordDefinition(path);
final Resource resource = getResource(path.toString(), recordDefinition);
final RecordReader reader = RecordReader.newRecordReader(resource);
if (reader == null) {
throw new IllegalArgumentException("Cannot find reader for: " + path);
} else {
final String typePath = this.typePathByResource.get(resource);
reader.setProperty("schema", recordDefinition.getSchema());
reader.setProperty("typePath", typePath);
return reader;
}
}
@Override
public String getRecordStoreType() {
return "Directory";
}
protected Resource getResource(final String path) {
final RecordDefinition recordDefinition = getRecordDefinition(path);
return getResource(path, recordDefinition);
}
protected Resource getResource(final String path, final RecordDefinition recordDefinition) {
if (recordDefinition == null) {
throw new IllegalArgumentException("Table does not exist " + path);
}
final Resource resource = this.resourcesByRecordDefinition.get(recordDefinition);
if (resource == null) {
throw new IllegalArgumentException("File does not exist for " + path);
}
return resource;
}
@PostConstruct
@Override
public void initialize() {
if (!this.directory.exists()) {
this.directory.mkdirs();
}
super.initialize();
}
@Override
public synchronized void insertRecord(final Record record) {
final RecordDefinition recordDefinition = record.getRecordDefinition();
final String typePath = recordDefinition.getPath();
RecordWriter writer = this.writers.get(typePath);
if (writer == null) {
final String schemaName = PathUtil.getPath(typePath);
final File subDirectory = FileUtil.getDirectory(getDirectory(), schemaName);
final String fileExtension = getFileExtension();
final File file = new File(subDirectory, recordDefinition.getName() + "." + fileExtension);
final Resource resource = new FileSystemResource(file);
writer = RecordWriter.newRecordWriter(recordDefinition, resource);
if (writer == null) {
throw new RuntimeException("Cannot create writer for: " + typePath);
} else if (writer instanceof ObjectWithProperties) {
final ObjectWithProperties properties = writer;
properties.setProperties(getProperties());
}
this.writers.put(typePath, writer);
}
writer.write(record);
addStatistic("Insert", record);
}
public boolean isCreateMissingRecordStore() {
return this.createMissingRecordStore;
}
public boolean isCreateMissingTables() {
return this.createMissingTables;
}
protected RecordDefinition loadRecordDefinition(final RecordStoreSchema schema,
final String schemaName, final Resource resource) {
try (
RecordReader recordReader = RecordReader.newRecordReader(resource)) {
final String typePath = PathUtil.toPath(schemaName, resource.getBaseName());
recordReader.setProperty("schema", schema);
recordReader.setProperty("typePath", typePath);
final RecordDefinition recordDefinition = recordReader.getRecordDefinition();
if (recordDefinition != null) {
this.resourcesByRecordDefinition.put(recordDefinition, resource);
this.typePathByResource.put(resource, typePath);
}
return recordDefinition;
}
}
@Override
public AbstractIterator<Record> newIterator(final Query query,
final Map<String, Object> properties) {
final PathName path = query.getTypePath();
final RecordReader reader = getRecords(path);
reader.setProperties(properties);
return new RecordReaderQueryIterator(reader, query);
}
@Override
public RecordWriter newRecordWriter() {
return new DirectoryRecordStoreWriter(this);
}
@Override
public RecordWriter newRecordWriter(final RecordDefinition recordDefinition) {
return new DirectoryRecordStoreWriter(this, recordDefinition);
}
@Override
protected Map<PathName, RecordStoreSchemaElement> refreshSchemaElements(
final RecordStoreSchema schema) {
final Map<PathName, RecordStoreSchemaElement> elements = new TreeMap<>();
final String schemaPath = schema.getPath();
final PathName schemaPathName = schema.getPathName();
final File subDirectory;
if (schemaPath.equals("/")) {
subDirectory = this.directory;
} else {
subDirectory = new File(this.directory, schemaPath);
}
final FileFilter filter = new ExtensionFilenameFilter(this.fileExtensions);
final File[] files = subDirectory.listFiles();
if (files != null) {
for (final File file : files) {
if (filter.accept(file)) {
final FileSystemResource resource = new FileSystemResource(file);
final RecordDefinition recordDefinition = loadRecordDefinition(schema, schemaPath,
resource);
if (recordDefinition != null) {
final PathName path = recordDefinition.getPathName();
elements.put(path, recordDefinition);
}
} else if (file.isDirectory()) {
final String name = file.getName();
final PathName childSchemaPath = schemaPathName.newChild(name);
RecordStoreSchema childSchema = schema.getSchema(childSchemaPath);
if (childSchema == null) {
childSchema = new RecordStoreSchema(schema, childSchemaPath);
} else {
if (!childSchema.isInitialized()) {
childSchema.refresh();
}
}
elements.put(childSchemaPath, childSchema);
}
}
}
return elements;
}
public void setCreateMissingRecordStore(final boolean createMissingRecordStore) {
this.createMissingRecordStore = createMissingRecordStore;
}
public void setCreateMissingTables(final boolean createMissingTables) {
this.createMissingTables = createMissingTables;
}
public void setDirectory(final File directory) {
this.directory = directory;
}
protected void setFileExtensions(final List<String> fileExtensions) {
this.fileExtensions = fileExtensions;
}
protected void superDelete(final Record record) {
super.deleteRecord(record);
}
protected void superUpdate(final Record record) {
super.updateRecord(record);
}
@Override
public String toString() {
final String fileExtension = getFileExtension();
return fileExtension + " " + this.directory;
}
@Override
public void updateRecord(final Record record) {
final RecordDefinition recordDefinition = record.getRecordDefinition();
final RecordStore recordStore = recordDefinition.getRecordStore();
if (recordStore == this) {
switch (record.getState()) {
case DELETED:
break;
case PERSISTED:
break;
case MODIFIED:
throw new UnsupportedOperationException();
default:
insertRecord(record);
break;
}
} else {
insertRecord(record);
}
}
}