1 | package org.dcache.cells; |
2 | |
3 | import java.util.Date; |
4 | import java.util.Properties; |
5 | import java.util.Enumeration; |
6 | import java.util.Set; |
7 | import java.util.TreeSet; |
8 | import java.util.Formatter; |
9 | import java.util.ArrayList; |
10 | import java.util.Map; |
11 | import java.util.HashMap; |
12 | import java.util.TreeMap; |
13 | import java.util.List; |
14 | import java.util.Collection; |
15 | import java.util.Collections; |
16 | import java.util.Arrays; |
17 | import java.util.concurrent.ExecutionException; |
18 | import java.util.concurrent.ExecutorService; |
19 | import java.io.ByteArrayOutputStream; |
20 | import java.io.File; |
21 | import java.io.FileWriter; |
22 | import java.io.StringWriter; |
23 | import java.io.PrintWriter; |
24 | import java.io.IOException; |
25 | import java.io.FileReader; |
26 | import java.io.BufferedReader; |
27 | import java.lang.reflect.InvocationTargetException; |
28 | import java.beans.PropertyDescriptor; |
29 | |
30 | import dmg.cells.nucleus.CellMessage; |
31 | import dmg.cells.nucleus.CellMessageAnswerable; |
32 | import dmg.cells.nucleus.NoRouteToCellException; |
33 | import dmg.cells.nucleus.CellAdapter; |
34 | import dmg.cells.nucleus.CellInfo; |
35 | import dmg.cells.nucleus.CellPath; |
36 | import dmg.cells.services.SetupInfoMessage; |
37 | import dmg.util.Args; |
38 | import dmg.util.CommandException; |
39 | |
40 | import org.dcache.util.ClassNameComparator; |
41 | |
42 | import org.springframework.context.ConfigurableApplicationContext; |
43 | import org.springframework.context.support.ClassPathXmlApplicationContext; |
44 | import org.springframework.core.io.ByteArrayResource; |
45 | import org.springframework.core.io.Resource; |
46 | import org.springframework.beans.BeansException; |
47 | import org.springframework.beans.factory.config.BeanPostProcessor; |
48 | import org.springframework.beans.factory.config.BeanDefinition; |
49 | import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; |
50 | import org.springframework.beans.factory.support.DefaultListableBeanFactory; |
51 | import org.springframework.beans.factory.NoSuchBeanDefinitionException; |
52 | import org.springframework.beans.BeanWrapper; |
53 | import org.springframework.beans.BeanWrapperImpl; |
54 | |
55 | /** |
56 | * Universal cell for building complex cells from simpler components. |
57 | * |
58 | * This class aims at being a universal cell for dCache. It makes it |
59 | * possible to construct complex cells from simpler cell |
60 | * components. The Spring Framework is used to manage the lifetime of |
61 | * the cell components, as well as the wiring between components. We |
62 | * therfore borrow the term "bean" from the Spring Framework and refer |
63 | * to cell components as beans. |
64 | * |
65 | * Beans can get access to core cell functionality by implementing one |
66 | * or more of the following interfaces: CellInfoProvider, |
67 | * CellCommunicationAware, ThreadFactoryAware, CellCommandListener, |
68 | * and CellSetupAware. When instantiated through this class, those |
69 | * interfaces are detected and the necessary wiring is performed |
70 | * automatically. |
71 | */ |
72 | public class UniversalSpringCell |
73 | extends AbstractCell |
74 | implements BeanPostProcessor |
75 | { |
76 | private final static long WAIT_FOR_FILE_SLEEP = 30000; |
77 | |
78 | /** |
79 | * Spring application context. All beans are created through this |
80 | * context. |
81 | */ |
82 | private ConfigurableApplicationContext _context; |
83 | |
84 | /** |
85 | * Registered info providers mapped to their bean names. Sorted to |
86 | * maintain consistent ordering. |
87 | */ |
88 | private final Map<CellInfoProvider,String> _infoProviders = |
89 | new TreeMap<CellInfoProvider,String>(new ClassNameComparator()); |
90 | |
91 | /** |
92 | * List of registered setup providers. Sorted to maintain |
93 | * consistent ordering. |
94 | */ |
95 | private final Set<CellSetupProvider> _setupProviders = |
96 | new TreeSet<CellSetupProvider>(new ClassNameComparator()); |
97 | |
98 | /** |
99 | * List of registered life cycle aware beans. Sorted to maintain |
100 | * consistent ordering. |
101 | */ |
102 | private final Set<CellLifeCycleAware> _lifeCycleAware = |
103 | new TreeSet<CellLifeCycleAware>(new ClassNameComparator()); |
104 | |
105 | |
106 | /** |
107 | * Cell name of the setup controller. |
108 | */ |
109 | private String _setupController; |
110 | |
111 | /** |
112 | * Setup class used for sending a setup to the setup controller. |
113 | */ |
114 | private String _setupClass; |
115 | |
116 | /** |
117 | * Setup to execute during start and to which to save the setup. |
118 | */ |
119 | private File _setupFile; |
120 | |
121 | public UniversalSpringCell(String cellName, String arguments) |
122 | throws InterruptedException, ExecutionException |
123 | { |
124 | super(cellName, arguments); |
125 | doInit(); |
126 | } |
127 | |
128 | @Override |
129 | protected void executeInit() |
130 | throws Exception |
131 | { |
132 | /* Process command line arguments. |
133 | */ |
134 | Args args = getArgs(); |
135 | if (args.argc() == 0) |
136 | throw new IllegalArgumentException("Configuration location missing"); |
137 | |
138 | _setupController = args.getOpt("setupController"); |
139 | info("Setup controller set to " |
140 | + (_setupController == null ? "none" : _setupController)); |
141 | _setupFile = |
142 | (args.getOpt("setupFile") == null) |
143 | ? null |
144 | : new File(args.getOpt("setupFile")); |
145 | _setupClass = args.getOpt("setupClass"); |
146 | |
147 | if (_setupController != null && _setupClass == null) |
148 | throw new IllegalArgumentException("Setup class must be specified when a setup controller is used"); |
149 | |
150 | /* Instantiate Spring application context. This will |
151 | * eagerly instantiate all beans. |
152 | */ |
153 | _context = |
154 | new UniversalSpringCellApplicationContext(getArgs()); |
155 | |
156 | /* Cell threading is configurable through arguments to |
157 | * UniversalSpringCell. The executors have to be created as |
158 | * beans in the Spring file, however the names of the beans |
159 | * are provided as cell arguments. |
160 | */ |
161 | setupCellExecutors(args.getOpt("callbackExecutor"), |
162 | args.getOpt("messageExecutor")); |
163 | |
164 | /* This is a NOP except if somebody subclassed |
165 | * UniversalSpringCell. |
166 | */ |
167 | init(); |
168 | |
169 | /* The timeout task is essential to handle cell message |
170 | * timeouts. |
171 | */ |
172 | startTimeoutTask(); |
173 | |
174 | /* To ensure that all required file systems are mounted, the |
175 | * admin may specify some required files. We will block until |
176 | * they become available. |
177 | */ |
178 | waitForFiles(); |
179 | |
180 | /* The setup may be provided as static configuration in the |
181 | * domain context, as a setup file on disk or through a setup |
182 | * controller cell. |
183 | */ |
184 | executeSetup(); |
185 | |
186 | /* Now that everything is instantiated and configured, we can |
187 | * start the cell. |
188 | */ |
189 | start(); |
190 | |
191 | /* Run the final initialisation hooks. |
192 | */ |
193 | for (CellLifeCycleAware bean: _lifeCycleAware) { |
194 | bean.afterStart(); |
195 | } |
196 | } |
197 | |
198 | /** |
199 | * Closes the application context, which will shutdown all beans. |
200 | */ |
201 | public void cleanUp() |
202 | { |
203 | super.cleanUp(); |
204 | for (CellLifeCycleAware bean: _lifeCycleAware) { |
205 | bean.beforeStop(); |
206 | } |
207 | if (_context != null) { |
208 | _context.close(); |
209 | _context = null; |
210 | } |
211 | _infoProviders.clear(); |
212 | _setupProviders.clear(); |
213 | _lifeCycleAware.clear(); |
214 | } |
215 | |
216 | private void setupCellExecutors(String callbackExecutor, String messageExecutor) |
217 | { |
218 | if (callbackExecutor != null) { |
219 | Object executor = getBean(callbackExecutor); |
220 | if (!(executor instanceof ExecutorService)) { |
221 | throw new IllegalStateException("No such bean: " + callbackExecutor); |
222 | } |
223 | getNucleus().setCallbackExecutor((ExecutorService) executor); |
224 | } |
225 | |
226 | if (messageExecutor != null) { |
227 | Object executor = getBean(messageExecutor); |
228 | if (!(executor instanceof ExecutorService)) { |
229 | throw new IllegalStateException("No such bean: " + messageExecutor); |
230 | } |
231 | getNucleus().setMessageExecutor((ExecutorService) executor); |
232 | } |
233 | } |
234 | |
235 | private File firstMissing(File[] files) |
236 | { |
237 | for (File file: files) { |
238 | if (!file.exists()) { |
239 | return file; |
240 | } |
241 | } |
242 | return null; |
243 | } |
244 | |
245 | private void waitForFiles() |
246 | throws InterruptedException |
247 | { |
248 | String s = getArgs().getOpt("waitForFiles"); |
249 | if (s != null) { |
250 | String[] paths = s.split(":"); |
251 | File[] files = new File[paths.length]; |
252 | for (int i = 0; i < paths.length; i++) { |
253 | files[i] = new File(paths[i]); |
254 | } |
255 | |
256 | File missing; |
257 | while ((missing = firstMissing(files)) != null) { |
258 | warn(String.format("File missing: %s; sleeping %d seconds", missing, WAIT_FOR_FILE_SLEEP / 1000)); |
259 | Thread.sleep(WAIT_FOR_FILE_SLEEP); |
260 | } |
261 | } |
262 | } |
263 | |
264 | private void executeSetup() |
265 | throws IOException, CommandException |
266 | { |
267 | executeDefinedSetup(); |
268 | |
269 | if( _setupFile != null && _setupFile.isFile() ) { |
270 | for (CellSetupProvider provider: _setupProviders) { |
271 | provider.beforeSetup(); |
272 | } |
273 | |
274 | execFile(_setupFile); |
275 | |
276 | for (CellSetupProvider provider: _setupProviders) { |
277 | provider.afterSetup(); |
278 | } |
279 | } |
280 | } |
281 | |
282 | @Override |
283 | public void messageToForward(CellMessage envelope) |
284 | { |
285 | super.messageToForward(envelope); |
286 | } |
287 | |
288 | @Override |
289 | public void messageArrived(CellMessage envelope) |
290 | { |
291 | super.messageArrived(envelope); |
292 | } |
293 | |
294 | /** |
295 | * Returns the singleton bean with a given name. Returns null if |
296 | * such a bean does not exist. |
297 | */ |
298 | private Object getBean(String name) |
299 | { |
300 | try { |
301 | if (_context != null && _context.isSingleton(name)) { |
302 | return _context.getBean(name); |
303 | } |
304 | } catch (NoSuchBeanDefinitionException e) { |
305 | } |
306 | return null; |
307 | } |
308 | |
309 | /** |
310 | * Returns the names of beans that depend of a given bean. |
311 | */ |
312 | private List<String> getDependentBeans(String name) |
313 | { |
314 | if (_context == null) { |
315 | return Collections.emptyList(); |
316 | } else { |
317 | return Arrays.asList(_context.getBeanFactory().getDependentBeans(name)); |
318 | } |
319 | } |
320 | |
321 | /** |
322 | * Returns the collection of singleton bean names. |
323 | */ |
324 | private List<String> getBeanNames() |
325 | { |
326 | if (_context == null) { |
327 | return Collections.emptyList(); |
328 | } else { |
329 | return Arrays.asList(_context.getBeanFactory().getSingletonNames()); |
330 | } |
331 | } |
332 | |
333 | /** |
334 | * Collects information from all registered info providers. |
335 | */ |
336 | public void getInfo(PrintWriter pw) |
337 | { |
338 | ConfigurableListableBeanFactory factory = _context.getBeanFactory(); |
339 | for (Map.Entry<CellInfoProvider,String> entry: _infoProviders.entrySet()) { |
340 | CellInfoProvider provider = entry.getKey(); |
341 | String name = entry.getValue(); |
342 | try { |
343 | BeanDefinition definition = factory.getBeanDefinition(name); |
344 | String description = definition.getDescription(); |
345 | if (description != null) { |
346 | pw.println(String.format("--- %s (%s) ---", |
347 | name, description)); |
348 | } else { |
349 | pw.println(String.format("--- %s ---", name)); |
350 | } |
351 | provider.getInfo(pw); |
352 | pw.println(); |
353 | } catch (NoSuchBeanDefinitionException e) { |
354 | error("Failed to query bean definition for " + name); |
355 | } |
356 | } |
357 | } |
358 | |
359 | /** |
360 | * Collects information about the cell and returns these in a |
361 | * CellInfo object. Information is collected from the CellAdapter |
362 | * base class and from all beans implementing CellInfoProvider. |
363 | */ |
364 | public CellInfo getCellInfo() |
365 | { |
366 | CellInfo info = super.getCellInfo(); |
367 | for (CellInfoProvider provider : _infoProviders.keySet()) { |
368 | info = provider.getCellInfo(info); |
369 | } |
370 | return info; |
371 | } |
372 | |
373 | /** |
374 | * Collects setup information from all registered setup providers. |
375 | */ |
376 | protected void printSetup(PrintWriter pw) |
377 | { |
378 | pw.println("#\n# Created by " + getCellName() + "(" |
379 | + getNucleus().getCellClass() + ") at " + (new Date()).toString() |
380 | + "\n#"); |
381 | for (CellSetupProvider provider: _setupProviders) |
382 | provider.printSetup(pw); |
383 | } |
384 | |
385 | public final String hh_save = "[-sc=<setupController>|none] [-file=<filename>] # saves setup to disk or setup controller"; |
386 | public String ac_save(Args args) |
387 | throws IOException, IllegalArgumentException, NoRouteToCellException |
388 | { |
389 | String controller = args.getOpt("sc"); |
390 | String file = args.getOpt("file"); |
391 | |
392 | if ("none".equals(controller)) { |
393 | controller = null; |
394 | file = _setupFile.getPath(); |
395 | } else if (file == null && controller == null) { |
396 | controller = _setupController; |
397 | file = _setupFile.getPath(); |
398 | } |
399 | |
400 | if (file == null && controller == null) { |
401 | throw new IllegalArgumentException("Either a setup controller or setup file must be specified"); |
402 | } |
403 | |
404 | if (controller != null) { |
405 | if (_setupClass == null || _setupClass.equals("")) |
406 | throw new IllegalStateException("Cannot save to a setup controller since the cell has no setup class"); |
407 | |
408 | try { |
409 | StringWriter sw = new StringWriter(); |
410 | printSetup(new PrintWriter(sw)); |
411 | |
412 | SetupInfoMessage info = |
413 | new SetupInfoMessage("put", getCellName(), |
414 | _setupClass, sw.toString()); |
415 | |
416 | sendMessage(new CellMessage(new CellPath(controller), info)); |
417 | } catch (NoRouteToCellException e) { |
418 | throw new NoRouteToCellException("Failed to send setup to " + controller + ": " + e.getMessage()); |
419 | } |
420 | } |
421 | |
422 | if (file != null) { |
423 | File path = new File(file).getAbsoluteFile(); |
424 | File directory = path.getParentFile(); |
425 | File temp = File.createTempFile(path.getName(), null, directory); |
426 | temp.deleteOnExit(); |
427 | |
428 | PrintWriter pw = new PrintWriter(new FileWriter(temp)); |
429 | try { |
430 | printSetup(pw); |
431 | } finally { |
432 | pw.close(); |
433 | } |
434 | |
435 | renameWithBackup(temp, path); |
436 | } |
437 | |
438 | return ""; |
439 | } |
440 | |
441 | private static void renameWithBackup(File source, File dest) |
442 | throws IOException |
443 | { |
444 | File backup = new File(dest.getPath() + ".bak"); |
445 | |
446 | if (dest.exists()) { |
447 | if (!dest.isFile()) { |
448 | throw new IOException("Cannot rename " + dest + ": Not a file"); |
449 | } |
450 | if (backup.exists()) { |
451 | if (!backup.isFile()) { |
452 | throw new IOException("Cannot delete " + backup + ": Not a file"); |
453 | } |
454 | if (!backup.delete()) { |
455 | throw new IOException("Failed to delete " + backup); |
456 | } |
457 | } |
458 | if (!dest.renameTo(backup)) { |
459 | throw new IOException("Failed to rename " + dest); |
460 | } |
461 | } |
462 | if (!source.renameTo(dest)) { |
463 | throw new IOException("Failed to rename" + source); |
464 | } |
465 | } |
466 | |
467 | private void execFile(File setup) |
468 | throws IOException, CommandException |
469 | { |
470 | BufferedReader br = new BufferedReader(new FileReader(setup)); |
471 | String line; |
472 | try { |
473 | int lineCount = 0; |
474 | while ((line = br.readLine()) != null) { |
475 | ++lineCount; |
476 | |
477 | line = line.trim(); |
478 | if (line.length() == 0) |
479 | continue; |
480 | if (line.charAt(0) == '#') |
481 | continue; |
482 | try { |
483 | command(new Args(line)); |
484 | } catch (CommandException e) { |
485 | throw new CommandException("Error at line " + lineCount |
486 | + ": " + e.getMessage()); |
487 | } |
488 | } |
489 | } finally { |
490 | try { |
491 | br.close(); |
492 | } catch (IOException e) { |
493 | // ignored |
494 | } |
495 | } |
496 | } |
497 | |
498 | public String hh_reload = "-yes"; |
499 | public String fh_reload = |
500 | "This command destroys the current setup and replaces it" + |
501 | "by the setup on disk."; |
502 | public String ac_reload(Args args) |
503 | throws IOException, CommandException |
504 | { |
505 | if (args.getOpt("yes") == null) { |
506 | return |
507 | " This command destroys the current setup\n" + |
508 | " and replaces it by the setup on disk\n" + |
509 | " Please use 'reload -yes' if you really want\n" + |
510 | " to do that."; |
511 | } |
512 | |
513 | if (_setupFile != null && !_setupFile.exists()) { |
514 | return String.format("Setup file [%s] does not exist", _setupFile); |
515 | } |
516 | |
517 | executeSetup(); |
518 | |
519 | return ""; |
520 | } |
521 | |
522 | /** |
523 | * Should be renamed to 'info' when command interpreter learns |
524 | * to handle overloaded commands. |
525 | */ |
526 | public static final String hh_infox = "<bean>"; |
527 | public String ac_infox_$_1(Args args) |
528 | { |
529 | String name = args.argv(0); |
530 | Object bean = getBean(name); |
531 | if (CellInfoProvider.class.isInstance(bean)) { |
532 | StringWriter s = new StringWriter(); |
533 | PrintWriter pw = new PrintWriter(s); |
534 | ((CellInfoProvider)bean).getInfo(pw); |
535 | return s.toString(); |
536 | } |
537 | return "No such bean: " + name; |
538 | } |
539 | |
540 | public static final String hh_bean_ls = "# lists running beans"; |
541 | public String ac_bean_ls(Args args) |
542 | { |
543 | final String format = "%-30s %s\n"; |
544 | Formatter s = new Formatter(new StringBuilder()); |
545 | ConfigurableListableBeanFactory factory = _context.getBeanFactory(); |
546 | |
547 | s.format(format, "Bean", "Description"); |
548 | s.format(format, "----", "-----------"); |
549 | for (String name : getBeanNames()) { |
550 | try { |
551 | BeanDefinition definition = factory.getBeanDefinition(name); |
552 | String description = definition.getDescription(); |
553 | s.format(format, name, |
554 | (description != null ? description : "-")); |
555 | } catch (NoSuchBeanDefinitionException e) { |
556 | error("Failed to query bean definition for " + name); |
557 | } |
558 | } |
559 | return s.toString(); |
560 | } |
561 | |
562 | public static final String hh_bean_dep = "# shows bean dependencies"; |
563 | public String ac_bean_dep(Args args) |
564 | { |
565 | final String format = "%-30s %s\n"; |
566 | Formatter s = new Formatter(new StringBuilder()); |
567 | ConfigurableListableBeanFactory factory = _context.getBeanFactory(); |
568 | |
569 | s.format(format, "Bean", "Used by"); |
570 | s.format(format, "----", "-------"); |
571 | for (String name : getBeanNames()) { |
572 | s.format(format, name, collectionToString(getDependentBeans(name))); |
573 | } |
574 | return s.toString(); |
575 | } |
576 | |
577 | /** |
578 | * If given a simple bean name, returns that bean (equivalent to |
579 | * calling getBean). If given a compound name using the syntax of |
580 | * BeanWrapper, then the respective property value is |
581 | * returned. E.g., getBeanProperty("foo") returns the bean named |
582 | * "foo", whereas getBeanProperty("foo.bar") returns the value of |
583 | * the "bar" property of bean "foo". |
584 | */ |
585 | private Object getBeanProperty(String s) |
586 | { |
587 | String[] a = s.split("\\.", 2); |
588 | Object o = getBean(a[0]); |
589 | if (o != null && a.length == 2) { |
590 | BeanWrapper bean = new BeanWrapperImpl(o); |
591 | o = bean.isReadableProperty(a[1]) |
592 | ? bean.getPropertyValue(a[1]) |
593 | : null; |
594 | } |
595 | return o; |
596 | } |
597 | |
598 | public static final String hh_bean_properties = |
599 | "<bean> # shows properties of a bean"; |
600 | public String ac_bean_properties_$_1(Args args) |
601 | { |
602 | String name = args.argv(0); |
603 | Object o = getBeanProperty(name); |
604 | if (o != null) { |
605 | StringBuilder s = new StringBuilder(); |
606 | BeanWrapper bean = new BeanWrapperImpl(o); |
607 | for (PropertyDescriptor p : bean.getPropertyDescriptors()) { |
608 | if (!p.isHidden()) { |
609 | String property = p.getName(); |
610 | if (bean.isReadableProperty(property)) { |
611 | Object value = bean.getPropertyValue(property); |
612 | s.append(property).append('=').append(value); |
613 | if (!bean.isWritableProperty(property)) { |
614 | s.append(" [read-only]"); |
615 | } |
616 | s.append('\n'); |
617 | } |
618 | } |
619 | } |
620 | return s.toString(); |
621 | } |
622 | return "No such bean: " + name; |
623 | } |
624 | |
625 | public static final String hh_bean_property = |
626 | "<property-name> # shows property of a bean"; |
627 | public String ac_bean_property_$_1(Args args) |
628 | { |
629 | String name = args.argv(0); |
630 | Object o = getBeanProperty(name); |
631 | return (o != null) ? o.toString() : "No such bean: " + name; |
632 | } |
633 | |
634 | /** Returns a formated name of a message class. */ |
635 | protected String getMessageName(Class c) |
636 | { |
637 | String name = c.getSimpleName(); |
638 | int length = name.length(); |
639 | if ((length > 7) && name.endsWith("Message")) { |
640 | name = name.substring(0, name.length() - 7); |
641 | } else if ((length > 3) && name.endsWith("Msg")) { |
642 | name = name.substring(0, name.length() - 3); |
643 | } |
644 | |
645 | return name; |
646 | } |
647 | |
648 | public static final String hh_bean_messages = |
649 | "[<bean>] # shows message types handled by beans"; |
650 | public String ac_bean_messages_$_0_1(Args args) |
651 | { |
652 | switch (args.argc()) { |
653 | case 0: |
654 | Map<String,Collection<Class>> map = new HashMap(); |
655 | for (String name: getBeanNames()) { |
656 | Object bean = getBean(name); |
657 | if (CellMessageReceiver.class.isInstance(bean)) { |
658 | Collection<Class> types = |
659 | _messageDispatcher.getMessageTypes(bean); |
660 | map.put(name, types); |
661 | } |
662 | } |
663 | |
664 | final String format = "%-40s %s\n"; |
665 | Formatter f = new Formatter(new StringBuilder()); |
666 | f.format(format, "Message", "Receivers"); |
667 | f.format(format, "-------", "---------"); |
668 | for (Map.Entry<Class,Collection<String>> e: invert(map).entrySet()) { |
669 | f.format(format, |
670 | getMessageName(e.getKey()), |
671 | collectionToString(e.getValue())); |
672 | } |
673 | |
674 | return f.toString(); |
675 | |
676 | case 1: |
677 | String name = args.argv(0); |
678 | Object bean = getBean(name); |
679 | if (CellMessageReceiver.class.isInstance(bean)) { |
680 | StringBuilder s = new StringBuilder(); |
681 | Collection<Class> types = |
682 | _messageDispatcher.getMessageTypes(bean); |
683 | for (Class t : types) { |
684 | s.append(getMessageName(t)).append('\n'); |
685 | } |
686 | return s.toString(); |
687 | } |
688 | return "No such bean: " + name; |
689 | |
690 | default: |
691 | return ""; |
692 | } |
693 | } |
694 | |
695 | /** |
696 | * Registers an info provider. Info providers contribute to the |
697 | * result of the <code>getInfo</code> method. |
698 | */ |
699 | public void addInfoProviderBean(CellInfoProvider bean, String name) |
700 | { |
701 | _infoProviders.put(bean, name); |
702 | } |
703 | |
704 | /** |
705 | * Add a message receiver. Message receiver receive messages via |
706 | * message handlers (see CellMessageDispatcher). |
707 | */ |
708 | public void addMessageReceiver(CellMessageReceiver bean) |
709 | { |
710 | addMessageListener(bean); |
711 | } |
712 | |
713 | /** |
714 | * Add a message sender. Message senders can send cell messages |
715 | * via a cell endpoint. |
716 | */ |
717 | public void addMessageSender(CellMessageSender bean) |
718 | { |
719 | bean.setCellEndpoint(this); |
720 | } |
721 | |
722 | /** |
723 | * Registers a setup provider. Setup providers contribute to the |
724 | * result of the <code>save</code> method. |
725 | */ |
726 | public void addSetupProviderBean(CellSetupProvider bean) |
727 | { |
728 | _setupProviders.add(bean); |
729 | } |
730 | |
731 | /** |
732 | * Registers a life cycle aware bean. Life cycle aware beans are |
733 | * notified about cell start and stop events. |
734 | */ |
735 | public void addLifeCycleAwareBean(CellLifeCycleAware bean) |
736 | { |
737 | _lifeCycleAware.add(bean); |
738 | } |
739 | |
740 | /** |
741 | * Registers a thread factory aware bean. Thread factory aware |
742 | * bean provide hooks for registering thread factories. This |
743 | * method registers the Cell nulceus thread factory on the bean. |
744 | */ |
745 | public void addThreadFactoryAwareBean(ThreadFactoryAware bean) |
746 | { |
747 | bean.setThreadFactory(getNucleus()); |
748 | } |
749 | |
750 | |
751 | /** |
752 | * Part of the BeanPostProcessor implementation. Recognizes beans |
753 | * implementing CellCommandListener, CellInfoProvider, |
754 | * CellCommunicationAware, CellSetupProvider and |
755 | * ThreadFactoryAware and performs the necessary wiring. |
756 | */ |
757 | public Object postProcessBeforeInitialization(Object bean, |
758 | String beanName) |
759 | throws BeansException |
760 | { |
761 | if (CellCommandListener.class.isInstance(bean)) { |
762 | addCommandListener(bean); |
763 | } |
764 | |
765 | if (CellInfoProvider.class.isInstance(bean)) { |
766 | addInfoProviderBean((CellInfoProvider)bean, beanName); |
767 | } |
768 | |
769 | if (CellMessageReceiver.class.isInstance(bean)) { |
770 | addMessageReceiver((CellMessageReceiver)bean); |
771 | } |
772 | |
773 | if (CellMessageSender.class.isInstance(bean)) { |
774 | addMessageSender((CellMessageSender)bean); |
775 | } |
776 | |
777 | if (CellSetupProvider.class.isInstance(bean)) { |
778 | addSetupProviderBean((CellSetupProvider)bean); |
779 | } |
780 | |
781 | if (CellLifeCycleAware.class.isInstance(bean)) { |
782 | addLifeCycleAwareBean((CellLifeCycleAware)bean); |
783 | } |
784 | |
785 | if (ThreadFactoryAware.class.isInstance(bean)) { |
786 | addThreadFactoryAwareBean((ThreadFactoryAware)bean); |
787 | } |
788 | return bean; |
789 | } |
790 | |
791 | public Object postProcessAfterInitialization(Object bean, |
792 | String beanName) |
793 | throws BeansException |
794 | { |
795 | return bean; |
796 | } |
797 | |
798 | class UniversalSpringCellApplicationContext |
799 | extends ClassPathXmlApplicationContext |
800 | { |
801 | UniversalSpringCellApplicationContext(Args args) |
802 | { |
803 | super(args.argv(0)); |
804 | } |
805 | |
806 | private ByteArrayResource getDomainContextResource() |
807 | { |
808 | Args args = (Args)getArgs().clone(); |
809 | args.shift(); |
810 | |
811 | Properties properties = new Properties(); |
812 | String arguments = |
813 | args.toString().replaceAll("-\\$\\{[0-9]+\\}", ""); |
814 | properties.setProperty("arguments", arguments); |
815 | properties.setProperty("thisCell", getCellName()); |
816 | properties.setProperty("thisDomain", getCellDomainName()); |
817 | mergeProperties(properties, getDomainContext()); |
818 | mergeProperties(properties, args.options()); |
819 | |
820 | /* Convert to byte array form such that we can make it |
821 | * available as a Spring resource. |
822 | */ |
823 | ByteArrayOutputStream out = new ByteArrayOutputStream(); |
824 | try { |
825 | properties.store(out, ""); |
826 | } catch (IOException e) { |
827 | /* This should never happen with a ByteArrayOutputStream. |
828 | */ |
829 | throw new RuntimeException("Unexpected exception", e); |
830 | } |
831 | final byte[] _domainContext = out.toByteArray(); |
832 | |
833 | return new ByteArrayResource(_domainContext) { |
834 | /** |
835 | * Fake file name to make |
836 | * PropertyPlaceholderConfigurer happy. |
837 | */ |
838 | public String getFilename() |
839 | { |
840 | return "domaincontext.properties"; |
841 | } |
842 | }; |
843 | } |
844 | |
845 | public Resource getResource(String location) |
846 | { |
847 | if (location.startsWith("domaincontext:")) { |
848 | return getDomainContextResource(); |
849 | } else { |
850 | return super.getResource(location); |
851 | } |
852 | } |
853 | |
854 | protected void customizeBeanFactory(DefaultListableBeanFactory beanFactory) |
855 | { |
856 | beanFactory.addBeanPostProcessor(UniversalSpringCell.this); |
857 | } |
858 | } |
859 | |
860 | /** |
861 | * Utility method for converting a collection of objects to a |
862 | * string. The string is formed by concatenating the string form |
863 | * of the objects, separated by a comma. |
864 | */ |
865 | private <T> String collectionToString(Collection<T> collection) |
866 | { |
867 | StringBuilder s = new StringBuilder(); |
868 | for (T o: collection) { |
869 | if (s.length() > 0) { |
870 | s.append(','); |
871 | } |
872 | s.append(o); |
873 | } |
874 | return s.toString(); |
875 | } |
876 | |
877 | /** |
878 | * Merges a map into a property set. |
879 | */ |
880 | private void mergeProperties(Properties properties, Map<?,?> map) |
881 | { |
882 | for (Map.Entry<?,?> e: map.entrySet()) { |
883 | Object key = e.getKey(); |
884 | Object value = e.getValue(); |
885 | properties.setProperty(key.toString(), value.toString()); |
886 | } |
887 | } |
888 | |
889 | /** |
890 | * Utility method for inverting a map. |
891 | * |
892 | * Given a map { "a" => { 1, 2, 3}, "b" => {2, 3, 4}, c => {3, 4, |
893 | * 5} }, this method returns a new map { 1 => { "a" }, 2 => { "a", |
894 | * "b" }, 3 => { "a", "b", "c" }, 4 => { "b", "c" }, 5 => { "c" |
895 | * }}. |
896 | * |
897 | * TODO: Should be moved to a utility library. |
898 | */ |
899 | private <T1,T2> Map<T1,Collection<T2>> invert(Map<T2,Collection<T1>> map) |
900 | { |
901 | Map<T1,Collection<T2>> result = new HashMap(); |
902 | for (Map.Entry<T2,Collection<T1>> e : map.entrySet()) { |
903 | for (T1 value : e.getValue()) { |
904 | Collection<T2> collection = result.get(value); |
905 | if (collection == null) { |
906 | collection = new ArrayList<T2>(); |
907 | result.put(value, collection); |
908 | } |
909 | collection.add(e.getKey()); |
910 | } |
911 | } |
912 | return result; |
913 | } |
914 | } |