001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019package org.apache.hadoop.lib.server;
020
021import org.apache.hadoop.conf.Configuration;
022import org.apache.hadoop.lib.util.Check;
023import org.apache.hadoop.lib.util.ConfigurationUtils;
024import org.apache.log4j.LogManager;
025import org.apache.log4j.PropertyConfigurator;
026import org.slf4j.Logger;
027import org.slf4j.LoggerFactory;
028
029import java.io.File;
030import java.io.FileInputStream;
031import java.io.IOException;
032import java.io.InputStream;
033import java.text.MessageFormat;
034import java.util.ArrayList;
035import java.util.Collections;
036import java.util.LinkedHashMap;
037import java.util.List;
038import java.util.Map;
039import java.util.Properties;
040
041/**
042 * A Server class provides standard configuration, logging and {@link Service}
043 * lifecyle management.
044 * <p/>
045 * A Server normally has a home directory, a configuration directory, a temp
046 * directory and logs directory.
047 * <p/>
048 * The Server configuration is loaded from 2 overlapped files,
049 * <code>#SERVER#-default.xml</code> and <code>#SERVER#-site.xml</code>. The
050 * default file is loaded from the classpath, the site file is laoded from the
051 * configuration directory.
052 * <p/>
053 * The Server collects all configuration properties prefixed with
054 * <code>#SERVER#</code>. The property names are then trimmed from the
055 * <code>#SERVER#</code> prefix.
056 * <p/>
057 * The Server log configuration is loaded from the
058 * <code>#SERVICE#-log4j.properties</code> file in the configuration directory.
059 * <p/>
060 * The lifecycle of server is defined in by {@link Server.Status} enum.
061 * When a server is create, its status is UNDEF, when being initialized it is
062 * BOOTING, once initialization is complete by default transitions to NORMAL.
063 * The <code>#SERVER#.startup.status</code> configuration property can be used
064 * to specify a different startup status (NORMAL, ADMIN or HALTED).
065 * <p/>
066 * Services classes are defined in the <code>#SERVER#.services</code> and
067 * <code>#SERVER#.services.ext</code> properties. They are loaded in order
068 * (services first, then services.ext).
069 * <p/>
070 * Before initializing the services, they are traversed and duplicate service
071 * interface are removed from the service list. The last service using a given
072 * interface wins (this enables a simple override mechanism).
073 * <p/>
074 * After the services have been resoloved by interface de-duplication they are
075 * initialized in order. Once all services are initialized they are
076 * post-initialized (this enables late/conditional service bindings).
077 * <p/>
078 */
079public class Server {
080  private Logger log;
081
082  /**
083   * Server property name that defines the service classes.
084   */
085  public static final String CONF_SERVICES = "services";
086
087  /**
088   * Server property name that defines the service extension classes.
089   */
090  public static final String CONF_SERVICES_EXT = "services.ext";
091
092  /**
093   * Server property name that defines server startup status.
094   */
095  public static final String CONF_STARTUP_STATUS = "startup.status";
096
097  /**
098   * Enumeration that defines the server status.
099   */
100  public enum Status {
101    UNDEF(false, false),
102    BOOTING(false, true),
103    HALTED(true, true),
104    ADMIN(true, true),
105    NORMAL(true, true),
106    SHUTTING_DOWN(false, true),
107    SHUTDOWN(false, false);
108
109    private boolean settable;
110    private boolean operational;
111
112    /**
113     * Status constructor.
114     *
115     * @param settable indicates if the status is settable.
116     * @param operational indicates if the server is operational
117     * when in this status.
118     */
119    private Status(boolean settable, boolean operational) {
120      this.settable = settable;
121      this.operational = operational;
122    }
123
124    /**
125     * Returns if this server status is operational.
126     *
127     * @return if this server status is operational.
128     */
129    public boolean isOperational() {
130      return operational;
131    }
132  }
133
134  /**
135   * Name of the log4j configuration file the Server will load from the
136   * classpath if the <code>#SERVER#-log4j.properties</code> is not defined
137   * in the server configuration directory.
138   */
139  public static final String DEFAULT_LOG4J_PROPERTIES = "default-log4j.properties";
140
141  private Status status;
142  private String name;
143  private String homeDir;
144  private String configDir;
145  private String logDir;
146  private String tempDir;
147  private Configuration config;
148  private Map<Class, Service> services = new LinkedHashMap<Class, Service>();
149
150  /**
151   * Creates a server instance.
152   * <p/>
153   * The config, log and temp directories are all under the specified home directory.
154   *
155   * @param name server name.
156   * @param homeDir server home directory.
157   */
158  public Server(String name, String homeDir) {
159    this(name, homeDir, null);
160  }
161
162  /**
163   * Creates a server instance.
164   *
165   * @param name server name.
166   * @param homeDir server home directory.
167   * @param configDir config directory.
168   * @param logDir log directory.
169   * @param tempDir temp directory.
170   */
171  public Server(String name, String homeDir, String configDir, String logDir, String tempDir) {
172    this(name, homeDir, configDir, logDir, tempDir, null);
173  }
174
175  /**
176   * Creates a server instance.
177   * <p/>
178   * The config, log and temp directories are all under the specified home directory.
179   * <p/>
180   * It uses the provided configuration instead loading it from the config dir.
181   *
182   * @param name server name.
183   * @param homeDir server home directory.
184   * @param config server configuration.
185   */
186  public Server(String name, String homeDir, Configuration config) {
187    this(name, homeDir, homeDir + "/conf", homeDir + "/log", homeDir + "/temp", config);
188  }
189
190  /**
191   * Creates a server instance.
192   * <p/>
193   * It uses the provided configuration instead loading it from the config dir.
194   *
195   * @param name server name.
196   * @param homeDir server home directory.
197   * @param configDir config directory.
198   * @param logDir log directory.
199   * @param tempDir temp directory.
200   * @param config server configuration.
201   */
202  public Server(String name, String homeDir, String configDir, String logDir, String tempDir, Configuration config) {
203    this.name = Check.notEmpty(name, "name").trim().toLowerCase();
204    this.homeDir = Check.notEmpty(homeDir, "homeDir");
205    this.configDir = Check.notEmpty(configDir, "configDir");
206    this.logDir = Check.notEmpty(logDir, "logDir");
207    this.tempDir = Check.notEmpty(tempDir, "tempDir");
208    checkAbsolutePath(homeDir, "homeDir");
209    checkAbsolutePath(configDir, "configDir");
210    checkAbsolutePath(logDir, "logDir");
211    checkAbsolutePath(tempDir, "tempDir");
212    if (config != null) {
213      this.config = new Configuration(false);
214      ConfigurationUtils.copy(config, this.config);
215    }
216    status = Status.UNDEF;
217  }
218
219  /**
220   * Validates that the specified value is an absolute path (starts with '/').
221   *
222   * @param value value to verify it is an absolute path.
223   * @param name name to use in the exception if the value is not an absolute
224   * path.
225   *
226   * @return the value.
227   *
228   * @throws IllegalArgumentException thrown if the value is not an absolute
229   * path.
230   */
231  private String checkAbsolutePath(String value, String name) {
232    if (!value.startsWith("/")) {
233      throw new IllegalArgumentException(
234        MessageFormat.format("[{0}] must be an absolute path [{1}]", name, value));
235    }
236    return value;
237  }
238
239  /**
240   * Returns the current server status.
241   *
242   * @return the current server status.
243   */
244  public Status getStatus() {
245    return status;
246  }
247
248  /**
249   * Sets a new server status.
250   * <p/>
251   * The status must be settable.
252   * <p/>
253   * All services will be notified o the status change via the
254   * {@link Service#serverStatusChange(Server.Status, Server.Status)} method. If a service
255   * throws an exception during the notification, the server will be destroyed.
256   *
257   * @param status status to set.
258   *
259   * @throws ServerException thrown if the service has been destroy because of
260   * a failed notification to a service.
261   */
262  public void setStatus(Status status) throws ServerException {
263    Check.notNull(status, "status");
264    if (status.settable) {
265      if (status != this.status) {
266        Status oldStatus = this.status;
267        this.status = status;
268        for (Service service : services.values()) {
269          try {
270            service.serverStatusChange(oldStatus, status);
271          } catch (Exception ex) {
272            log.error("Service [{}] exception during status change to [{}] -server shutting down-,  {}",
273                      new Object[]{service.getInterface().getSimpleName(), status, ex.getMessage(), ex});
274            destroy();
275            throw new ServerException(ServerException.ERROR.S11, service.getInterface().getSimpleName(),
276                                      status, ex.getMessage(), ex);
277          }
278        }
279      }
280    } else {
281      throw new IllegalArgumentException("Status [" + status + " is not settable");
282    }
283  }
284
285  /**
286   * Verifies the server is operational.
287   *
288   * @throws IllegalStateException thrown if the server is not operational.
289   */
290  protected void ensureOperational() {
291    if (!getStatus().isOperational()) {
292      throw new IllegalStateException("Server is not running");
293    }
294  }
295
296  /**
297   * Convenience method that returns a resource as inputstream from the
298   * classpath.
299   * <p/>
300   * It first attempts to use the Thread's context classloader and if not
301   * set it uses the <code>ClassUtils</code> classloader.
302   *
303   * @param name resource to retrieve.
304   *
305   * @return inputstream with the resource, NULL if the resource does not
306   *         exist.
307   */
308  static InputStream getResource(String name) {
309    Check.notEmpty(name, "name");
310    ClassLoader cl = Thread.currentThread().getContextClassLoader();
311    if (cl == null) {
312      cl = Server.class.getClassLoader();
313    }
314    return cl.getResourceAsStream(name);
315  }
316
317  /**
318   * Initializes the Server.
319   * <p/>
320   * The initialization steps are:
321   * <ul>
322   * <li>It verifies the service home and temp directories exist</li>
323   * <li>Loads the Server <code>#SERVER#-default.xml</code>
324   * configuration file from the classpath</li>
325   * <li>Initializes log4j logging. If the
326   * <code>#SERVER#-log4j.properties</code> file does not exist in the config
327   * directory it load <code>default-log4j.properties</code> from the classpath
328   * </li>
329   * <li>Loads the <code>#SERVER#-site.xml</code> file from the server config
330   * directory and merges it with the default configuration.</li>
331   * <li>Loads the services</li>
332   * <li>Initializes the services</li>
333   * <li>Post-initializes the services</li>
334   * <li>Sets the server startup status</li>
335   *
336   * @throws ServerException thrown if the server could not be initialized.
337   */
338  public void init() throws ServerException {
339    if (status != Status.UNDEF) {
340      throw new IllegalStateException("Server already initialized");
341    }
342    status = Status.BOOTING;
343    verifyDir(homeDir);
344    verifyDir(tempDir);
345    Properties serverInfo = new Properties();
346    try {
347      InputStream is = getResource(name + ".properties");
348      serverInfo.load(is);
349      is.close();
350    } catch (IOException ex) {
351      throw new RuntimeException("Could not load server information file: " + name + ".properties");
352    }
353    initLog();
354    log.info("++++++++++++++++++++++++++++++++++++++++++++++++++++++");
355    log.info("Server [{}] starting", name);
356    log.info("  Built information:");
357    log.info("    Version           : {}", serverInfo.getProperty(name + ".version", "undef"));
358    log.info("    Source Repository : {}", serverInfo.getProperty(name + ".source.repository", "undef"));
359    log.info("    Source Revision   : {}", serverInfo.getProperty(name + ".source.revision", "undef"));
360    log.info("    Built by          : {}", serverInfo.getProperty(name + ".build.username", "undef"));
361    log.info("    Built timestamp   : {}", serverInfo.getProperty(name + ".build.timestamp", "undef"));
362    log.info("  Runtime information:");
363    log.info("    Home   dir: {}", homeDir);
364    log.info("    Config dir: {}", (config == null) ? configDir : "-");
365    log.info("    Log    dir: {}", logDir);
366    log.info("    Temp   dir: {}", tempDir);
367    initConfig();
368    log.debug("Loading services");
369    List<Service> list = loadServices();
370    try {
371      log.debug("Initializing services");
372      initServices(list);
373      log.info("Services initialized");
374    } catch (ServerException ex) {
375      log.error("Services initialization failure, destroying initialized services");
376      destroyServices();
377      throw ex;
378    }
379    Status status = Status.valueOf(getConfig().get(getPrefixedName(CONF_STARTUP_STATUS), Status.NORMAL.toString()));
380    setStatus(status);
381    log.info("Server [{}] started!, status [{}]", name, status);
382  }
383
384  /**
385   * Verifies the specified directory exists.
386   *
387   * @param dir directory to verify it exists.
388   *
389   * @throws ServerException thrown if the directory does not exist or it the
390   * path it is not a directory.
391   */
392  private void verifyDir(String dir) throws ServerException {
393    File file = new File(dir);
394    if (!file.exists()) {
395      throw new ServerException(ServerException.ERROR.S01, dir);
396    }
397    if (!file.isDirectory()) {
398      throw new ServerException(ServerException.ERROR.S02, dir);
399    }
400  }
401
402  /**
403   * Initializes Log4j logging.
404   *
405   * @throws ServerException thrown if Log4j could not be initialized.
406   */
407  protected void initLog() throws ServerException {
408    verifyDir(logDir);
409    LogManager.resetConfiguration();
410    File log4jFile = new File(configDir, name + "-log4j.properties");
411    if (log4jFile.exists()) {
412      PropertyConfigurator.configureAndWatch(log4jFile.toString(), 10 * 1000); //every 10 secs
413      log = LoggerFactory.getLogger(Server.class);
414    } else {
415      Properties props = new Properties();
416      try {
417        InputStream is = getResource(DEFAULT_LOG4J_PROPERTIES);
418        props.load(is);
419      } catch (IOException ex) {
420        throw new ServerException(ServerException.ERROR.S03, DEFAULT_LOG4J_PROPERTIES, ex.getMessage(), ex);
421      }
422      PropertyConfigurator.configure(props);
423      log = LoggerFactory.getLogger(Server.class);
424      log.warn("Log4j [{}] configuration file not found, using default configuration from classpath", log4jFile);
425    }
426  }
427
428  /**
429   * Loads and inializes the server configuration.
430   *
431   * @throws ServerException thrown if the configuration could not be loaded/initialized.
432   */
433  protected void initConfig() throws ServerException {
434    verifyDir(configDir);
435    File file = new File(configDir);
436    Configuration defaultConf;
437    String defaultConfig = name + "-default.xml";
438    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
439    InputStream inputStream = classLoader.getResourceAsStream(defaultConfig);
440    if (inputStream == null) {
441      log.warn("Default configuration file not available in classpath [{}]", defaultConfig);
442      defaultConf = new Configuration(false);
443    } else {
444      try {
445        defaultConf = new Configuration(false);
446        ConfigurationUtils.load(defaultConf, inputStream);
447      } catch (Exception ex) {
448        throw new ServerException(ServerException.ERROR.S03, defaultConfig, ex.getMessage(), ex);
449      }
450    }
451
452    if (config == null) {
453      Configuration siteConf;
454      File siteFile = new File(file, name + "-site.xml");
455      if (!siteFile.exists()) {
456        log.warn("Site configuration file [{}] not found in config directory", siteFile);
457        siteConf = new Configuration(false);
458      } else {
459        if (!siteFile.isFile()) {
460          throw new ServerException(ServerException.ERROR.S05, siteFile.getAbsolutePath());
461        }
462        try {
463          log.debug("Loading site configuration from [{}]", siteFile);
464          inputStream = new FileInputStream(siteFile);
465          siteConf = new Configuration(false);
466          ConfigurationUtils.load(siteConf, inputStream);
467        } catch (IOException ex) {
468          throw new ServerException(ServerException.ERROR.S06, siteFile, ex.getMessage(), ex);
469        }
470      }
471
472      config = new Configuration(false);
473      ConfigurationUtils.copy(siteConf, config);
474    }
475
476    ConfigurationUtils.injectDefaults(defaultConf, config);
477
478    for (String name : System.getProperties().stringPropertyNames()) {
479      String value = System.getProperty(name);
480      if (name.startsWith(getPrefix() + ".")) {
481        config.set(name, value);
482        if (name.endsWith(".password") || name.endsWith(".secret")) {
483          value = "*MASKED*";
484        }
485        log.info("System property sets  {}: {}", name, value);
486      }
487    }
488
489    log.debug("Loaded Configuration:");
490    log.debug("------------------------------------------------------");
491    for (Map.Entry<String, String> entry : config) {
492      String name = entry.getKey();
493      String value = config.get(entry.getKey());
494      if (name.endsWith(".password") || name.endsWith(".secret")) {
495        value = "*MASKED*";
496      }
497      log.debug("  {}: {}", entry.getKey(), value);
498    }
499    log.debug("------------------------------------------------------");
500  }
501
502  /**
503   * Loads the specified services.
504   *
505   * @param classes services classes to load.
506   * @param list list of loaded service in order of appearance in the
507   * configuration.
508   *
509   * @throws ServerException thrown if a service class could not be loaded.
510   */
511  private void loadServices(Class[] classes, List<Service> list) throws ServerException {
512    for (Class klass : classes) {
513      try {
514        Service service = (Service) klass.newInstance();
515        log.debug("Loading service [{}] implementation [{}]", service.getInterface(),
516                  service.getClass());
517        if (!service.getInterface().isInstance(service)) {
518          throw new ServerException(ServerException.ERROR.S04, klass, service.getInterface().getName());
519        }
520        list.add(service);
521      } catch (ServerException ex) {
522        throw ex;
523      } catch (Exception ex) {
524        throw new ServerException(ServerException.ERROR.S07, klass, ex.getMessage(), ex);
525      }
526    }
527  }
528
529  /**
530   * Loads services defined in <code>services</code> and
531   * <code>services.ext</code> and de-dups them.
532   *
533   * @return List of final services to initialize.
534   *
535   * @throws ServerException throw if the services could not be loaded.
536   */
537  protected List<Service> loadServices() throws ServerException {
538    try {
539      Map<Class, Service> map = new LinkedHashMap<Class, Service>();
540      Class[] classes = getConfig().getClasses(getPrefixedName(CONF_SERVICES));
541      Class[] classesExt = getConfig().getClasses(getPrefixedName(CONF_SERVICES_EXT));
542      List<Service> list = new ArrayList<Service>();
543      loadServices(classes, list);
544      loadServices(classesExt, list);
545
546      //removing duplicate services, strategy: last one wins
547      for (Service service : list) {
548        if (map.containsKey(service.getInterface())) {
549          log.debug("Replacing service [{}] implementation [{}]", service.getInterface(),
550                    service.getClass());
551        }
552        map.put(service.getInterface(), service);
553      }
554      list = new ArrayList<Service>();
555      for (Map.Entry<Class, Service> entry : map.entrySet()) {
556        list.add(entry.getValue());
557      }
558      return list;
559    } catch (RuntimeException ex) {
560      throw new ServerException(ServerException.ERROR.S08, ex.getMessage(), ex);
561    }
562  }
563
564  /**
565   * Initializes the list of services.
566   *
567   * @param services services to initialized, it must be a de-dupped list of
568   * services.
569   *
570   * @throws ServerException thrown if the services could not be initialized.
571   */
572  protected void initServices(List<Service> services) throws ServerException {
573    for (Service service : services) {
574      log.debug("Initializing service [{}]", service.getInterface());
575      checkServiceDependencies(service);
576      service.init(this);
577      this.services.put(service.getInterface(), service);
578    }
579    for (Service service : services) {
580      service.postInit();
581    }
582  }
583
584  /**
585   * Checks if all service dependencies of a service are available.
586   *
587   * @param service service to check if all its dependencies are available.
588   *
589   * @throws ServerException thrown if a service dependency is missing.
590   */
591  protected void checkServiceDependencies(Service service) throws ServerException {
592    if (service.getServiceDependencies() != null) {
593      for (Class dependency : service.getServiceDependencies()) {
594        if (services.get(dependency) == null) {
595          throw new ServerException(ServerException.ERROR.S10, service.getClass(), dependency);
596        }
597      }
598    }
599  }
600
601  /**
602   * Destroys the server services.
603   */
604  protected void destroyServices() {
605    List<Service> list = new ArrayList<Service>(services.values());
606    Collections.reverse(list);
607    for (Service service : list) {
608      try {
609        log.debug("Destroying service [{}]", service.getInterface());
610        service.destroy();
611      } catch (Throwable ex) {
612        log.error("Could not destroy service [{}], {}",
613                  new Object[]{service.getInterface(), ex.getMessage(), ex});
614      }
615    }
616    log.info("Services destroyed");
617  }
618
619  /**
620   * Destroys the server.
621   * <p/>
622   * All services are destroyed in reverse order of initialization, then the
623   * Log4j framework is shutdown.
624   */
625  public void destroy() {
626    ensureOperational();
627    destroyServices();
628    log.info("Server [{}] shutdown!", name);
629    log.info("======================================================");
630    if (!Boolean.getBoolean("test.circus")) {
631      LogManager.shutdown();
632    }
633    status = Status.SHUTDOWN;
634  }
635
636  /**
637   * Returns the name of the server.
638   *
639   * @return the server name.
640   */
641  public String getName() {
642    return name;
643  }
644
645  /**
646   * Returns the server prefix for server configuration properties.
647   * <p/>
648   * By default it is the server name.
649   *
650   * @return the prefix for server configuration properties.
651   */
652  public String getPrefix() {
653    return getName();
654  }
655
656  /**
657   * Returns the prefixed name of a server property.
658   *
659   * @param name of the property.
660   *
661   * @return prefixed name of the property.
662   */
663  public String getPrefixedName(String name) {
664    return getPrefix() + "." + Check.notEmpty(name, "name");
665  }
666
667  /**
668   * Returns the server home dir.
669   *
670   * @return the server home dir.
671   */
672  public String getHomeDir() {
673    return homeDir;
674  }
675
676  /**
677   * Returns the server config dir.
678   *
679   * @return the server config dir.
680   */
681  public String getConfigDir() {
682    return configDir;
683  }
684
685  /**
686   * Returns the server log dir.
687   *
688   * @return the server log dir.
689   */
690  public String getLogDir() {
691    return logDir;
692  }
693
694  /**
695   * Returns the server temp dir.
696   *
697   * @return the server temp dir.
698   */
699  public String getTempDir() {
700    return tempDir;
701  }
702
703  /**
704   * Returns the server configuration.
705   *
706   * @return the server configuration.
707   */
708  public Configuration getConfig() {
709    return config;
710
711  }
712
713  /**
714   * Returns the {@link Service} associated to the specified interface.
715   *
716   * @param serviceKlass service interface.
717   *
718   * @return the service implementation.
719   */
720  @SuppressWarnings("unchecked")
721  public <T> T get(Class<T> serviceKlass) {
722    ensureOperational();
723    Check.notNull(serviceKlass, "serviceKlass");
724    return (T) services.get(serviceKlass);
725  }
726
727  /**
728   * Adds a service programmatically.
729   * <p/>
730   * If a service with the same interface exists, it will be destroyed and
731   * removed before the given one is initialized and added.
732   * <p/>
733   * If an exception is thrown the server is destroyed.
734   *
735   * @param klass service class to add.
736   *
737   * @throws ServerException throw if the service could not initialized/added
738   * to the server.
739   */
740  public void setService(Class<? extends Service> klass) throws ServerException {
741    ensureOperational();
742    Check.notNull(klass, "serviceKlass");
743    if (getStatus() == Status.SHUTTING_DOWN) {
744      throw new IllegalStateException("Server shutting down");
745    }
746    try {
747      Service newService = klass.newInstance();
748      Service oldService = services.get(newService.getInterface());
749      if (oldService != null) {
750        try {
751          oldService.destroy();
752        } catch (Throwable ex) {
753          log.error("Could not destroy service [{}], {}",
754                    new Object[]{oldService.getInterface(), ex.getMessage(), ex});
755        }
756      }
757      newService.init(this);
758      services.put(newService.getInterface(), newService);
759    } catch (Exception ex) {
760      log.error("Could not set service [{}] programmatically -server shutting down-, {}", klass, ex);
761      destroy();
762      throw new ServerException(ServerException.ERROR.S09, klass, ex.getMessage(), ex);
763    }
764  }
765
766}