Source code: org/modama/framework/operations/AbstractOperation.java
1 /**
2 Modama project, Institute for Polymer Research Dresden
3 Copyright (C) 2003 P. Fritsche, A. Uhlig
4 http://www.modama.org
5 info@modama.org
6
7 This program is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation; either version 2 of the License, or
10 (at your option) any later version.
11
12 This program is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 GNU General Public License for more details.
16
17 You should have received a copy of the GNU General Public License
18 along with this program; if not, write to the Free Software
19 Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20
21 file created: Mar 18, 2003
22
23 */
24 package org.modama.framework.operations;
25
26 import org.modama.framework.entities.AbstractEntity;
27 import org.modama.framework.AbstractFrameworkModel;
28 import org.modama.framework.tools.Resources;
29 import org.modama.framework.exceptions.DeleteCancelledException;
30 import org.modama.framework.world.World;
31 import org.modama.tools.BeanCloner;
32 import org.modama.Model;
33 import org.modama.gui.tools.Utils;
34 import org.apache.log4j.*;
35
36 import java.beans.*;
37 import java.util.List;
38 import java.util.LinkedList;
39 import java.util.Iterator;
40 import java.net.URL;
41 import java.io.IOException;
42 import java.io.OutputStream;
43 import java.io.FileWriter;
44 import java.io.FileOutputStream;
45
46 /**
47 * The abstract base class for an operation. An op consists of several inputentities,
48 * after setting needed inputs, an op can be executed to create an output entity.
49 *
50 * if you want to create a new operation extend this class, if your operation depends on the
51 * input, you can use the strategy pattern -> create innerclasses that implement Calculator, they
52 * are automaticly choosen by searching a calculator that accepts the current input
53 *
54 * in addition, if you change an input entity, the op will recalculate
55 * and set the new data to the resulting entity, what will also affect an event, etc..
56 *
57 * TODO add thread
58 * TODO test threadsafety
59
60 changes:
61 05/10/03 - synopia - added getInputCount(), need this to start calculation, when op is ready
62 */
63
64 public abstract class AbstractOperation extends AbstractFrameworkModel implements PropertyChangeListener
65 {
66 /**
67 * is fired when the execution thread finished
68 */
69 public static final String EVENT_EXECUTION_FINISHED = "opExecutionFinished";
70 public static final String EVENT_EXECUTION_STARTED = "opExecutionStarted";
71 public static final String EVENT_CONNECT = "opConnectWithEntity";
72 public static final String EVENT_DISCONNECT = "opDisconnectWithEntity";
73
74 protected static Logger logger = Logger.getLogger(AbstractOperation.class);
75
76 /**
77 * class of the entity that results by this operation
78 */
79 protected Class resultingClass;
80
81 /**
82 * the result of the operation or null if not yet executed
83 */
84 protected AbstractEntity output;
85
86 /**
87 * the input entities of the operation
88 */
89 protected List input;
90 /**
91 * when created an operator is inactive, by executing its acitvated and will automaticly recalc if input changes
92 */
93 protected boolean active = false;
94 /**
95 * this is a strategypattern, the concret calculation depends on the inputs and should be choosen when the inputlist changes
96 */
97 protected Calculator calculator;
98 /**
99 * an array with instances of all the inner classes that implement the Calculator interface,
100 * this is automaticly filled in the constructor
101 */
102 protected Calculator[] availabeCalculators;
103 /**
104 * interface for the strategypattern
105 */
106 interface Calculator {
107 public AbstractEntity calculate();
108 /**
109 * returns true if the current input is valid for this calculation
110 */
111 public boolean isValidInput();
112 public Class getOutputClass();
113 }
114
115 /**
116 * create an empty operation
117 */
118 public AbstractOperation()
119 {
120 this("operation");
121 }
122
123 /**
124 * create operation with given name
125 * @param name
126 */
127 public AbstractOperation( String name )
128 {
129 this.name = name;
130 input = new LinkedList();
131 fillAvailabeCalculatorsArray();
132 create();
133 }
134
135 /**
136 * this has to be implemented, all the available calculators have to be inserted here
137 */
138 protected abstract void fillAvailabeCalculatorsArray();
139 /**
140 * this piece of code doesnt work, because its impossible to create innerclasses like this, another searchalgorithm could
141 * be used and the calculator transformed to static innerclasses
142 */
143
144 /*
145 {
146 // get this classobject
147 Class me = this.getClass();
148 // get all the inner classes
149 Class[] classes = me.getClasses();
150 // count the calculatorclasses
151 int cnt = 0;
152 for (int i = 0; i < classes.length; i++)
153 if( Calculator.class.isAssignableFrom( classes[i] ) && !classes[i].isInterface() ) cnt++;
154 // if 0, return
155 if( cnt == 0 ) return;
156 // create the array
157 availabeCalculators = new Calculator[cnt];
158 // fill the array
159 cnt = 0;
160 for (int i = 0; i < classes.length; i++) {
161 if( Calculator.class.isAssignableFrom( classes[i] ) && !classes[i].isInterface() ) {
162 try {
163 availabeCalculators[cnt] = (Calculator) classes[i].newInstance();
164 } catch (InstantiationException e) {
165 e.printStackTrace(); //To change body of catch statement use Options | File Templates.
166 } catch (IllegalAccessException e) {
167 e.printStackTrace(); //To change body of catch statement use Options | File Templates.
168 }
169 cnt++;
170 }
171 }
172 }
173 */
174
175 /**
176 * put the entity to the world
177 */
178 protected void create() {
179 World.getInstance().put( this );
180 }
181
182 /**
183 * delete the entity from the world
184 * @throws org.modama.framework.exceptions.DeleteCancelledException
185 */
186 public void delete() throws DeleteCancelledException {
187 World.getInstance().remove( this );
188 }
189
190 /**
191 * uses the XMLDecoder to load the object from an inputstream
192 * @param url
193 * @return
194 * @throws IOException
195 */
196 public boolean load(URL url) throws IOException {
197 World.getInstance().setAcceptObjects( false );
198 XMLDecoder xml = new XMLDecoder( url.openStream() );
199 AbstractOperation loadedobject = null;
200 try {
201 loadedobject = (AbstractOperation) xml.readObject();
202 xml.close();
203 BeanCloner.clone( loadedobject, this, this.getClass() );
204 // successfull loaded
205 state = SAVED;
206 World.getInstance().setAcceptObjects( true );
207 return true;
208 } catch (Exception e) {
209 xml.close();
210 }
211 World.getInstance().setAcceptObjects( true );
212 return false;
213 }
214
215 /**
216 * uses the XMLEncoder to save the object
217 */
218 public void save(URL url) throws IOException {
219 World.getInstance().setAcceptObjects( false );
220 XMLEncoder xml = new XMLEncoder( new FileOutputStream( url.getFile() ) );
221 xml.writeObject( this );
222 xml.close();
223 World.getInstance().setAcceptObjects( true );
224 // successfull saved
225 state = SAVED;
226 }
227
228
229 /**
230 * execute the operation, if the last created output entity is of the same type as the one we will create this time,
231 * it is reused, otherwise a new one is created
232 * @return the resulting entity of the operation, null if no valid input
233 */
234 public AbstractEntity execute()
235 {
236 // check if we can at all produce an output
237 if( resultingClass == null ) return null;
238 // check if the input is not empty
239 for (Iterator iterator = input.iterator(); iterator.hasNext();) {
240 AbstractEntity entity = (AbstractEntity) iterator.next();
241 if( entity.isEmpty() ){
242 logger.error("Cannot execute with empty input");
243 return null;
244 }
245 }
246 // check if we have a valid output entity
247 if( output == null || output.getClass() != resultingClass /*|| output.getState() == Model.DELETED*/ ) {
248 // we have to create a new output class
249 createOutputEntity();
250 }
251 fire( EVENT_EXECUTION_STARTED );
252 output = calculate();
253 active = true;
254 fire( EVENT_EXECUTION_FINISHED, output );
255 return output;
256 }
257
258 /**
259 * get the result of the operation
260 * @return
261 */
262 public AbstractEntity Output() {
263 return output;
264 }
265
266 /**
267 * returns true if the operation is active, what means it listens for changes in the input and recalculates automaticly
268 * @return
269 */
270 public boolean isActive() {
271 return active;
272 }
273
274 /**
275 * if set to false, the operation will not recalc automaticly if inputdata changes
276 * @param active
277 */
278 public void setActive(boolean active) {
279 this.active = active;
280 }
281
282 /**
283 * an inputproperty has changed, normaly this means we have to recalculate
284 * TODO do handling for partly changes of lists, so only changed parts have to be recalculated
285 * @param evt
286 */
287 public void propertyChange(PropertyChangeEvent evt) {
288 if( canExecute() || active )
289 execute();
290 }
291
292
293 /**
294 * input methods
295 */
296
297 /**
298 * add this entity to the input<br>
299 * IllegalArgumentException is thrown if the entity is not accepted (this should be tested bevor by using the method <code>accept</code>)
300 * @param entity
301 */
302 public void put( AbstractEntity entity )
303 {
304 // throw exceptions if entity not accepted, we will also not accept the output as input
305 if( !accept(entity) || entity == output ) throw new IllegalArgumentException();
306 // register me as listener
307 entity.addPropertyChangeListener( this );
308 // add entity to inputlist
309 input.add( entity );
310 // search a strategy
311 searchCalculator();
312
313 fire( EVENT_CONNECT, entity );
314 }
315
316 /**
317 * replaces the old entity versus the new<br>
318 * IllegalArgumentException is thrown if the entity is not accepted
319 * @param newentity
320 * @param oldentity
321 */
322 //public abstract void replace( AbstractEntity newentity, AbstractEntity oldentity );
323
324 /**
325 * removes all input and the output
326 */
327 public void removeAll()
328 {
329 setOutput( null );
330 removeAllInput();
331 }
332
333 /**
334 * removes all input
335 */
336 public void removeAllInput()
337 {
338 Iterator it = input.iterator();
339 // remove me from listenerlist
340 while( it.hasNext() ) {
341 AbstractEntity entity = (AbstractEntity) it.next();
342 entity.removePropertyChangeListener( this );
343 it.remove();
344 }
345 // search a strategy
346 searchCalculator();
347
348 fire( EVENT_DISCONNECT );
349 }
350
351 /**
352 * removes the entity from the inputlist, or removes the output if entity is the outputentity
353 * @param entity
354 */
355 public void remove( AbstractEntity entity )
356 {
357 if( entity == output )
358 {
359 setOutput( null );
360 }
361 else
362 {
363 // remove me from listenerlist
364 entity.removePropertyChangeListener( this );
365 // remove entity from list
366 input.remove( entity );
367 // search a strategy
368 searchCalculator();
369 }
370 fire( EVENT_DISCONNECT, entity);
371 }
372
373 /**
374 * returns true if the entity will be accepted if put to the operation
375 * @param entity
376 * @return
377 */
378 public abstract boolean accept( AbstractEntity entity );
379
380 /**
381 * end
382 * input methods
383 */
384
385 /**
386 * here YOU can place your code for the calculation if you dont want to use the strategypattern, but in this case
387 * you also have to change the search method (because it wont find anything and so sets some values to null),
388 * so you sould use the pattern :)
389 * @return
390 */
391 protected AbstractEntity calculate(){
392 if( calculator != null ) {
393 return calculator.calculate();
394 }
395 return null;
396 }
397
398 /**
399 * uses reflection to search for a fitting calculator<br>
400 * if one is found, that the outputClass
401 */
402 protected void searchCalculator()
403 {
404 if( availabeCalculators != null ) {
405 // for each availabe calculator
406 for (int i = 0; i < availabeCalculators.length; i++) {
407 Calculator calc = availabeCalculators[i];
408 // acceppts it the input?
409 if( calc.isValidInput() ) {
410 // that the outputclass
411 resultingClass = calc.getOutputClass();
412 // set the strategy
413 calculator = calc;
414 // return, because we found one
415 return;
416 }
417 }
418 }
419 // nothing found
420 resultingClass = null;
421 calculator = null;
422 }
423
424 /**
425 * creates an instance of the resulting class
426 */
427 protected void createOutputEntity()
428 {
429 try
430 {
431 output = ( AbstractEntity )resultingClass.newInstance();
432 // fire a connections event
433 fire( EVENT_CONNECT, output );
434 } catch( InstantiationException e1 )
435 {
436 logger.error("Cannot istantiate Class "+resultingClass.getName(), e1);
437 } catch( IllegalAccessException e1 )
438 {
439 logger.error("Cannot istantiate Class "+resultingClass.getName(), e1);
440 }
441 }
442
443 /**
444 * returns the number of inputs
445 * @return
446 */
447 public int getInputCount()
448 {
449 return input.size();
450 }
451
452 public abstract boolean canExecute();
453
454 public List getInputs() { return input; }
455
456 public AbstractEntity getOutput() {
457 //if( output != null && output.getState() == Model.DELETED ) output = null;
458 return output;
459 }
460
461 public void setOutput(AbstractEntity output) {
462 this.output = output;
463 }
464
465 /**
466 * get a string for the classname from the resources
467 * @return
468 */
469 public String toString() {
470 return Resources.getString( Utils.getClassNameWithoutPackage( this.getClass() ) );
471 }
472 }