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