001/*
002 * #%L
003 * Netarchivesuite - harvester
004 * %%
005 * Copyright (C) 2005 - 2014 The Royal Danish Library, the Danish State and University Library,
006 *             the National Library of France and the Austrian National Library.
007 * %%
008 * This program is free software: you can redistribute it and/or modify
009 * it under the terms of the GNU Lesser General Public License as
010 * published by the Free Software Foundation, either version 2.1 of the
011 * License, or (at your option) any later version.
012 * 
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Lesser Public License for more details.
017 * 
018 * You should have received a copy of the GNU General Lesser Public
019 * License along with this program.  If not, see
020 * <http://www.gnu.org/licenses/lgpl-2.1.html>.
021 * #L%
022 */
023
024package dk.netarkivet.harvester.datamodel;
025
026import java.sql.Connection;
027import java.sql.PreparedStatement;
028import java.sql.ResultSet;
029import java.sql.SQLException;
030import java.sql.Statement;
031import java.sql.Types;
032import java.util.Date;
033import java.util.Iterator;
034import java.util.List;
035
036import org.slf4j.Logger;
037import org.slf4j.LoggerFactory;
038
039import dk.netarkivet.common.exceptions.ArgumentNotValid;
040import dk.netarkivet.common.exceptions.IOFailure;
041import dk.netarkivet.common.exceptions.PermissionDenied;
042import dk.netarkivet.common.exceptions.UnknownID;
043import dk.netarkivet.common.utils.DBUtils;
044import dk.netarkivet.common.utils.ExceptionUtils;
045import dk.netarkivet.common.utils.FilterIterator;
046
047/**
048 * A database-based implementation of the ScheduleDAO.
049 * <p>
050 * The statements to create the tables are now in scripts/sql/createfullhddb.sql and scripts/sql/createfullhddb.mysql.
051 */
052public class ScheduleDBDAO extends ScheduleDAO {
053
054    /** The logger. */
055    private static final Logger log = LoggerFactory.getLogger(ScheduleDBDAO.class);
056
057    /**
058     * Constructor for this class, that only checks that the schedules table has the expected version.
059     */
060    protected ScheduleDBDAO() {
061        Connection connection = HarvestDBConnection.get();
062        try {
063            HarvesterDatabaseTables.checkVersion(connection, HarvesterDatabaseTables.SCHEDULES);
064        } finally {
065            HarvestDBConnection.release(connection);
066        }
067    }
068
069    /**
070     * Create a new schedule.
071     *
072     * @param schedule The schedule to create
073     * @throws ArgumentNotValid if schedule is null
074     * @throws PermissionDenied if a schedule already exists
075     */
076    public synchronized void create(Schedule schedule) {
077        ArgumentNotValid.checkNotNull(schedule, "schedule");
078
079        Connection c = HarvestDBConnection.get();
080        PreparedStatement s = null;
081        try {
082            if (exists(c, schedule.getName())) {
083                String msg = "Cannot create already existing schedule " + schedule;
084                log.debug(msg);
085                throw new PermissionDenied(msg);
086            }
087
088            s = c.prepareStatement("INSERT INTO schedules " + "( name, comments, startdate, enddate, maxrepeats, "
089                    + "timeunit, numtimeunits, anytime, onminute, onhour," + " ondayofweek, ondayofmonth, edition )"
090                    + "VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? )", Statement.RETURN_GENERATED_KEYS);
091            setScheduleParameters(s, schedule);
092            final long edition = 1;
093            s.setLong(13, edition);
094            s.executeUpdate();
095            schedule.setID(DBUtils.getGeneratedID(s));
096            schedule.setEdition(edition);
097        } catch (SQLException e) {
098            throw new IOFailure("SQL error while creating schedule " + schedule + "\n"
099                    + ExceptionUtils.getSQLExceptionCause(e), e);
100        } finally {
101            HarvestDBConnection.release(c);
102        }
103    }
104
105    /**
106     * Sets the first twelve parameters of a Schedule in the order. name, comments, startdate, enddate, maxrepeats,
107     * timeunit, numtimeunits, anytime, onminute, onhour, ondayofweek, ondayofmonth
108     *
109     * @param s a prepared SQL statement
110     * @param schedule a given schedule.
111     * @throws SQLException If the operation fails.
112     */
113    private void setScheduleParameters(PreparedStatement s, Schedule schedule) throws SQLException {
114        DBUtils.setName(s, 1, schedule, Constants.MAX_NAME_SIZE);
115        DBUtils.setComments(s, 2, schedule, Constants.MAX_COMMENT_SIZE);
116        final Date startDate = schedule.getStartDate();
117        final int fieldNum = 3;
118        DBUtils.setDateMaybeNull(s, fieldNum, startDate);
119        if (schedule instanceof TimedSchedule) {
120            TimedSchedule ts = (TimedSchedule) schedule;
121            DBUtils.setDateMaybeNull(s, 4, ts.getEndDate());
122            s.setNull(5, Types.BIGINT);
123        } else {
124            s.setNull(4, Types.DATE);
125            RepeatingSchedule rs = (RepeatingSchedule) schedule;
126            s.setLong(5, rs.getRepeats());
127        }
128        Frequency freq = schedule.getFrequency();
129        s.setInt(6, freq.ordinal());
130        s.setInt(7, freq.getNumUnits());
131        s.setBoolean(8, freq.isAnytime());
132        DBUtils.setIntegerMaybeNull(s, 9, freq.getOnMinute());
133        DBUtils.setIntegerMaybeNull(s, 10, freq.getOnHour());
134        DBUtils.setIntegerMaybeNull(s, 11, freq.getOnDayOfWeek());
135        DBUtils.setIntegerMaybeNull(s, 12, freq.getOnDayOfMonth());
136    }
137
138    /**
139     * Returns whether a named schedule exists.
140     *
141     * @param scheduleName The name of a schedule
142     * @return True if the schedule exists.
143     * @throws ArgumentNotValid if the schedulename is null or empty
144     */
145    public synchronized boolean exists(String scheduleName) {
146        ArgumentNotValid.checkNotNullOrEmpty(scheduleName, "String scheduleName");
147
148        Connection c = HarvestDBConnection.get();
149        try {
150            return exists(c, scheduleName);
151        } finally {
152            HarvestDBConnection.release(c);
153        }
154    }
155
156    /**
157     * Returns whether a named schedule exists.
158     *
159     * @param c An open connection to the harvestDatabase.
160     * @param scheduleName The name of a schedule
161     * @return True if the schedule exists.
162     */
163    private synchronized boolean exists(Connection c, String scheduleName) {
164        final int count = DBUtils.selectIntValue(c, "SELECT COUNT(*) FROM schedules WHERE name = ?", scheduleName);
165        return (1 == count);
166    }
167
168    /**
169     * Read an existing schedule.
170     *
171     * @param scheduleName the name of the schedule
172     * @return The schedule read
173     * @throws ArgumentNotValid if schedulename is null or empty
174     * @throws UnknownID if the schedule doesn't exist
175     */
176    public synchronized Schedule read(String scheduleName) {
177        ArgumentNotValid.checkNotNullOrEmpty(scheduleName, "String scheduleName");
178        Connection c = HarvestDBConnection.get();
179        PreparedStatement s = null;
180        try {
181            s = c.prepareStatement("SELECT schedule_id, comments, startdate, " + "enddate, maxrepeats, timeunit, "
182                    + "numtimeunits, anytime, onminute, " + "onhour, ondayofweek, ondayofmonth, edition "
183                    + "FROM schedules WHERE name = ?");
184            s.setString(1, scheduleName);
185            ResultSet rs = s.executeQuery();
186            if (!rs.next()) {
187                throw new UnknownID("No schedule named '" + scheduleName + "' found");
188            }
189            long id = rs.getLong(1);
190            boolean isTimedSchedule;
191            String comments = rs.getString(2);
192            Date startdate = DBUtils.getDateMaybeNull(rs, 3);
193            Date enddate = DBUtils.getDateMaybeNull(rs, 4);
194            int maxrepeats = rs.getInt(5);
195            isTimedSchedule = rs.wasNull();
196            int timeunit = rs.getInt(6);
197            int numtimeunits = rs.getInt(7);
198            boolean anytime = rs.getBoolean(8);
199            Integer minute = DBUtils.getIntegerMaybeNull(rs, 9);
200            Integer hour = DBUtils.getIntegerMaybeNull(rs, 10);
201            Integer dayofweek = DBUtils.getIntegerMaybeNull(rs, 11);
202            Integer dayofmonth = DBUtils.getIntegerMaybeNull(rs, 12);
203            if (log.isDebugEnabled()) {
204                log.debug("Creating frequency for (timeunit,anytime,numtimeunits,hour, minute, dayofweek, dayofmonth)"
205                        + " = ({},{},{},{},{},{},{},)", timeunit, anytime, numtimeunits, minute, hour, dayofweek,
206                        dayofmonth);
207            }
208            Frequency freq = Frequency.getNewInstance(timeunit, anytime, numtimeunits, minute, hour, dayofweek,
209                    dayofmonth);
210            long edition = rs.getLong(13);
211            final Schedule schedule;
212            if (isTimedSchedule) {
213                schedule = Schedule.getInstance(startdate, enddate, freq, scheduleName, comments);
214            } else {
215                schedule = Schedule.getInstance(startdate, maxrepeats, freq, scheduleName, comments);
216            }
217            schedule.setID(id);
218            schedule.setEdition(edition);
219            return schedule;
220        } catch (SQLException e) {
221            throw new IOFailure("SQL error reading schedule " + scheduleName + "\n"
222                    + ExceptionUtils.getSQLExceptionCause(e), e);
223        } finally {
224            DBUtils.closeStatementIfOpen(s);
225            HarvestDBConnection.release(c);
226        }
227    }
228
229    /**
230     * Update a schedule in the DAO.
231     *
232     * @param schedule The schedule to update
233     * @throws ArgumentNotValid If the schedule is null
234     * @throws UnknownID If the schedule doesn't exist in the DAO
235     * @throws PermissionDenied If the edition of the schedule to update is older than the DAO's
236     */
237    public synchronized void update(Schedule schedule) {
238        ArgumentNotValid.checkNotNull(schedule, "schedule");
239
240        Connection c = HarvestDBConnection.get();
241        PreparedStatement s = null;
242        try {
243            if (!exists(c, schedule.getName())) {
244                throw new PermissionDenied("No schedule with name " + schedule.getName() + " exists");
245            }
246
247            s = c.prepareStatement("UPDATE schedules " + "SET name = ?," + "    comments = ?," + "    startdate = ?,"
248                    + "    enddate = ?," + "    maxrepeats = ?," + "    timeunit = ?," + "    numtimeunits = ?,"
249                    + "    anytime = ?," + "    onminute = ?," + "    onhour = ?, " + "    ondayofweek = ?,"
250                    + "    ondayofmonth = ?," + "    edition = ?" + " WHERE name = ? AND edition = ?");
251            setScheduleParameters(s, schedule);
252            long newEdition = schedule.getEdition() + 1;
253            s.setLong(13, newEdition);
254            s.setString(14, schedule.getName());
255            s.setLong(15, schedule.getEdition());
256            int rows = s.executeUpdate();
257            if (rows == 0) {
258                String message = "Edition " + schedule.getEdition() + " has expired, cannot update " + schedule;
259                log.debug(message);
260                throw new PermissionDenied(message);
261            }
262            schedule.setEdition(newEdition);
263        } catch (SQLException e) {
264            throw new IOFailure("SQL error while creating schedule " + schedule + "\n"
265                    + ExceptionUtils.getSQLExceptionCause(e), e);
266        } finally {
267            HarvestDBConnection.release(c);
268        }
269    }
270
271    /**
272     * Get iterator to all available schedules.
273     *
274     * @return iterator to all available schedules
275     */
276    public synchronized Iterator<Schedule> getAllSchedules() {
277        Connection c = HarvestDBConnection.get();
278        try {
279            List<String> names = DBUtils.selectStringList(c, "SELECT name FROM schedules ORDER BY name");
280            return new FilterIterator<String, Schedule>(names.iterator()) {
281                /**
282                 * Returns the object corresponding to the given object, or null if that object is to be skipped.
283                 *
284                 * @param s An object in the source iterator domain
285                 * @return An object in this iterators domain, or null
286                 */
287                public Schedule filter(String s) {
288                    return read(s);
289                }
290            };
291        } finally {
292            HarvestDBConnection.release(c);
293        }
294    }
295
296    @Override
297    public synchronized int getCountSchedules() {
298        Connection c = HarvestDBConnection.get();
299        try {
300            return DBUtils.selectIntValue(c, "SELECT COUNT(*) FROM schedules");
301        } finally {
302            HarvestDBConnection.release(c);
303        }
304    }
305
306}