001 /* 002 * Gridarta MMORPG map editor for Crossfire, Daimonin and similar games. 003 * Copyright (C) 2000-2010 The Gridarta Developers. 004 * 005 * This program is free software; you can redistribute it and/or modify 006 * it under the terms of the GNU General Public License as published by 007 * the Free Software Foundation; either version 2 of the License, or 008 * (at your option) any later version. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 013 * GNU General Public License for more details. 014 * 015 * You should have received a copy of the GNU General Public License along 016 * with this program; if not, write to the Free Software Foundation, Inc., 017 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 018 */ 019 020 package net.sf.gridarta.model.validation.errors; 021 022 import java.io.Serializable; 023 import java.util.ArrayList; 024 import java.util.Collection; 025 import java.util.Collections; 026 import java.util.Comparator; 027 import java.util.List; 028 import net.sf.gridarta.model.archetype.Archetype; 029 import net.sf.gridarta.model.gameobject.GameObject; 030 import net.sf.gridarta.model.maparchobject.MapArchObject; 031 import net.sf.gridarta.model.mapmodel.MapModel; 032 import net.sf.gridarta.model.mapmodel.MapSquare; 033 import net.sf.japi.swing.action.ActionBuilder; 034 import net.sf.japi.swing.action.ActionBuilderFactory; 035 import org.jetbrains.annotations.NotNull; 036 import org.jetbrains.annotations.Nullable; 037 038 /** 039 * Super class of all errors that could occur during map validation. The base 040 * methods of this class all return null and might return meaningful values if 041 * overridden in a subclass. 042 * @author <a href="mailto:cher@riedquat.de">Christian Hujer</a> 043 * @author Andreas Kirschbaum 044 */ 045 public abstract class ValidationError<G extends GameObject<G, A, R>, A extends MapArchObject<A>, R extends Archetype<G, A, R>> implements Serializable { 046 047 /** 048 * The maximum number of map squares to show in the title. 049 */ 050 private static final int TITLE_MAP_SQUARES = 3; 051 052 /** 053 * The maximum number of game objects to show in the title. 054 */ 055 private static final int TITLE_GAME_OBJECTS = 5; 056 057 /** 058 * The serial version UID. 059 */ 060 private static final long serialVersionUID = 1L; 061 062 /** 063 * Action Builder. 064 */ 065 @NotNull 066 private static final ActionBuilder ACTION_BUILDER = ActionBuilderFactory.getInstance().getActionBuilder("net.sf.gridarta"); 067 068 /** 069 * Key. 070 * @serial 071 */ 072 @NotNull 073 private final String key; 074 075 /** 076 * The {@link MapModel} that caused the error. 077 * @serial 078 */ 079 @NotNull 080 private final MapModel<G, A, R> mapModel; 081 082 /** 083 * The {@link MapSquare MapSquares} that caused the error. 084 * @serial 085 */ 086 @NotNull 087 private final List<MapSquare<G, A, R>> mapSquares = new ArrayList<MapSquare<G, A, R>>(0); 088 089 /** 090 * The {@link GameObject GameObjects} that caused the error. 091 * @serial 092 */ 093 @NotNull 094 private final List<G> gameObjects = new ArrayList<G>(0); 095 096 /** 097 * The {@link Formatter} for formatting {@link MapSquare} instances. 098 * @serial 099 */ 100 @NotNull 101 private final MapSquareFormatter mapSquareFormatter = new MapSquareFormatter(); 102 103 /** 104 * The {@link Formatter} for formatting {@link GameObject} instances. 105 * @serial 106 */ 107 @NotNull 108 private final GameObjectFormatter gameObjectFormatter = new GameObjectFormatter(); 109 110 /** 111 * Creates a new instance. The key is generated from the validator's name, 112 * which must end on "Checker". 113 * @param mapModel the map model that caused the error 114 * @throws IllegalArgumentException in case the validator's name does not 115 * end on "Checker" 116 */ 117 protected ValidationError(@NotNull final MapModel<G, A, R> mapModel) throws IllegalArgumentException { 118 final String name = getClass().getSimpleName(); 119 if (!name.endsWith("Error")) { 120 throw new IllegalArgumentException("Class name must end with \"Error\""); 121 } 122 key = "Validator." + name.substring(0, name.indexOf("Error")); 123 this.mapModel = mapModel; 124 } 125 126 /** 127 * Creates a new instance. 128 * @param key Key 129 * @param mapModel the map model that caused the error 130 */ 131 protected ValidationError(@NotNull final String key, @NotNull final MapModel<G, A, R> mapModel) { 132 this.key = key; 133 this.mapModel = mapModel; 134 } 135 136 /** 137 * Return the map on which the error occurred. 138 * @return the erroneous map 139 */ 140 @NotNull 141 public MapModel<G, A, R> getMapModel() { 142 return mapModel; 143 } 144 145 /** 146 * Returns the {@link MapSquare MapSquares} that caused the error. 147 * @return the map squares 148 */ 149 @NotNull 150 public List<MapSquare<G, A, R>> getMapSquares() { 151 return Collections.unmodifiableList(mapSquares); 152 } 153 154 /** 155 * Adds an erroneous {@link MapSquare}. 156 * @param mapSquare the erroneous map square 157 */ 158 protected void addMapSquare(@NotNull final MapSquare<G, A, R> mapSquare) { 159 if (!mapSquares.contains(mapSquare)) { 160 if (mapSquare.getMapModel() != mapModel) { 161 throw new IllegalArgumentException(); 162 } 163 mapSquares.add(mapSquare); 164 } 165 } 166 167 /** 168 * Returns the {@link GameObject GameObjects} that caused the error. 169 * @return the game objects 170 */ 171 @NotNull 172 @SuppressWarnings("TypeMayBeWeakened") 173 public List<G> getGameObjects() { 174 return Collections.unmodifiableList(gameObjects); 175 } 176 177 /** 178 * Adds an erroneous {@link GameObject}. 179 * @param gameObject the erroneous game object 180 */ 181 protected void addGameObject(@NotNull final G gameObject) { 182 if (!gameObjects.contains(gameObject)) { 183 gameObjects.add(gameObject); 184 final MapSquare<G, A, R> mapSquare = gameObject.getMapSquare(); 185 if (mapSquare != null) { 186 addMapSquare(mapSquare); 187 } 188 } 189 } 190 191 /** 192 * Returns a parameter string to be used in the error message. It can be 193 * accessed with {4+<code>id</code>}. 194 * @param id the error id 195 * @return the error string 196 */ 197 @Nullable 198 public abstract String getParameter(final int id); 199 200 /** 201 * Returns the error message for this validation error. 202 * @return the error message for this validation error 203 * @todo validation errors should be able to provide their own arguments for 204 * message formatting 205 */ 206 @NotNull 207 public String getMessage() { 208 final int x; 209 final int y; 210 if (mapSquares.isEmpty()) { 211 x = 0; 212 y = 0; 213 } else { 214 final MapSquare<G, A, R> mapSquare = mapSquares.get(0); 215 x = mapSquare != null ? mapSquare.getMapX() : -1; 216 y = mapSquare != null ? mapSquare.getMapY() : -1; 217 } 218 final String archName = gameObjects.isEmpty() ? null : gameObjects.get(0).getBestName(); 219 final String parameter0 = getParameter(0); 220 final String parameter1 = getParameter(1); 221 final String parameter2 = getParameter(2); 222 final String parameter3 = getParameter(3); 223 final StringBuilder sb = new StringBuilder(); 224 if (mapSquares.size() > 1) { 225 appendMapSquares(sb); 226 } 227 appendTitle(sb); 228 appendGameObjects(sb); 229 return ACTION_BUILDER.format(key + ".msg", sb.toString(), x, y, archName, parameter0, parameter1, parameter2, parameter3); 230 } 231 232 /** 233 * {@inheritDoc} The String representation of an error is its title. 234 */ 235 @Nullable 236 @Override 237 public String toString() { 238 final StringBuilder sb = new StringBuilder(); 239 appendMapSquares(sb); 240 appendTitle(sb); 241 appendGameObjects(sb); 242 return sb.toString(); 243 } 244 245 /** 246 * Appends the map squares of this error to a {@link StringBuilder}. 247 * @param sb the string builder 248 */ 249 private void appendMapSquares(@NotNull final StringBuilder sb) { 250 appendObjects(sb, mapSquares, mapSquareFormatter, "[", "] ", TITLE_MAP_SQUARES); 251 } 252 253 /** 254 * Appends the title of this error to a {@link StringBuilder}. 255 * @param sb the string builder 256 */ 257 private void appendTitle(@NotNull final StringBuilder sb) { 258 final String title = ACTION_BUILDER.getString(key + ".title"); 259 if (title == null) { 260 sb.append(key); 261 } else { 262 sb.append(title); 263 } 264 } 265 266 /** 267 * Appends the game objects of this error to a {@link StringBuilder}. 268 * @param sb the string builder 269 */ 270 private void appendGameObjects(@NotNull final StringBuilder sb) { 271 appendObjects(sb, gameObjects, gameObjectFormatter, " [", "]", TITLE_GAME_OBJECTS); 272 } 273 274 /** 275 * Appends the given objects to a {@link StringBuilder}. 276 * @param sb the string builder 277 * @param objects the objects to format 278 * @param formatter the formatter for formatting the objects 279 * @param prefix the prefix to prepend to the objects 280 * @param postfix the postfix to append to the objects 281 * @param maxObjects the maximum number of objects to show 282 */ 283 private static <O> void appendObjects(@NotNull final StringBuilder sb, @NotNull final Collection<O> objects, @NotNull final Formatter<O> formatter, @NotNull final String prefix, @NotNull final String postfix, final int maxObjects) { 284 if (objects.isEmpty()) { 285 return; 286 } 287 288 final List<O> sortedObjects = new ArrayList<O>(objects); 289 Collections.sort(sortedObjects, formatter); 290 291 sb.append(prefix); 292 int count = 0; 293 for (final O object : sortedObjects) { 294 if (count > 0) { 295 sb.append(' '); 296 } 297 sb.append(formatter.toString(object)); 298 count++; 299 if (count >= maxObjects && sortedObjects.size() > maxObjects + 1) { 300 sb.append("..."); 301 break; 302 } 303 } 304 sb.append(postfix); 305 } 306 307 /** 308 * Interface for formatting objects. 309 * @param <O> the object type to format 310 * @author Andreas Kirschbaum 311 */ 312 private interface Formatter<O> extends Comparator<O>, Serializable { 313 314 /** 315 * Returns a string representation of an object. 316 * @param obj the object 317 * @return the string representation 318 */ 319 @NotNull 320 String toString(@NotNull O obj); 321 322 } 323 324 /** 325 * A {@link Formatter} for {@link GameObject GameObjects}. 326 * @author Andreas Kirschbaum 327 */ 328 private class GameObjectFormatter implements Formatter<G> { 329 330 /** 331 * The serial version UID. 332 */ 333 private static final long serialVersionUID = 1L; 334 335 @NotNull 336 @Override 337 public String toString(@NotNull final G obj) { 338 return obj.getBestName(); 339 } 340 341 @Override 342 public int compare(@NotNull final G o1, @NotNull final G o2) { 343 return toString(o1).compareTo(toString(o2)); 344 } 345 346 } 347 348 /** 349 * A {@link Formatter} for {@link MapSquare MapSquares}. 350 * @author Andreas Kirschbaum 351 */ 352 private class MapSquareFormatter implements Formatter<MapSquare<G, A, R>> { 353 354 /** 355 * The serial version UID. 356 */ 357 private static final long serialVersionUID = 1L; 358 359 @NotNull 360 @Override 361 public String toString(@NotNull final MapSquare<G, A, R> obj) { 362 return obj.getMapX() + "|" + obj.getMapY(); 363 } 364 365 @Override 366 public int compare(@NotNull final MapSquare<G, A, R> o1, @NotNull final MapSquare<G, A, R> o2) { 367 final int y1 = o1.getMapY(); 368 final int y2 = o2.getMapY(); 369 if (y1 < y2) { 370 return -1; 371 } else if (y1 > y2) { 372 return +1; 373 } 374 375 final int x1 = o1.getMapX(); 376 final int x2 = o2.getMapX(); 377 if (x1 < x2) { 378 return -1; 379 } else if (x1 > x2) { 380 return +1; 381 } 382 383 return 0; 384 } 385 386 } 387 388 }