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    }