![JAR search and dependency download from the Maven repository](/logo.png)
es.ree.eemws.kit.folders.OutputTask Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of eemws-kit Show documentation
Show all versions of eemws-kit Show documentation
Client implementation of IEC 62325-504 technical specification. eemws-kit includes command line utilities to invoke the eem web services, as well as several GUI applications (browser, editor, ...)
The newest version!
/*
* Copyright 2024 Redeia.
*
* This program 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 3 of the license.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTIBIILTY or FITNESS FOR A PARTICULAR PURPOSE. See GNU Lesser General
* Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this program. If not, see
* http://www.gnu.org/licenses/.
*
* Any redistribution and/or modification of this program has to make
* reference to Redeia as the copyright owner of the program.
*/
package es.ree.eemws.kit.folders;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
import java.util.regex.Pattern;
import es.ree.eemws.client.get.GetMessage;
import es.ree.eemws.client.get.RetrievedMessage;
import es.ree.eemws.client.list.ListMessages;
import es.ree.eemws.client.list.MessageListEntry;
import es.ree.eemws.core.utils.file.FileUtil;
import es.ree.eemws.core.utils.iec61968100.EnumIntervalTimeType;
import es.ree.eemws.core.utils.operations.get.GetOperationException;
import es.ree.eemws.core.utils.operations.list.ListOperationException;
/**
* Executes a list + get loop to retrieve messages.
*
* @author Redeia.
* @version 2.1 01/01/2024
*
*/
public final class OutputTask implements Runnable {
/** Object which locks message for group working. */
private final LockHandler lh;
/** Log system. */
private static final Logger LOGGER = Logger.getLogger(OutputTask.class.getName());
/** Temporary file prefix. */
private static final String TMP_PREFIX = "_tmp_out_"; //$NON-NLS-1$
/** File name extension for "xml". */
private static final String FILE_NAME_EXTENSION_XML = "xml"; //$NON-NLS-1$
/** File name extension separator. */
private static final String FILE_ELEMENTS_SEPARTOR = "."; //$NON-NLS-1$
/** System property to rest the list code to 0. */
private static final String RESET_CODE_KEY = "RESET_CODE"; //$NON-NLS-1$
/** Headers values that identifies the file type. */
private static final String[] HEADER_VALUES = { "BZh91AY", "7z", "PDF", "PK", "PNG", "JFIF" };
/** File extensions according to the header value. */
private static final String[] EXTENSION_VALUES = { "bz2", "7z", "pdf", "zip", "png", "jpg" };
/** Reads up to 20 bytes in order to guess the proper file extension. */
private static final int FIRST_BYTES_OF_MESSAGE = 20;
/** Number of attemps to rename a temporary file that is locked. */
private static final int MAX_RENAME_RETRIES = 5;
/** Sleep time between rename task retries. */
private static final long TMP_FILE_LOCKED_SLEEP_TIME = 5000L;
/** By default only ask for a file once. */
private static final String DEFAULT_ATTEMPTS = "1"; //$NON-NLS-1$
/** Name of the system parameter that allows to retry a failed get operation. */
private static final String NUM_ATTEMPTS_PARAMETER_KEY = "MF_RETRY_ATTEMPTS"; //$NON-NLS-1$
/** Waiting time between get attempts. */
private static final long RETRY_SLEEP = 5000L;
/** Configuration key to provide a given date in epoch form. */
private static final String RESET_DATE_KEY = "RESET_DATE"; //$NON-NLS-1$
/** This task configuration set values. */
private final List ocs;
/** Number of get attempts if the server fails. */
private int numAttempts;
/** Last retrieved code. */
private long lastListCode;
/** Last retrieved calendar. */
private Calendar lastListDate;
/** Prefernces object key to store the last retrieved code. */
private String preferenceKey;
/** Full list of files to retrieve. */
private List totalTypesToRetrieve;
/** Message List object. */
private final ListMessages list;
/** Message get object. */
private final GetMessage get;
/** This output task set of ids. */
private final String setIds;
/** Last retrieved file. */
private RetrievedMessage lastRetrievedFile = null;
/** Whether this task will ask list by code (default) or by server date. */
private final boolean listByCode;
/** Preference object in order to keep the lastest list's code value. */
private final Preferences preferences = Preferences.userNodeForPackage(getClass());
/**
* Constructor. Initializes parameters for detection thread.
*
* @param lockHandler Lock Manager.
* @param oc List of output configuration sets that shares the same
* url.
* @param setIdss This output task set of ids.
*/
public OutputTask(final LockHandler lockHandler, final List oc, final String setIdss) {
totalTypesToRetrieve = new ArrayList<>();
var retrieveAllMessages = false;
for (final OutputConfigurationSet o : oc) {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info(o.toString());
}
if (!retrieveAllMessages) {
final var lstRetr = o.getMessagesTypesList();
if (lstRetr == null) {
retrieveAllMessages = true;
totalTypesToRetrieve = null;
} else {
totalTypesToRetrieve.addAll(lstRetr);
}
}
}
checkAttempts();
final var endPoint = oc.get(0).getOutputUrlEndPoint();
list = new ListMessages();
list.setEndPoint(endPoint);
get = new GetMessage();
get.setEndPoint(endPoint);
setIds = setIdss;
lh = lockHandler;
ocs = oc;
var instanceId = oc.get(0).getInstanceID();
if (instanceId == null) {
preferenceKey = endPoint.toString();
} else {
preferenceKey = instanceId + endPoint.toString();
}
listByCode = System.getProperty(MagicFolderConfiguration.LIST_BY_DATE_KEY) == null;
getLastList();
}
/**
* Initializes the code or date to be used in the list + get loop.
*/
private void getLastList() {
if (listByCode) {
getLastListCode();
} else {
getLastListDate();
}
}
/**
* Sets the date to be used in the list loop by date. If a previous loop was
* completed, the date will be retrieved from the preferences set. The user can
* provide a particular date using the key RESET_DATE_KEY
given the
* date using the epoch form. If no date is provided and this is the first time
* that the application is lauched, the date will be calculated as current time
* - 1 hour.
*/
private void getLastListDate() {
if (System.getProperty(RESET_DATE_KEY) == null) {
var lastSavedDate = preferences.getLong(preferenceKey, 0);
/* Retrieve lastListDate from the preferences, continue previous execution. */
if (lastSavedDate != 0) {
lastListDate = Calendar.getInstance();
lastListDate.setTimeInMillis(lastSavedDate);
lastListDate.set(Calendar.MILLISECOND, 0);
lastListDate.set(Calendar.SECOND, 0);
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info(MessageCatalog.MF_CONFIG_LST_DATE.getMessage(setIds, lastListDate.getTime()));
}
}
} else {
/* Set lastListDate from user data (RESET_DATE_KEY) */
try {
var resetDate = Long.parseLong(System.getProperty(RESET_DATE_KEY));
lastListDate = Calendar.getInstance();
lastListDate.setTimeInMillis(resetDate);
lastListDate.set(Calendar.MILLISECOND, 0);
lastListDate.set(Calendar.SECOND, 0);
preferences.putLong(preferenceKey, lastListDate.getTimeInMillis());
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info(MessageCatalog.MF_CONFIG_LST_DATE_RESET.getMessage(setIds, lastListDate.getTime()));
}
} catch (NumberFormatException e) {
/* Invalid value for REST_DATE_KEY */
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info(MessageCatalog.MF_CONFIG_LST_DATE_INVALID_VALUE.getMessage(setIds,
System.getProperty(RESET_DATE_KEY)));
}
}
}
/* Default: go 1h back in time */
if (lastListDate == null) {
lastListDate = Calendar.getInstance();
lastListDate.add(Calendar.HOUR_OF_DAY, -1);
lastListDate.set(Calendar.MILLISECOND, 0);
lastListDate.set(Calendar.SECOND, 0);
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info(MessageCatalog.MF_CONFIG_LST_DATE_DEFAULT_VALUE.getMessage(setIds, lastListDate.getTime()));
}
}
}
/**
* Gets the last message code that magicfolder used to get list form the given
* URL. This allows magic folder to continue listing from the previous point
* after a restart. If the system key RESET_CODE
is set, the code
* will be set to 0.
*/
private void getLastListCode() {
if (System.getProperty(RESET_CODE_KEY) == null) {
lastListCode = preferences.getLong(preferenceKey, 0);
if (lastListCode != 0 && LOGGER.isLoggable(Level.INFO)) {
LOGGER.info(MessageCatalog.MF_CONFIG_LST_CODE.getMessage(setIds, String.valueOf(lastListCode)));
}
} else {
lastListCode = 0;
preferences.putLong(preferenceKey, 0);
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info(MessageCatalog.MF_CONFIG_LST_CODE_RESET.getMessage(setIds));
}
}
}
/**
* Check that the value given to the parameter
* NUM_ATTEMPTS_PARAMETER_KEY
is correct. If the value is not
* numerical or is below 1, the number is set to the default:
* DEFAULT_ATTEMPTS
*/
private void checkAttempts() {
try {
numAttempts = Integer.parseInt(System.getProperty(NUM_ATTEMPTS_PARAMETER_KEY, DEFAULT_ATTEMPTS));
if (numAttempts < 1) {
numAttempts = Integer.parseInt(DEFAULT_ATTEMPTS);
}
} catch (NumberFormatException ex) {
numAttempts = Integer.parseInt(DEFAULT_ATTEMPTS);
}
}
/**
* Retrieves and stores a message.
*
* @param mle List element retrieved in detection process.
*/
private void retrieveAndStore(final MessageListEntry mle) {
final var code = mle.getCode().longValue();
final var codeStr = String.valueOf(code);
final var lockFile = lh.tryLock(codeStr);
if (lockFile) {
try {
lastRetrievedFile = null;
for (final OutputConfigurationSet oc : ocs) {
final var type = oc.getMessagesTypesList();
if ((type == null || type.contains(mle.getType()))
&& isMessageIdMatched(mle.getMessageIdentification(), oc.getMessageIdPatternsList())) {
saveFile(mle, oc);
}
}
} catch (final GetOperationException e) {
if (mle.getVersion() == null) {
LOGGER.log(Level.SEVERE, MessageCatalog.MF_UNABLE_TO_GET_WO_VERSION.getMessage(setIds,
String.valueOf(code), mle.getMessageIdentification()), e);
} else {
LOGGER.log(Level.SEVERE, MessageCatalog.MF_UNABLE_TO_GET.getMessage(setIds, String.valueOf(code),
mle.getMessageIdentification(), mle.getVersion()), e);
}
} catch (final IOException e) {
if (mle.getVersion() == null) {
LOGGER.log(Level.SEVERE, MessageCatalog.MF_UNABLE_TO_SAVE_WO_VERSION.getMessage(setIds,
String.valueOf(code), mle.getMessageIdentification()), e);
} else {
LOGGER.log(Level.SEVERE, MessageCatalog.MF_UNABLE_TO_SAVE.getMessage(setIds, String.valueOf(code),
mle.getMessageIdentification(), mle.getVersion()), e);
}
} finally {
lh.releaseLock(codeStr);
/* Avoid holding this (probably) huge value */
lastRetrievedFile = null;
}
}
}
/**
* Checks if the given message has to be saved according to the configured set
* of patterns.
*
* @param messageId Message identification.
* @param messageIdPatternsList List of patterns to match with the message
* identification.
* @return true
if the message matches at least one of the patterns
* or if messageIdPatternsList
is null or
* empty.false
otherwise.
*/
private boolean isMessageIdMatched(final String messageId, final List messageIdPatternsList) {
var matches = false;
if (messageIdPatternsList == null || messageIdPatternsList.isEmpty()) {
matches = true;
} else {
final var patternItrn = messageIdPatternsList.iterator();
while (!matches && patternItrn.hasNext()) {
final var pattern = patternItrn.next();
final var matcher = pattern.matcher(messageId);
matches = matcher.matches();
}
}
return matches;
}
/**
* Gets the message given its MessageListEntry
. The method wont
* call the get operation if the file was already retrieved.
*
* @param mle MessageListEntry with the information about the file to be
* retrieved.
* @return The message retrieved according the given MessageListEntry parameter.
* null
is never returned.
* @throws GetOperationException If the message cannot be retrieved.
*/
private RetrievedMessage retrieveFile(final MessageListEntry mle) throws GetOperationException {
if (lastRetrievedFile == null) {
GetOperationException lastException = null;
final var code = mle.getCode().longValue();
final var codeStr = String.valueOf(code);
if (mle.getVersion() == null) {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info(MessageCatalog.MF_RETRIEVING_MESSAGE_WO_VERSION.getMessage(setIds, codeStr,
mle.getMessageIdentification()));
}
} else if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info(MessageCatalog.MF_RETRIEVING_MESSAGE.getMessage(setIds, codeStr,
mle.getMessageIdentification(), mle.getVersion()));
}
for (var cont = 0; lastRetrievedFile == null && cont < numAttempts; cont++) {
try {
lastRetrievedFile = get.get(code);
} catch (GetOperationException e) {
lastException = e;
/* Do not show the retry log if this was the last attempt. */
if (cont + 1 < numAttempts) {
if (mle.getVersion() == null) {
LOGGER.log(Level.SEVERE, MessageCatalog.MF_RETRY_GET_WO_VERSION.getMessage(setIds, codeStr,
mle.getMessageIdentification()));
} else {
LOGGER.log(Level.SEVERE, MessageCatalog.MF_RETRY_GET.getMessage(setIds, codeStr,
mle.getMessageIdentification(), mle.getVersion()));
}
try {
Thread.sleep(RETRY_SLEEP);
} catch (InterruptedException e1) {
Thread.currentThread().interrupt();
}
}
}
}
if (lastRetrievedFile == null && lastException != null) {
throw lastException;
}
if (LOGGER.isLoggable(Level.INFO)) {
if (mle.getVersion() == null) {
LOGGER.info(MessageCatalog.MF_RETRIEVED_MESSAGE_WO_VERSION.getMessage(setIds, codeStr,
mle.getMessageIdentification()));
} else {
LOGGER.info(MessageCatalog.MF_RETRIEVED_MESSAGE.getMessage(setIds, codeStr,
mle.getMessageIdentification(), mle.getVersion()));
}
}
}
return lastRetrievedFile;
}
/**
* Saves the current message and (optionally) executes a program.
*
* @param mle Retrieved message information.
* @param oc Configuration to be used in order to save the file.
* @throws IOException If the message cannot be saved or if the
* provided command line produces error.
* @throws GetOperationException If the message cannot be retrieved.
*/
private void saveFile(final MessageListEntry mle, final OutputConfigurationSet oc)
throws IOException, GetOperationException {
final var fileName = calculateFileName(mle, oc.getFileNameExtension());
final var abosoluteFileName = oc.getOutputFolder() + File.separator + fileName;
if (FileUtil.exists(abosoluteFileName)) {
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info(MessageCatalog.MF_RETRIEVED_MESSAGE_ALREADY_EXISTS.getMessage(oc.getIndex(),
abosoluteFileName));
}
} else {
final var message = retrieveFile(mle);
/*
* Avoid "broken files" in case of anormal program termination. First write into
* a temporaly file then rename it.
*/
final var tmpFile = File.createTempFile(TMP_PREFIX, null, new File(oc.getOutputFolder()));
if (message.isBinary()) {
FileUtil.write(tmpFile.getAbsolutePath(), message.getBinaryPayload());
} else {
FileUtil.writeUTF8(tmpFile.getAbsolutePath(), message.getStringPayload());
}
final var destFile = new File(abosoluteFileName);
renameFile(destFile, tmpFile);
ProgramExecutor.execute(oc.getProgramCmdLine(), destFile, null, mle.getType());
}
}
/**
* Rename the given file. If a file cannot be renamed the method will retry
* several times (the file could be locked by other application)
*
* @param destFile Destination file.
* @param tmpFile Temp file to be renamed.
*/
private void renameFile(final File destFile, final File tmpFile) {
var renamed = false;
for (var cont = 0; !renamed && cont < MAX_RENAME_RETRIES; cont++) {
try {
renamed = tmpFile.renameTo(destFile);
if (renamed) {
if (cont > 0 && LOGGER.isLoggable(Level.INFO)) {
LOGGER.info(MessageCatalog.MF_FILE_RENAMED.getMessage(tmpFile.getName(), destFile.getName()));
}
} else {
if (LOGGER.isLoggable(Level.WARNING)) {
LOGGER.log(Level.WARNING, MessageCatalog.MF_FILE_CANNOT_BE_RENAMED_RETRYING
.getMessage(tmpFile.getName(), destFile.getName()));
}
Thread.sleep(TMP_FILE_LOCKED_SLEEP_TIME);
}
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
}
}
if (!renamed && LOGGER.isLoggable(Level.SEVERE)) {
LOGGER.log(Level.SEVERE, MessageCatalog.MF_FILE_CANNOT_BE_RENAMED_GIVING_UP.getMessage(tmpFile.getName(),
destFile.getName()));
}
}
/**
* Gets the file name and extension according to the given extension
* configuration value. The extension is obtained as following:
* AUTO (for binary): fileName + ["." + calculatedExtension"]
* AUTO (non binary): fileName + ["." + version] + ".xml"
* NONE: messageId + ["." + version]
* XXXX: messageId + ["." + version] + ".XXXX
*
* NOTE: If the retrieved binary file has an unsupported extension the
* calculated file name won't have extension. NOTE: AUTO forces file retrieving
* in order to know the proper extension according to the file's content.
*
* @param mle Current message list entry.
* @param fExtension Extension configuration value.
* @throws GetOperationException if file cannot be retrieved.
*/
private String calculateFileName(final MessageListEntry mle, final String fExtension) throws GetOperationException {
final var extStr = new StringBuilder();
if (fExtension.equalsIgnoreCase(OutputConfigurationSet.FILE_NAME_EXTENSION_AUTO)) {
/* Extension == AUTO and the file was not yet retrieved -> get the file */
final var message = retrieveFile(mle);
if (message.isBinary()) {
extStr.append(message.getFileName());
final var b = message.getBinaryPayload();
if (b.length > FIRST_BYTES_OF_MESSAGE) {
final var headerValue = new String(message.getBinaryPayload(), 0, FIRST_BYTES_OF_MESSAGE);
var found = false;
for (var cont = 0; cont < HEADER_VALUES.length && !found; cont++) {
if (headerValue.indexOf(HEADER_VALUES[cont]) != -1) {
extStr.append(FILE_ELEMENTS_SEPARTOR);
extStr.append(EXTENSION_VALUES[cont]);
found = true;
}
}
}
} else {
extStr.append(mle.getMessageIdentification());
if (mle.getVersion() != null) {
extStr.append(FILE_ELEMENTS_SEPARTOR);
extStr.append(mle.getVersion());
extStr.append(FILE_ELEMENTS_SEPARTOR);
extStr.append(FILE_NAME_EXTENSION_XML);
}
}
} else {
extStr.append(mle.getMessageIdentification());
if (mle.getVersion() != null) {
extStr.append(FILE_ELEMENTS_SEPARTOR);
extStr.append(mle.getVersion());
}
if (!fExtension.equalsIgnoreCase(OutputConfigurationSet.FILE_NAME_EXTENSION_NONE)) {
extStr.append(FILE_ELEMENTS_SEPARTOR);
extStr.append(fExtension);
}
}
return extStr.toString();
}
private List listMessageByDate(final Date newTime) {
List messageList = null;
try {
messageList = list.list(lastListDate.getTime(), newTime, EnumIntervalTimeType.SERVER);
} catch (final ListOperationException ex) {
LOGGER.log(Level.SEVERE, MessageCatalog.MF_UNABLE_TO_LIST.getMessage(setIds), ex);
}
return messageList;
}
/**
* Gets a message list using the last list code.
*
* @return A message list. null
if the client cannot connect with
* the server.
*/
private List listMessagesByCode() {
List messageList = null;
try {
messageList = list.list(lastListCode);
} catch (final ListOperationException ex) {
LOGGER.log(Level.SEVERE, MessageCatalog.MF_UNABLE_TO_LIST.getMessage(setIds), ex);
}
return messageList;
}
/**
* Detection cycle.
*/
@Override
public void run() {
try {
if (listByCode) {
runByCode();
} else {
runByDate();
}
} catch (final Exception ex) {
/*
* Defensive exception, if runnable task ends with exception won't be exectued
* againg!
*/
LOGGER.log(Level.SEVERE, MessageCatalog.MF_UNEXPECTED_ERROR_O.getMessage(setIds), ex);
}
}
/**
* Executes a list loop task using a code value. This is Magic Folder's default
* behaviour.
*/
private void runByCode() {
final var messageList = listMessagesByCode();
if (messageList != null) {
final var len = messageList.size();
if (len > 1) {
StatusIcon.getStatus().setBusy();
for (final MessageListEntry message : messageList) {
if (totalTypesToRetrieve == null || totalTypesToRetrieve.contains(message.getType())) {
retrieveAndStore(message);
}
/* Take the highest message code to start listing form this one. */
final var msgCode = message.getCode().intValue();
if (msgCode > lastListCode) {
lastListCode = msgCode;
preferences.putLong(preferenceKey, lastListCode);
}
}
StatusIcon.getStatus().setIdle();
}
}
}
/**
* Executes a list loop task using a date value.
*/
private void runByDate() {
var newTime = (Calendar) lastListDate.clone();
newTime.add(Calendar.MINUTE, 1);
var now = Calendar.getInstance();
now.add(Calendar.MINUTE, -1);
while (now.after(newTime)) {
final var messageList = listMessageByDate(newTime.getTime());
if (messageList != null) {
final var len = messageList.size();
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.log(Level.INFO, MessageCatalog.MF_DATE_LOG.getMessage(setIds, lastListDate.getTime(),
newTime.getTime(), len));
}
if (len > 1) {
StatusIcon.getStatus().setBusy();
for (final MessageListEntry message : messageList) {
if (totalTypesToRetrieve == null || totalTypesToRetrieve.contains(message.getType())) {
retrieveAndStore(message);
}
}
StatusIcon.getStatus().setIdle();
}
}
lastListDate = newTime;
newTime = (Calendar) lastListDate.clone();
newTime.add(Calendar.MINUTE, 1);
now = Calendar.getInstance();
now.add(Calendar.MINUTE, -1);
preferences.putLong(preferenceKey, lastListDate.getTimeInMillis());
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy