source: josm/trunk/src/org/openstreetmap/josm/command/Command.java@ 17226

Last change on this file since 17226 was 17226, checked in by GerdP, 4 years ago

see #19885: memory leak with "temporary" objects in validator and actions

  • initialize cloneMap as Collections.emptyMap() to reduce memory footprint for Commamds

The original code allocated a HashMap for all commands, only some require it. Typically, the map contains 0 or 1 entries, so use Utils.toUnmodifiable() to optimize further.

  • Property svn:eol-style set to native
File size: 8.9 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.command;
3
4import java.util.ArrayList;
5import java.util.Collection;
6import java.util.Collections;
7import java.util.LinkedHashMap;
8import java.util.Map;
9import java.util.Map.Entry;
10import java.util.Objects;
11
12import org.openstreetmap.josm.data.coor.EastNorth;
13import org.openstreetmap.josm.data.coor.LatLon;
14import org.openstreetmap.josm.data.osm.DataSet;
15import org.openstreetmap.josm.data.osm.Node;
16import org.openstreetmap.josm.data.osm.OsmPrimitive;
17import org.openstreetmap.josm.data.osm.PrimitiveData;
18import org.openstreetmap.josm.data.osm.Relation;
19import org.openstreetmap.josm.data.osm.Way;
20import org.openstreetmap.josm.data.osm.visitor.OsmPrimitiveVisitor;
21import org.openstreetmap.josm.tools.CheckParameterUtil;
22import org.openstreetmap.josm.tools.Utils;
23
24/**
25 * Classes implementing Command modify a dataset in a specific way. A command is
26 * one atomic action on a specific dataset, such as move or delete.
27 *
28 * The command remembers the {@link DataSet} it is operating on.
29 *
30 * @author imi
31 * @since 21 (creation)
32 * @since 10599 (signature)
33 */
34public abstract class Command implements PseudoCommand {
35
36 /** IS_OK : operation is okay */
37 public static final int IS_OK = 0;
38 /** IS_OUTSIDE : operation on element outside of download area */
39 public static final int IS_OUTSIDE = 1;
40 /** IS_INCOMPLETE: operation on incomplete target */
41 public static final int IS_INCOMPLETE = 2;
42
43 private static final class CloneVisitor implements OsmPrimitiveVisitor {
44 final Map<OsmPrimitive, PrimitiveData> orig = new LinkedHashMap<>();
45
46 @Override
47 public void visit(Node n) {
48 orig.put(n, n.save());
49 }
50
51 @Override
52 public void visit(Way w) {
53 orig.put(w, w.save());
54 }
55
56 @Override
57 public void visit(Relation e) {
58 orig.put(e, e.save());
59 }
60 }
61
62 /**
63 * Small helper for holding the interesting part of the old data state of the objects.
64 */
65 public static class OldNodeState {
66
67 private final LatLon latLon;
68 private final EastNorth eastNorth; // cached EastNorth to be used for applying exact displacement
69 private final boolean modified;
70
71 /**
72 * Constructs a new {@code OldNodeState} for the given node.
73 * @param node The node whose state has to be remembered
74 */
75 public OldNodeState(Node node) {
76 latLon = node.getCoor();
77 eastNorth = node.getEastNorth();
78 modified = node.isModified();
79 }
80
81 /**
82 * Returns old lat/lon.
83 * @return old lat/lon
84 * @see Node#getCoor()
85 * @since 10248
86 */
87 public final LatLon getLatLon() {
88 return latLon;
89 }
90
91 /**
92 * Returns old east/north.
93 * @return old east/north
94 * @see Node#getEastNorth()
95 */
96 public final EastNorth getEastNorth() {
97 return eastNorth;
98 }
99
100 /**
101 * Returns old modified state.
102 * @return old modified state
103 * @see Node #isModified()
104 */
105 public final boolean isModified() {
106 return modified;
107 }
108
109 @Override
110 public int hashCode() {
111 return Objects.hash(latLon, eastNorth, modified);
112 }
113
114 @Override
115 public boolean equals(Object obj) {
116 if (this == obj) return true;
117 if (obj == null || getClass() != obj.getClass()) return false;
118 OldNodeState that = (OldNodeState) obj;
119 return modified == that.modified &&
120 Objects.equals(latLon, that.latLon) &&
121 Objects.equals(eastNorth, that.eastNorth);
122 }
123 }
124
125 /** the map of OsmPrimitives in the original state to OsmPrimitives in cloned state */
126 private Map<OsmPrimitive, PrimitiveData> cloneMap = Collections.emptyMap();
127
128 /** the dataset which this command is applied to */
129 private final DataSet data;
130
131 /**
132 * Creates a new command in the context of a specific data set, without data layer
133 *
134 * @param data the data set. Must not be null.
135 * @throws IllegalArgumentException if data is null
136 * @since 11240
137 */
138 protected Command(DataSet data) {
139 CheckParameterUtil.ensureParameterNotNull(data, "data");
140 this.data = data;
141 }
142
143 /**
144 * Executes the command on the dataset. This implementation will remember all
145 * primitives returned by fillModifiedData for restoring them on undo.
146 * <p>
147 * The layer should be invalidated after execution so that it can be re-painted.
148 * @return true
149 */
150 public boolean executeCommand() {
151 CloneVisitor visitor = new CloneVisitor();
152 Collection<OsmPrimitive> all = new ArrayList<>();
153 fillModifiedData(all, all, all);
154 for (OsmPrimitive osm : all) {
155 osm.accept(visitor);
156 }
157 cloneMap = Utils.toUnmodifiableMap(visitor.orig);
158 return true;
159 }
160
161 /**
162 * Undoes the command.
163 * It can be assumed that all objects are in the same state they were before.
164 * It can also be assumed that executeCommand was called exactly once before.
165 *
166 * This implementation undoes all objects stored by a former call to executeCommand.
167 */
168 public void undoCommand() {
169 for (Entry<OsmPrimitive, PrimitiveData> e : cloneMap.entrySet()) {
170 OsmPrimitive primitive = e.getKey();
171 if (primitive.getDataSet() != null) {
172 e.getKey().load(e.getValue());
173 }
174 }
175 }
176
177 /**
178 * Lets other commands access the original version
179 * of the object. Usually for undoing.
180 * @param osm The requested OSM object
181 * @return The original version of the requested object, if any
182 */
183 public PrimitiveData getOrig(OsmPrimitive osm) {
184 return cloneMap.get(osm);
185 }
186
187 /**
188 * Gets the data set this command affects.
189 * @return The data set. May be <code>null</code> if no layer was set and no edit layer was found.
190 * @since 10467
191 */
192 public DataSet getAffectedDataSet() {
193 return data;
194 }
195
196 /**
197 * Fill in the changed data this command operates on.
198 * Add to the lists, don't clear them.
199 *
200 * @param modified The modified primitives
201 * @param deleted The deleted primitives
202 * @param added The added primitives
203 */
204 public abstract void fillModifiedData(Collection<OsmPrimitive> modified,
205 Collection<OsmPrimitive> deleted,
206 Collection<OsmPrimitive> added);
207
208 /**
209 * Return the primitives that take part in this command.
210 * The collection is computed during execution.
211 */
212 @Override
213 public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
214 return cloneMap.keySet();
215 }
216
217 /**
218 * Check whether user is about to operate on data outside of the download area.
219 *
220 * @param primitives the primitives to operate on
221 * @param ignore {@code null} or a primitive to be ignored
222 * @return true, if operating on outlying primitives is OK; false, otherwise
223 */
224 public static int checkOutlyingOrIncompleteOperation(
225 Collection<? extends OsmPrimitive> primitives,
226 Collection<? extends OsmPrimitive> ignore) {
227 int res = 0;
228 for (OsmPrimitive osm : primitives) {
229 if (osm.isIncomplete()) {
230 res |= IS_INCOMPLETE;
231 } else if ((res & IS_OUTSIDE) == 0 && (osm.isOutsideDownloadArea()
232 || (osm instanceof Node && !osm.isNew() && osm.getDataSet() != null && osm.getDataSet().getDataSourceBounds().isEmpty()))
233 && (ignore == null || !ignore.contains(osm))) {
234 res |= IS_OUTSIDE;
235 }
236 }
237 return res;
238 }
239
240 /**
241 * Ensures that all primitives that are participating in this command belong to the affected data set.
242 *
243 * Commands may use this in their update methods to check the consitency of the primitives they operate on.
244 * @throws AssertionError if no {@link DataSet} is set or if any primitive does not belong to that dataset.
245 */
246 protected void ensurePrimitivesAreInDataset() {
247 for (OsmPrimitive primitive : this.getParticipatingPrimitives()) {
248 if (primitive.getDataSet() != this.getAffectedDataSet()) {
249 throw new AssertionError("Primitive is of wrong data set for this command: " + primitive);
250 }
251 }
252 }
253
254 @Override
255 public int hashCode() {
256 return Objects.hash(cloneMap, data);
257 }
258
259 @Override
260 public boolean equals(Object obj) {
261 if (this == obj) return true;
262 if (obj == null || getClass() != obj.getClass()) return false;
263 Command command = (Command) obj;
264 return Objects.equals(cloneMap, command.cloneMap) &&
265 Objects.equals(data, command.data);
266 }
267}
Note: See TracBrowser for help on using the repository browser.