001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.text.ParseException;
007import java.util.Date;
008import java.util.regex.Matcher;
009import java.util.regex.Pattern;
010
011import org.openstreetmap.josm.tools.Logging;
012import org.openstreetmap.josm.tools.date.DateUtils;
013
014/**
015 * A ChangesetClosedException is thrown if the server replies with a HTTP
016 * return code 409 (Conflict) with the error header {@link #ERROR_HEADER_PATTERN}.
017 *
018 * Depending on the context the exception is thrown in we have to react differently.
019 * <ul>
020 *   <li>if it is thrown when we try to update a changeset, the changeset was most
021 *   likely closed before, either explicitly by the user or because of a timeout</li>
022 *   <li>if it is thrown when we try to upload data to the changeset, the changeset
023 *   was most likely closed because we reached the servers capability limit for the size
024 *   of a changeset.</li>
025 *  </ul>
026 */
027public class ChangesetClosedException extends OsmTransferException {
028    /** the error header pattern for in case of HTTP response 409 indicating
029     * that a changeset was closed
030     */
031    public static final String ERROR_HEADER_PATTERN = "The changeset (\\d+) was closed at (.*)";
032
033    public enum Source {
034        /**
035         * The exception was thrown when a changeset was updated. This most likely means
036         * that the changeset was closed before.
037         */
038        UPDATE_CHANGESET,
039        /**
040         * The exception was thrown when data was uploaded to the changeset. This most
041         * likely means that the servers capability limits for a changeset have been
042         * exceeded.
043         */
044        UPLOAD_DATA,
045        /**
046         * Unspecified source
047         */
048        UNSPECIFIED
049    }
050
051    /**
052     * Replies true if <code>errorHeader</code> matches with {@link #ERROR_HEADER_PATTERN}
053     *
054     * @param errorHeader the error header
055     * @return true if <code>errorHeader</code> matches with {@link #ERROR_HEADER_PATTERN}
056     */
057    public static boolean errorHeaderMatchesPattern(String errorHeader) {
058        if (errorHeader == null)
059            return false;
060        Pattern p = Pattern.compile(ERROR_HEADER_PATTERN);
061        Matcher m = p.matcher(errorHeader);
062        return m.matches();
063    }
064
065    /** the changeset id */
066    private long changesetId;
067    /** the date on which the changeset was closed */
068    private Date closedOn;
069    /** the source */
070    private Source source;
071
072    protected final void parseErrorHeader(String errorHeader) {
073        Pattern p = Pattern.compile(ERROR_HEADER_PATTERN);
074        Matcher m = p.matcher(errorHeader);
075        if (m.matches()) {
076            changesetId = Long.parseLong(m.group(1));
077            try {
078                closedOn = DateUtils.newOsmApiDateTimeFormat().parse(m.group(2));
079            } catch (ParseException ex) {
080                Logging.error(tr("Failed to parse date ''{0}'' replied by server.", m.group(2)));
081                Logging.error(ex);
082            }
083        } else {
084            Logging.error(tr("Unexpected format of error header for conflict in changeset update. Got ''{0}''", errorHeader));
085        }
086    }
087
088    /**
089     * Creates the exception with the given <code>errorHeader</code>
090     *
091     * @param errorHeader the error header
092     */
093    public ChangesetClosedException(String errorHeader) {
094        super(errorHeader);
095        parseErrorHeader(errorHeader);
096        this.source = Source.UNSPECIFIED;
097    }
098
099    /**
100     * Creates the exception with the given error header and source.
101     *
102     * @param errorHeader the error header
103     * @param source the source for the exception
104     */
105    public ChangesetClosedException(String errorHeader, Source source) {
106        this(errorHeader, source, null);
107    }
108
109    /**
110     * Creates the exception with the given error header, source and cause.
111     *
112     * @param errorHeader the error header
113     * @param source the source for the exception
114     * @param cause  The cause (which is saved for later retrieval by the {@link #getCause} method).
115     *               A null value is permitted, and indicates that the cause is nonexistent or unknown.
116     * @since 13207
117     */
118    public ChangesetClosedException(String errorHeader, Source source, Throwable cause) {
119        super(errorHeader, cause);
120        parseErrorHeader(errorHeader);
121        this.source = source == null ? Source.UNSPECIFIED : source;
122    }
123
124    /**
125     * Creates the exception
126     *
127     * @param changesetId the id if the closed changeset
128     * @param closedOn the date the changeset was closed on
129     * @param source the source for the exception
130     */
131    public ChangesetClosedException(long changesetId, Date closedOn, Source source) {
132        super("");
133        this.source = source == null ? Source.UNSPECIFIED : source;
134        this.changesetId = changesetId;
135        this.closedOn = DateUtils.cloneDate(closedOn);
136    }
137
138    /**
139     * Replies the id of the changeset which was closed
140     *
141     * @return the id of the changeset which was closed
142     */
143    public long getChangesetId() {
144        return changesetId;
145    }
146
147    /**
148     * Replies the date the changeset was closed
149     *
150     * @return the date the changeset was closed. May be null if the date isn't known.
151     */
152    public Date getClosedOn() {
153        return DateUtils.cloneDate(closedOn);
154    }
155
156    /**
157     * Replies the source where the exception was thrown
158     *
159     * @return the source
160     */
161    public Source getSource() {
162        return source;
163    }
164
165    public void setSource(Source source) {
166        this.source = source == null ? Source.UNSPECIFIED : source;
167    }
168}