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
020package net.sf.gridarta.model.validation.errors;
021
022import java.io.Serializable;
023import java.util.ArrayList;
024import java.util.Collection;
025import java.util.Collections;
026import java.util.List;
027import net.sf.gridarta.model.archetype.Archetype;
028import net.sf.gridarta.model.gameobject.GameObject;
029import net.sf.gridarta.model.maparchobject.MapArchObject;
030import net.sf.gridarta.model.mapmodel.MapModel;
031import net.sf.gridarta.model.mapmodel.MapSquare;
032import net.sf.japi.swing.action.ActionBuilder;
033import net.sf.japi.swing.action.ActionBuilderFactory;
034import org.jetbrains.annotations.NotNull;
035import org.jetbrains.annotations.Nullable;
036
037/**
038 * Super class of all errors that could occur during map validation. The base
039 * methods of this class all return null and might return meaningful values if
040 * overridden in a subclass.
041 * @author <a href="mailto:cher@riedquat.de">Christian Hujer</a>
042 * @author Andreas Kirschbaum
043 */
044public abstract class ValidationError<G extends GameObject<G, A, R>, A extends MapArchObject<A>, R extends Archetype<G, A, R>> implements Serializable {
045
046    /**
047     * The maximum number of map squares to show in the title.
048     */
049    private static final int TITLE_MAP_SQUARES = 3;
050
051    /**
052     * The maximum number of game objects to show in the title.
053     */
054    private static final int TITLE_GAME_OBJECTS = 5;
055
056    /**
057     * The serial version UID.
058     */
059    private static final long serialVersionUID = 1L;
060
061    /**
062     * Action Builder.
063     */
064    @NotNull
065    private static final ActionBuilder ACTION_BUILDER = ActionBuilderFactory.getInstance().getActionBuilder("net.sf.gridarta");
066
067    /**
068     * Key.
069     * @serial
070     */
071    @NotNull
072    private final String key;
073
074    /**
075     * The {@link MapModel} that caused the error.
076     * @serial
077     */
078    @NotNull
079    private final MapModel<G, A, R> mapModel;
080
081    /**
082     * The {@link MapSquare MapSquares} that caused the error.
083     * @serial
084     */
085    @NotNull
086    private final List<MapSquare<G, A, R>> mapSquares = new ArrayList<MapSquare<G, A, R>>(0);
087
088    /**
089     * The {@link GameObject GameObjects} that caused the error.
090     * @serial
091     */
092    @NotNull
093    private final List<G> gameObjects = new ArrayList<G>(0);
094
095    /**
096     * The {@link MapSquareFormatter} for formatting {@link
097     * MapSquare} instances.
098     * @serial
099     */
100    @NotNull
101    private final Formatter<G, A, R, MapSquare<G, A, R>> mapSquareFormatter = new MapSquareFormatter<G, A, R>();
102
103    /**
104     * The {@link MapSquareFormatter} for formatting {@link
105     * GameObject} instances.
106     * @serial
107     */
108    @NotNull
109    private final Formatter<G, A, R, G> gameObjectFormatter = new GameObjectFormatter<G, A, R>();
110
111    /**
112     * Creates a new instance. The key is generated from the validator's name,
113     * which must end on "Checker".
114     * @param mapModel the map model that caused the error
115     * @throws IllegalArgumentException in case the validator's name does not
116     * end on "Checker"
117     */
118    protected ValidationError(@NotNull final MapModel<G, A, R> mapModel) throws IllegalArgumentException {
119        final String name = getClass().getSimpleName();
120        if (!name.endsWith("Error")) {
121            throw new IllegalArgumentException("Class name must end with \"Error\"");
122        }
123        key = "Validator." + name.substring(0, name.indexOf("Error"));
124        this.mapModel = mapModel;
125    }
126
127    /**
128     * Creates a new instance.
129     * @param key Key
130     * @param mapModel the map model that caused the error
131     */
132    protected ValidationError(@NotNull final String key, @NotNull final MapModel<G, A, R> mapModel) {
133        this.key = key;
134        this.mapModel = mapModel;
135    }
136
137    /**
138     * Return the map on which the error occurred.
139     * @return the erroneous map
140     */
141    @NotNull
142    public MapModel<G, A, R> getMapModel() {
143        return mapModel;
144    }
145
146    /**
147     * Returns the {@link MapSquare MapSquares} that caused the error.
148     * @return the map squares
149     */
150    @NotNull
151    public List<MapSquare<G, A, R>> getMapSquares() {
152        return Collections.unmodifiableList(mapSquares);
153    }
154
155    /**
156     * Adds an erroneous {@link MapSquare}.
157     * @param mapSquare the erroneous map square
158     */
159    protected void addMapSquare(@NotNull final MapSquare<G, A, R> mapSquare) {
160        if (!mapSquares.contains(mapSquare)) {
161            if (mapSquare.getMapModel() != mapModel) {
162                throw new IllegalArgumentException();
163            }
164            mapSquares.add(mapSquare);
165        }
166    }
167
168    /**
169     * Returns the {@link GameObject GameObjects} that caused the error.
170     * @return the game objects
171     */
172    @NotNull
173    @SuppressWarnings("TypeMayBeWeakened")
174    public List<G> getGameObjects() {
175        return Collections.unmodifiableList(gameObjects);
176    }
177
178    /**
179     * Adds an erroneous {@link GameObject}.
180     * @param gameObject the erroneous game object
181     */
182    protected void addGameObject(@NotNull final G gameObject) {
183        if (!gameObjects.contains(gameObject)) {
184            gameObjects.add(gameObject);
185            final MapSquare<G, A, R> mapSquare = gameObject.getMapSquare();
186            if (mapSquare != null) {
187                addMapSquare(mapSquare);
188            }
189        }
190    }
191
192    /**
193     * Returns a parameter string to be used in the error message. It can be
194     * accessed with {4+<code>id</code>}.
195     * @param id the error id
196     * @return the error string
197     */
198    @Nullable
199    public abstract String getParameter(final int id);
200
201    /**
202     * Returns the error message for this validation error.
203     * @return the error message for this validation error
204     * @todo validation errors should be able to provide their own arguments for
205     * message formatting
206     */
207    @NotNull
208    public String getMessage() {
209        final int x;
210        final int y;
211        if (mapSquares.isEmpty()) {
212            x = 0;
213            y = 0;
214        } else {
215            final MapSquare<G, A, R> mapSquare = mapSquares.get(0);
216            x = mapSquare != null ? mapSquare.getMapX() : -1;
217            y = mapSquare != null ? mapSquare.getMapY() : -1;
218        }
219        final String archName = gameObjects.isEmpty() ? null : gameObjects.get(0).getBestName();
220        final String parameter0 = getParameter(0);
221        final String parameter1 = getParameter(1);
222        final String parameter2 = getParameter(2);
223        final String parameter3 = getParameter(3);
224        final StringBuilder sb = new StringBuilder();
225        if (mapSquares.size() > 1) {
226            appendMapSquares(sb);
227        }
228        appendTitle(sb);
229        appendGameObjects(sb);
230        return ACTION_BUILDER.format(key + ".msg", sb.toString(), x, y, archName, parameter0, parameter1, parameter2, parameter3);
231    }
232
233    /**
234     * {@inheritDoc} The String representation of an error is its title.
235     */
236    @Nullable
237    @Override
238    public String toString() {
239        final StringBuilder sb = new StringBuilder();
240        appendMapSquares(sb);
241        appendTitle(sb);
242        appendGameObjects(sb);
243        return sb.toString();
244    }
245
246    /**
247     * Appends the map squares of this error to a {@link StringBuilder}.
248     * @param sb the string builder
249     */
250    private void appendMapSquares(@NotNull final StringBuilder sb) {
251        appendObjects(sb, mapSquares, mapSquareFormatter, "[", "] ", TITLE_MAP_SQUARES);
252    }
253
254    /**
255     * Appends the title of this error to a {@link StringBuilder}.
256     * @param sb the string builder
257     */
258    private void appendTitle(@NotNull final StringBuilder sb) {
259        final String title = ACTION_BUILDER.getString(key + ".title");
260        if (title == null) {
261            sb.append(key);
262        } else {
263            sb.append(title);
264        }
265    }
266
267    /**
268     * Appends the game objects of this error to a {@link StringBuilder}.
269     * @param sb the string builder
270     */
271    private void appendGameObjects(@NotNull final StringBuilder sb) {
272        appendObjects(sb, gameObjects, gameObjectFormatter, " [", "]", TITLE_GAME_OBJECTS);
273    }
274
275    /**
276     * Appends the given objects to a {@link StringBuilder}.
277     * @param sb the string builder
278     * @param objects the objects to format
279     * @param formatter the formatter for formatting the objects
280     * @param prefix the prefix to prepend to the objects
281     * @param postfix the postfix to append to the objects
282     * @param maxObjects the maximum number of objects to show
283     */
284    private <O> void appendObjects(@NotNull final StringBuilder sb, @NotNull final Collection<O> objects, @NotNull final Formatter<G, A, R, O> formatter, @NotNull final String prefix, @NotNull final String postfix, final int maxObjects) {
285        if (objects.isEmpty()) {
286            return;
287        }
288
289        final List<O> sortedObjects = new ArrayList<O>(objects);
290        Collections.sort(sortedObjects, formatter);
291
292        sb.append(prefix);
293        int count = 0;
294        for (final O object : sortedObjects) {
295            if (count > 0) {
296                sb.append(' ');
297            }
298            sb.append(formatter.toString(object));
299            count++;
300            if (count >= maxObjects && sortedObjects.size() > maxObjects + 1) {
301                sb.append("...");
302                break;
303            }
304        }
305        sb.append(postfix);
306    }
307
308}