/*
* Licensed 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.
*
* Contributions from 2013-2017 where performed either by US government
* employees, or under US Veterans Health Administration contracts.
*
* US Veterans Health Administration contributions by government employees
* are work of the U.S. Government and are not subject to copyright
* protection in the United States. Portions contributed by government
* employees are USGovWork (17USC ยง105). Not subject to copyright.
*
* Contribution by contractors to the US Veterans Health Administration
* during this period are contractually contributed under the
* Apache License, Version 2.0.
*
* See: https://www.usa.gov/government-works
*
* Contributions prior to 2013:
*
* Copyright (C) International Health Terminology Standards Development Organisation.
* Licensed under the Apache License, Version 2.0.
*
*/
package sh.isaac.api.externalizable;
//~--- JDK imports ------------------------------------------------------------
import java.io.IOException;
import java.nio.file.Path;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
//~--- non-JDK imports --------------------------------------------------------
import org.apache.logging.log4j.LogManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sh.isaac.api.LookupService;
//~--- classes ----------------------------------------------------------------
/**
* Simple wrapper class to allow us to serialize to multiple formats at once.
*
* Also includes logic for incorporating the date and a UUID into the file name to ensure uniqueueness,
* and logic for rotating the changeset files.
*
* {@link MultipleDataWriterService}
*
* @author <a href="mailto:daniel.armbrust.list@gmail.com">Dan Armbrust</a>
*/
public class MultipleDataWriterService
implements DataWriterService {
/** The writers. */
ArrayList<DataWriterService> writers = new ArrayList<>();
/** The logger. */
private final Logger logger = LoggerFactory.getLogger(MultipleDataWriterService.class);
/** The sdf. */
private final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
/** The object write count. */
private final AtomicInteger objectWriteCount = new AtomicInteger();
/** The rotate after. */
private final int rotateAfter =
10000; // This will cause us to rotate files after ~ 1 MB of IBDF content, in rough testing.
/** The prefix. */
private String prefix;
/** The enable rotate. */
private final boolean enableRotate;
//~--- constructors --------------------------------------------------------
/**
* This constructor creates a multiple data writer service which writes to the specified files, and does not do any rotation or autonaming.
*
* @param jsonPath the json path
* @param ibdfPath the ibdf path
* @throws IOException Signals that an I/O exception has occurred.
*/
public MultipleDataWriterService(Optional<Path> jsonPath, Optional<Path> ibdfPath)
throws IOException {
this.enableRotate = false;
if (jsonPath.isPresent()) {
// Use HK2 here to make fortify stop false-flagging an open resource error
final DataWriterService writer = LookupService.get()
.getService(DataWriterService.class, "jsonWriter");
if (writer != null) {
writer.configure(jsonPath.get());
this.writers.add(writer);
} else {
LogManager.getLogger()
.warn("json writer was requested, but not found on classpath!");
}
}
if (ibdfPath.isPresent()) {
final DataWriterService writer = LookupService.get()
.getService(DataWriterService.class, "ibdfWriter");
if (writer != null) {
writer.configure(ibdfPath.get());
this.writers.add(writer);
} else {
LogManager.getLogger()
.warn("ibdf writer was requested, but not found on classpath!");
}
}
}
/**
* This constructor sets up the multipleDataWriter in such a way that is will create date stamped and UUID unique file names, rotating them after
* a certain number of writes, to prevent them from growing too large.
*
* This constructor will also start a mode where we do NOT keep 0 length files - therefore, if we start, and stop, and the last file that was being written
* to is size 0, the last file will be deleted.
*
* @param folderToWriteInto the folder to write into
* @param prefix the prefix
* @param jsonExtension the json extension
* @param ibdfExtension the ibdf extension
* @throws IOException Signals that an I/O exception has occurred.
*/
public MultipleDataWriterService(Path folderToWriteInto,
String prefix,
Optional<String> jsonExtension,
Optional<String> ibdfExtension)
throws IOException {
this.prefix = prefix;
this.enableRotate = true;
final String fileNamePrefix = prefix + this.sdf.format(new Date()) + "_" + UUID.randomUUID().toString() + ".";
if (jsonExtension.isPresent()) {
// Use HK2 here to make fortify stop false-flagging an open resource error
final DataWriterService writer = LookupService.get()
.getService(DataWriterService.class, "jsonWriter");
if (writer != null) {
writer.configure(folderToWriteInto.resolve(fileNamePrefix + jsonExtension.get()));
this.writers.add(writer);
} else {
LogManager.getLogger()
.warn("json writer was requested, but not found on classpath!");
}
}
if (ibdfExtension.isPresent()) {
final DataWriterService writer = LookupService.get()
.getService(DataWriterService.class, "ibdfWriter");
if (writer != null) {
writer.configure(folderToWriteInto.resolve(fileNamePrefix + ibdfExtension.get()));
this.writers.add(writer);
} else {
LogManager.getLogger()
.warn("ibdf writer was requested, but not found on classpath!");
}
}
}
//~--- methods -------------------------------------------------------------
/**
* Close.
*
* @throws IOException Signals that an I/O exception has occurred.
* @see sh.isaac.api.externalizable.DataWriterService#close()
*/
@Override
public void close()
throws IOException {
handleMulti((writer) -> {
try {
writer.close();
return null;
} catch (final IOException e) {
return e;
}
});
}
/**
* Configure.
*
* @param path the path
* @throws UnsupportedOperationException the unsupported operation exception
* @see sh.isaac.api.externalizable.DataWriterService#configure(java.nio.file.Path)
*/
@Override
public void configure(Path path)
throws UnsupportedOperationException {
throw new UnsupportedOperationException("Method not supported");
}
/**
* Flush.
*
* @throws IOException Signals that an I/O exception has occurred.
* @see sh.isaac.api.externalizable.DataWriterService#flush()
*/
@Override
public void flush()
throws IOException {
handleMulti((writer) -> {
try {
writer.flush();
return null;
} catch (final IOException e) {
return e;
}
});
}
/**
* Handle multi.
*
* @param function the function
* @throws IOException Signals that an I/O exception has occurred.
*/
public void handleMulti(Function<DataWriterService, IOException> function)
throws IOException {
final ArrayList<IOException> exceptions = new ArrayList<>();
for (final DataWriterService writer: this.writers) {
final IOException e = function.apply(writer);
if (e != null) {
exceptions.add(e);
}
}
if (exceptions.size() > 0) {
if (exceptions.size() > 1) {
for (int i = 1; i < exceptions.size(); i++) {
this.logger.error("extra, unthrown exception: ", exceptions.get(i));
}
}
throw exceptions.get(0);
}
}
/**
* Pause.
*
* @throws IOException Signals that an I/O exception has occurred.
* @see sh.isaac.api.externalizable.DataWriterService#pause()
*/
@Override
public void pause()
throws IOException {
handleMulti((writer) -> {
try {
writer.pause();
return null;
} catch (final IOException e) {
return e;
}
});
}
/**
* Put.
*
* @param ochreObject the ochre object
* @throws RuntimeException the runtime exception
* @see sh.isaac.api.externalizable.DataWriterService#put(sh.isaac.api.externalizable.OchreExternalizable)
*/
@Override
public void put(OchreExternalizable ochreObject)
throws RuntimeException {
try {
handleMulti((writer) -> {
try {
writer.put(ochreObject);
return null;
} catch (final RuntimeException e) {
return new IOException(e);
}
});
} catch (final IOException e) {
if ((e.getCause() != null) && (e.getCause() instanceof RuntimeException)) {
throw(RuntimeException) e.getCause();
} else {
this.logger.warn("Unexpected", e);
throw new RuntimeException(e);
}
}
if (this.enableRotate && (this.objectWriteCount.incrementAndGet() >= this.rotateAfter)) {
rotateFiles();
}
}
/**
* Resume.
*
* @throws IOException Signals that an I/O exception has occurred.
* @see sh.isaac.api.externalizable.DataWriterService#resume()
*/
@Override
public void resume()
throws IOException {
handleMulti((writer) -> {
try {
writer.resume();
return null;
} catch (final IOException e) {
return e;
}
});
}
/**
* Rotate files.
*
* @throws RuntimeException the runtime exception
*/
private void rotateFiles()
throws RuntimeException {
try {
pause();
final String fileNamePrefix = this.prefix + this.sdf.format(new Date()) + "_" + UUID.randomUUID().toString();
for (final DataWriterService writer: this.writers) {
String extension = writer.getCurrentPath()
.getFileName()
.toString();
extension = extension.substring(extension.lastIndexOf('.'));
writer.configure(writer.getCurrentPath()
.getParent()
.resolve(fileNamePrefix + extension));
}
this.objectWriteCount.set(0);
resume();
} catch (final IOException e) {
this.logger.error("Unexpected error rotating changeset files!", e);
throw new RuntimeException(e);
}
}
//~--- get methods ---------------------------------------------------------
/**
* Gets the current path.
*
* @return the current path
*/
@Override
public Path getCurrentPath() {
throw new UnsupportedOperationException("Method not supported");
}
}