/** * Copyright (c) 2014-2017 by the respective copyright holders. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.eclipse.smarthome.core.scheduler; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.StringTokenizer; import java.util.TimeZone; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * <code>AbstractExpression</code> is an abstract implementation of {@link Expression} that provides common * functionality to other concrete implementations of <code>Expression</code> * * @author Karel Goderis - Initial Contribution * */ public abstract class AbstractExpression<E extends AbstractExpressionPart> implements Expression { private final Logger logger = LoggerFactory.getLogger(this.getClass().getName()); static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); private int minimumCandidates = 1; private int maximumCandidates = 100; private String expression; private String delimiters; private ArrayList<E> expressionParts = new ArrayList<E>(); private boolean continueSearch; private ArrayList<Date> candidates = new ArrayList<Date>(); private Date startDate = null; private TimeZone timeZone = null; /** * Build an {@link Expression} * * @param expression the expression * @param delimiters delimiters to consider when splitting the expression into expression parts * @param startDate the start date of the expression * @param timeZone the time zone of the expression * @param minimumCandidates the minimum number of candidate dates to calculate * @throws ParseException when the expression cannot be parsed correctly */ public AbstractExpression(String expression, String delimiters, Date startDate, TimeZone timeZone, int minimumCandidates, int maximumCandidates) throws ParseException { if (expression == null) { throw new IllegalArgumentException("The expression cannot be null"); } this.expression = expression; this.delimiters = delimiters; this.startDate = startDate; this.timeZone = timeZone; this.minimumCandidates = minimumCandidates; this.maximumCandidates = maximumCandidates; if (startDate == null) { throw new IllegalArgumentException("The start date of the rule must not be null"); } setStartDate(startDate); setTimeZone(timeZone); parseExpression(expression); } @Override public final Date getStartDate() { if (startDate == null) { try { setStartDate(Calendar.getInstance(getTimeZone()).getTime()); } catch (Exception e) { // This code will never be reached } } return startDate; } @Override public void setStartDate(Date startDate) throws IllegalArgumentException, ParseException { if (startDate == null) { throw new IllegalArgumentException("The start date of the rule must not be null"); } this.startDate = startDate; logger.trace("Setting the start date to {}", sdf.format(startDate)); parseExpression(expression); } @Override public TimeZone getTimeZone() { if (timeZone == null) { timeZone = TimeZone.getDefault(); } return timeZone; } @Override public final void setTimeZone(TimeZone timeZone) throws IllegalArgumentException, ParseException { if (timeZone == null) { throw new IllegalArgumentException("The time zone must not be null"); } this.timeZone = timeZone; parseExpression(expression); } @Override public String getExpression() { return expression; } @Override public void setExpression(String expression) throws ParseException { this.expression = expression; parseExpression(expression); } @Override public String toString() { return expression; } /** * Parse the given expression * * @param expression the expression to parse * @throws ParseException when the expression cannot be successfully be parsed * @throws IllegalArgumentException when expression parts conflict with each other */ public final void parseExpression(String expression) throws ParseException, IllegalArgumentException { parseExpression(expression, true); } /** * Parse the given expression * * @param expression the expression to parse * @param searchMode keep nearest/farthest dates when true/false * @throws ParseException when the expression cannot be successfully be parsed * @throws IllegalArgumentException when expression parts conflict with each other */ public final void parseExpression(String expression, boolean searchMode) throws ParseException, IllegalArgumentException { StringTokenizer expressionTokenizer = new StringTokenizer(expression, delimiters, false); int position = 0; setExpressionParts(new ArrayList<E>()); setCandidates(new ArrayList<Date>()); while (expressionTokenizer.hasMoreTokens()) { String token = expressionTokenizer.nextToken().trim(); position++; getExpressionParts().add(parseToken(token, position)); } validateExpression(); if (startDate == null) { setStartDate(Calendar.getInstance().getTime()); } applyExpressionParts(searchMode); synchronized (this) { continueSearch = true; while (getCandidates().size() < minimumCandidates && continueSearch) { populateWithSeeds(); getCandidates().clear(); applyExpressionParts(searchMode); } continueSearch = false; } for (Date aDate : getCandidates()) { logger.trace("Final candidate {} is {}", getCandidates().indexOf(aDate), sdf.format(aDate)); } } abstract protected void validateExpression() throws IllegalArgumentException; protected void applyExpressionParts(boolean searchMode) { Collections.sort(getExpressionParts()); for (ExpressionPart part : getExpressionParts()) { logger.trace("Expanding {} from {} candidates", part.getClass().getSimpleName(), getCandidates().size()); setCandidates(part.apply(startDate, getCandidates())); logger.trace("Expanded to {} candidates", getCandidates().size()); for (Date aDate : getCandidates()) { logger.trace("Candidate {} is {}", getCandidates().indexOf(aDate), sdf.format(aDate)); } if (searchMode) { pruneFarthest(); } else { pruneNearest(); } } } protected void pruneFarthest() { Collections.sort(getCandidates()); ArrayList<Date> beforeDates = new ArrayList<Date>(); for (Date candidate : getCandidates()) { if (candidate.before(startDate)) { beforeDates.add(candidate); } } getCandidates().removeAll(beforeDates); if (getCandidates().size() > maximumCandidates) { logger.trace("Pruning from {} to {} nearest candidates", getCandidates().size(), maximumCandidates); int size = getCandidates().size(); for (int i = maximumCandidates; i < size; i++) { getCandidates().remove(getCandidates().size() - 1); } } } protected void pruneNearest() { Collections.sort(getCandidates()); ArrayList<Date> beforeDates = new ArrayList<Date>(); for (Date candidate : getCandidates()) { if (candidate.before(startDate)) { beforeDates.add(candidate); } } getCandidates().removeAll(beforeDates); if (getCandidates().size() > maximumCandidates) { logger.trace("Pruning from {} to {} farthest candidates", getCandidates().size(), maximumCandidates); int size = getCandidates().size(); for (int i = 1; i <= size - maximumCandidates; i++) { getCandidates().remove(0); } } } @Override public Date getTimeAfter(Date afterTime) { Date currentStartDate = getStartDate(); if (hasFloatingStartDate()) { try { clearCandidates(); setStartDate(afterTime); } catch (IllegalArgumentException | ParseException e) { logger.error("An exception occurred while setting the start date : '{}'", e.getMessage()); } } else if (getCandidates().isEmpty()) { try { setStartDate(afterTime); } catch (ParseException e) { logger.error("An exception occurred while setting the start date : '{}'", e.getMessage()); } } if (!getCandidates().isEmpty()) { if (getCandidates().size() == 1) { return getCandidates().get(0); } else { while (getCandidates().size() > 1) { Collections.sort(getCandidates()); Date newStartDate = null; try { for (Date candidate : getCandidates()) { newStartDate = candidate; if (candidate.after(afterTime)) { setStartDate(currentStartDate); return candidate; } } clearCandidates(); setStartDate(newStartDate); } catch (IllegalArgumentException | ParseException e) { logger.error("An exception occurred while parsing the expression : '{}'", e.getMessage()); } } } } return null; } @Override public Date getFinalFireTime() { try { parseExpression(getExpression(), false); } catch (ParseException e) { logger.error("An exception occurred while parsing the expression : '{}'", e.getMessage()); } Date lastCandidate = null; if (!getCandidates().isEmpty()) { lastCandidate = getCandidates().get(getCandidates().size() - 1); } return lastCandidate; } /** * Parses a token from the expression into an {@link ExpressionPart} */ abstract protected E parseToken(String token, int position) throws ParseException; /** * Helper function that is called to populate the list of candidates dates in case not enough candidates were * generated in a first instance */ abstract protected void populateWithSeeds(); public ExpressionPart getExpressionPart(Class<?> part) { for (ExpressionPart aPart : getExpressionParts()) { if (aPart.getClass().equals(part)) { return aPart; } } return null; } protected ArrayList<Date> getCandidates() { return candidates; } protected void setCandidates(ArrayList<Date> candidates) { this.candidates = candidates; } protected void clearCandidates() { this.candidates = null; } public ArrayList<E> getExpressionParts() { return expressionParts; } public void setExpressionParts(ArrayList<E> expressionParts) { synchronized (this) { this.expressionParts = expressionParts; } } }