All Downloads are FREE. Search and download functionalities are using the official Maven repository.

es.ree.eemws.kit.folders.OutputTask Maven / Gradle / Ivy

Go to download

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