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}