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.time.Instant;
007import java.util.regex.Matcher;
008import java.util.regex.Pattern;
009
010import org.openstreetmap.josm.tools.Logging;
011import org.openstreetmap.josm.tools.UncheckedParseException;
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    /**
034     * Identifies when the changeset exception occurred.
035     */
036    public enum Source {
037        /**
038         * The exception was thrown when a changeset was updated. This most likely means
039         * that the changeset was closed before.
040         */
041        UPDATE_CHANGESET,
042        /**
043         * The exception was thrown when data was uploaded to the changeset. This most
044         * likely means that the servers capability limits for a changeset have been
045         * exceeded.
046         */
047        UPLOAD_DATA,
048        /**
049         * The exception was thrown when we tried to close a changeset.  Probably the changeset
050         * already timed out on the server.
051         * @since 18283
052         */
053        CLOSE_CHANGESET,
054        /**
055         * Unspecified source
056         */
057        UNSPECIFIED
058    }
059
060    /**
061     * Replies true if <code>errorHeader</code> matches with {@link #ERROR_HEADER_PATTERN}
062     *
063     * @param errorHeader the error header
064     * @return true if <code>errorHeader</code> matches with {@link #ERROR_HEADER_PATTERN}
065     */
066    public static boolean errorHeaderMatchesPattern(String errorHeader) {
067        if (errorHeader == null)
068            return false;
069        Pattern p = Pattern.compile(ERROR_HEADER_PATTERN);
070        Matcher m = p.matcher(errorHeader);
071        return m.matches();
072    }
073
074    /** the changeset id */
075    private long changesetId;
076    /** the date on which the changeset was closed */
077    private Instant closedOn;
078    /** the source */
079    private Source source;
080
081    protected final void parseErrorHeader(String errorHeader) {
082        Pattern p = Pattern.compile(ERROR_HEADER_PATTERN);
083        Matcher m = p.matcher(errorHeader);
084        if (m.matches()) {
085            changesetId = Long.parseLong(m.group(1));
086            try {
087                closedOn = DateUtils.parseInstant(m.group(2));
088            } catch (UncheckedParseException ex) {
089                Logging.error(tr("Failed to parse date ''{0}'' replied by server.", m.group(2)));
090                Logging.error(ex);
091            }
092        } else {
093            Logging.error(tr("Unexpected format of error header for conflict in changeset update. Got ''{0}''", errorHeader));
094        }
095    }
096
097    /**
098     * Creates the exception with the given <code>errorHeader</code>
099     *
100     * @param errorHeader the error header
101     */
102    public ChangesetClosedException(String errorHeader) {
103        super(errorHeader);
104        parseErrorHeader(errorHeader);
105        this.source = Source.UNSPECIFIED;
106    }
107
108    /**
109     * Creates the exception with the given error header and source.
110     *
111     * @param errorHeader the error header
112     * @param source the source for the exception
113     */
114    public ChangesetClosedException(String errorHeader, Source source) {
115        this(errorHeader, source, null);
116    }
117
118    /**
119     * Creates the exception with the given error header, source and cause.
120     *
121     * @param errorHeader the error header
122     * @param source the source for the exception
123     * @param cause  The cause (which is saved for later retrieval by the {@link #getCause} method).
124     *               A null value is permitted, and indicates that the cause is nonexistent or unknown.
125     * @since 13207
126     */
127    public ChangesetClosedException(String errorHeader, Source source, Throwable cause) {
128        super(errorHeader, cause);
129        parseErrorHeader(errorHeader);
130        this.source = source == null ? Source.UNSPECIFIED : source;
131    }
132
133    /**
134     * Creates the exception
135     *
136     * @param changesetId the id if the closed changeset
137     * @param closedOn the date the changeset was closed on
138     * @param source the source for the exception
139     */
140    public ChangesetClosedException(long changesetId, Instant closedOn, Source source) {
141        super("");
142        this.source = source == null ? Source.UNSPECIFIED : source;
143        this.changesetId = changesetId;
144        this.closedOn = closedOn;
145    }
146
147    /**
148     * Replies the id of the changeset which was closed
149     *
150     * @return the id of the changeset which was closed
151     */
152    public long getChangesetId() {
153        return changesetId;
154    }
155
156    /**
157     * Replies the date the changeset was closed
158     *
159     * @return the date the changeset was closed. May be null if the date isn't known.
160     */
161    public Instant getClosedOn() {
162        return closedOn;
163    }
164
165    /**
166     * Replies the source where the exception was thrown
167     *
168     * @return the source
169     */
170    public Source getSource() {
171        return source;
172    }
173
174    /**
175     * Sets the source where the exception was thrown
176     *
177     * @param source the source where the exception was thrown
178     */
179    public void setSource(Source source) {
180        this.source = source == null ? Source.UNSPECIFIED : source;
181    }
182}