001    /**
002     *   GRANITE DATA SERVICES
003     *   Copyright (C) 2006-2013 GRANITE DATA SERVICES S.A.S.
004     *
005     *   This file is part of the Granite Data Services Platform.
006     *
007     *   Granite Data Services is free software; you can redistribute it and/or
008     *   modify it under the terms of the GNU Lesser General Public
009     *   License as published by the Free Software Foundation; either
010     *   version 2.1 of the License, or (at your option) any later version.
011     *
012     *   Granite Data Services is distributed in the hope that it will be useful,
013     *   but WITHOUT ANY WARRANTY; without even the implied warranty of
014     *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
015     *   General Public License for more details.
016     *
017     *   You should have received a copy of the GNU Lesser General Public
018     *   License along with this library; if not, write to the Free Software
019     *   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
020     *   USA, or see <http://www.gnu.org/licenses/>.
021     */
022    package org.granite.config.servlet3;
023    
024    import java.io.InputStream;
025    import java.lang.annotation.Annotation;
026    import java.lang.reflect.Field;
027    import java.lang.reflect.Method;
028    import java.math.BigDecimal;
029    import java.math.BigInteger;
030    import java.util.*;
031    
032    import javax.servlet.FilterRegistration;
033    import javax.servlet.Servlet;
034    import javax.servlet.ServletContainerInitializer;
035    import javax.servlet.ServletContext;
036    import javax.servlet.ServletException;
037    import javax.servlet.ServletRegistration;
038    import javax.servlet.annotation.HandlesTypes;
039    
040    import org.granite.config.ConfigProvider;
041    import org.granite.config.GraniteConfig;
042    import org.granite.config.GraniteConfigListener;
043    import org.granite.config.GraniteConfigListener.ServiceConfigurator;
044    import org.granite.config.ServletGraniteConfig;
045    import org.granite.config.flex.Channel;
046    import org.granite.config.flex.Destination;
047    import org.granite.config.flex.EndPoint;
048    import org.granite.config.flex.Factory;
049    import org.granite.config.flex.Service;
050    import org.granite.config.flex.ServicesConfig;
051    import org.granite.config.flex.ServletServicesConfig;
052    import org.granite.gravity.GravityManager.GravityServiceConfigurator;
053    import org.granite.gravity.config.AbstractActiveMQTopicDestination;
054    import org.granite.gravity.config.AbstractJmsTopicDestination;
055    import org.granite.gravity.config.AbstractMessagingDestination;
056    import org.granite.gravity.config.servlet3.ActiveMQTopicDestination;
057    import org.granite.gravity.config.servlet3.JmsTopicDestination;
058    import org.granite.gravity.config.servlet3.MessagingDestination;
059    import org.granite.gravity.security.GravityDestinationSecurizer;
060    import org.granite.logging.Logger;
061    import org.granite.messaging.amf.io.util.externalizer.BigDecimalExternalizer;
062    import org.granite.messaging.amf.io.util.externalizer.BigIntegerExternalizer;
063    import org.granite.messaging.amf.io.util.externalizer.Externalizer;
064    import org.granite.messaging.amf.io.util.externalizer.LongExternalizer;
065    import org.granite.messaging.amf.process.AMF3MessageInterceptor;
066    import org.granite.messaging.service.ExceptionConverter;
067    import org.granite.messaging.service.ServiceFactory;
068    import org.granite.messaging.service.SimpleServiceFactory;
069    import org.granite.messaging.service.security.RemotingDestinationSecurizer;
070    import org.granite.messaging.service.security.SecurityService;
071    import org.granite.messaging.service.tide.TideComponentAnnotatedWithMatcher;
072    import org.granite.messaging.service.tide.TideComponentInstanceOfMatcher;
073    import org.granite.messaging.service.tide.TideComponentNameMatcher;
074    import org.granite.messaging.service.tide.TideComponentTypeMatcher;
075    import org.granite.messaging.webapp.AMFMessageFilter;
076    import org.granite.messaging.webapp.AMFMessageServlet;
077    import org.granite.util.TypeUtil;
078    import org.granite.util.XMap;
079    
080    /**
081     * @author William DRAI
082     */
083    @HandlesTypes({ServerFilter.class})
084    public class GraniteServlet3Initializer implements ServletContainerInitializer {
085    
086            public void onStartup(Set<Class<?>> scannedClasses, ServletContext servletContext) throws ServletException {
087                    Set<Class<?>> classes = new HashSet<Class<?>>();
088                    if (scannedClasses != null) {
089                            classes.addAll(scannedClasses);
090                            classes.remove(ServerFilter.class);     // JBossWeb adds the annotation ???
091                    }
092                    if (classes.size() > 1)
093                            throw new ServletException("Application must have only one ServerFilter configuration class");
094                    
095                    if (!classes.isEmpty()) {
096                            // Configure GraniteDS only if we find a config class annotated with @ServerFilter
097                            Class<?> clazz = classes.iterator().next();
098                            ServerFilter serverFilter = clazz.getAnnotation(ServerFilter.class);
099                            
100                            servletContext.setAttribute(GraniteConfigListener.GRANITE_CONFIG_ATTRIBUTE, new Servlet3ServiceConfigurator(clazz));
101                            
102                        servletContext.addListener(new GraniteConfigListener());
103                            
104                            if (servletContext.getFilterRegistration("AMFMessageFilter") == null) {
105                                    FilterRegistration.Dynamic graniteFilter = servletContext.addFilter("AMFMessageFilter", AMFMessageFilter.class);
106                                    graniteFilter.addMappingForUrlPatterns(null, true, serverFilter.graniteUrlMapping());
107                            }
108                            if (servletContext.getServletRegistration("AMFMessageServlet") == null) {
109                                    ServletRegistration.Dynamic graniteServlet = servletContext.addServlet("AMFMessageServlet", AMFMessageServlet.class);
110                                    graniteServlet.setLoadOnStartup(1);
111                                    graniteServlet.addMapping(serverFilter.graniteUrlMapping());
112                            }
113                            
114                            try {
115                                    if (servletContext.getServletRegistration("GravityServlet") == null) {
116                                            Class<? extends Servlet> gravityAsyncServletClass = TypeUtil.forName("org.granite.gravity.servlet3.GravityAsyncServlet", Servlet.class);
117                                            ServletRegistration.Dynamic gravityServlet = servletContext.addServlet("GravityServlet", gravityAsyncServletClass);
118                                            gravityServlet.setLoadOnStartup(1);
119                                            gravityServlet.setAsyncSupported(true);
120                                            gravityServlet.addMapping(serverFilter.gravityUrlMapping());
121                                    }
122                            }
123                            catch (ClassNotFoundException e) {
124                                    servletContext.log("Could not setup GravityAsyncServlet", e);
125                            }
126                    }
127            }
128            
129            
130            public static class Servlet3ServiceConfigurator implements ServiceConfigurator, GravityServiceConfigurator {
131                    
132                    private static final Logger log = Logger.getLogger(Servlet3ServiceConfigurator.class);
133                    
134                    private final Class<?> serverFilterClass;
135                    
136                    public Servlet3ServiceConfigurator(Class<?> serverFilterClass) {
137                            this.serverFilterClass = serverFilterClass;
138                    }
139                    
140                    public void initialize(ServletContext servletContext) {
141                            servletContext.setAttribute(ServletGraniteConfig.GRANITE_CONFIG_DEFAULT_KEY, "org/granite/config/servlet3/granite-config-servlet3.xml");
142                    }
143                    
144                public void configureServices(ServletContext servletContext) throws ServletException {
145                    GraniteConfig graniteConfig = ServletGraniteConfig.loadConfig(servletContext);
146                    ServicesConfig servicesConfig = ServletServicesConfig.loadConfig(servletContext, true);
147                    
148                    ServerFilter serverFilter = serverFilterClass.getAnnotation(ServerFilter.class);
149                    
150                    ConfigProvider configProvider = null;
151                    
152                    boolean useTide = serverFilter.tide();
153                    String type = serverFilter.type();
154                    Class<?> factoryClass = null;
155                    Set<Class<?>> tideInterfaces = new HashSet<Class<?>>(Arrays.asList(serverFilter.tideInterfaces()));
156                    Set<Class<? extends Annotation>> tideAnnotations = new HashSet<Class<? extends Annotation>>(Arrays.asList(serverFilter.tideAnnotations()));
157                    
158                    if (!serverFilter.configProviderClass().equals(ConfigProvider.class)) {
159                            try {
160                                    configProvider = TypeUtil.newInstance(serverFilter.configProviderClass(), new Class[] { ServletContext.class }, new Object[] { servletContext });
161                                    
162                                    servletContext.setAttribute(GraniteConfigListener.GRANITE_CONFIG_PROVIDER_ATTRIBUTE, configProvider);
163                                    
164                                    if (configProvider.useTide() != null)
165                                            useTide = configProvider.useTide();
166                                    
167                                    if (configProvider.getType() != null)
168                                            type = configProvider.getType();
169                                    
170                                    factoryClass = configProvider.getFactoryClass();
171                                    
172                                    if (configProvider.getTideInterfaces() != null)
173                                            tideInterfaces.addAll(Arrays.asList(configProvider.getTideInterfaces()));
174                                    
175                                    if (configProvider.getTideAnnotations() != null)
176                                            tideAnnotations.addAll(Arrays.asList(configProvider.getTideAnnotations()));
177                                    }
178                                    catch (Exception e) {
179                                            log.error(e, "Could not set config provider of type %s", serverFilter.configProviderClass().getName());
180                                    }
181                    }
182                    
183                    if (!serverFilter.factoryClass().equals(ServiceFactory.class))
184                            factoryClass = serverFilter.factoryClass();
185                else if (!serverFilter.factoryClassName().equals("")) {
186                    try {
187                        factoryClass = TypeUtil.forName(serverFilter.factoryClassName(), ServiceFactory.class);
188                    }
189                    catch (ClassNotFoundException e) {
190                        throw new ServletException("Could not find service factory class " + serverFilter.factoryClassName(), e);
191                    }
192                }
193                    
194                    if (factoryClass == null) {
195                            factoryClass = SimpleServiceFactory.class;
196                            useTide = false;
197                    }
198                    
199                    for (Class<?> ti : tideInterfaces) {
200                            try {
201                                    graniteConfig.getTideComponentMatchers().add(new TideComponentInstanceOfMatcher(ti.getName(), false));
202                                    log.debug("Enabled components implementing %s for Tide remoting", ti);
203                            }
204                            catch (Exception e) {
205                                    log.error(e, "Could not add tide-component interface %s", ti);
206                            }
207                    }
208                    for (Class<? extends Annotation> ta : tideAnnotations) {
209                            try {
210                                    graniteConfig.getTideComponentMatchers().add(new TideComponentAnnotatedWithMatcher(ta.getName(), false));
211                                    log.debug("Enabled components annotated with %s for Tide remoting", ta);
212                            }
213                            catch (Exception e) {
214                                    log.error(e, "Could not add tide-component annotation %s", ta);
215                            }
216                    }
217                    for (String tn : serverFilter.tideNames()) {
218                            try {
219                                    graniteConfig.getTideComponentMatchers().add(new TideComponentNameMatcher(tn, false));
220                                    log.debug("Enabled components with name %s for Tide remoting", tn);
221                            }
222                            catch (Exception e) {
223                                    log.error(e, "Could not add tide-component name %s", tn);
224                            }
225                    }
226                    for (String tt : serverFilter.tideTypes()) {
227                            try {
228                                    graniteConfig.getTideComponentMatchers().add(new TideComponentTypeMatcher(tt, false));
229                                    log.debug("Enabled components with type %s for Tide remoting", tt);
230                            }
231                            catch (Exception e) {
232                                    log.error(e, "Could not add tide-component type %s", tt);
233                            }
234                    }
235                    
236                    for (Class<? extends ExceptionConverter> ec : serverFilter.exceptionConverters()) {
237                            graniteConfig.registerExceptionConverter(ec, true);
238                                    log.debug("Registered exception converter %s", ec);
239                    }
240                    if (configProvider != null) {
241                            for (ExceptionConverter ec : configProvider.findInstances(ExceptionConverter.class)) {
242                                    graniteConfig.registerExceptionConverter(ec, true);
243                                    log.debug("Registered exception converter %s", ec.getClass());
244                            }
245                    }
246    
247                if (configProvider != null) {
248                    for (Externalizer ext : configProvider.findInstances(Externalizer.class)) {
249                        graniteConfig.registerExternalizer(ext);
250                        log.debug("Registered externalizer %s", ext.getClass());
251                    }
252                }
253    
254                    if (serverFilter.useBigDecimal())
255                            graniteConfig.setExternalizersByType(BigDecimal.class.getName(), BigDecimalExternalizer.class.getName());
256                    
257                    if (serverFilter.useBigInteger())
258                            graniteConfig.setExternalizersByType(BigInteger.class.getName(), BigIntegerExternalizer.class.getName());
259                    
260                    if (serverFilter.useLong())
261                            graniteConfig.setExternalizersByType(Long.class.getName(), LongExternalizer.class.getName());
262                    
263                    if (!serverFilter.securityServiceClass().equals(SecurityService.class)) {
264                            try {
265                                    graniteConfig.setSecurityService(TypeUtil.newInstance(serverFilter.securityServiceClass(), SecurityService.class));
266                            }
267                            catch (Exception e) {
268                                    throw new ServletException("Could not setup security service", e);
269                            }
270                    }
271                    else if (graniteConfig.getSecurityService() == null && configProvider != null) {
272                                    SecurityService securityService = configProvider.findInstance(SecurityService.class);
273                                    graniteConfig.setSecurityService(securityService);
274                            }
275                    if (graniteConfig.getSecurityService() == null) {
276                            String securityServiceClassName = null;
277                            // Try auto-detect
278                            try {
279                                    TypeUtil.forName("org.mortbay.jetty.Request");
280                                    securityServiceClassName = "org.granite.messaging.service.security.Jetty6SecurityService";
281                            }
282                            catch (ClassNotFoundException e1) {
283                                    try {
284                                            TypeUtil.forName("org.eclipse.jetty.server.Request");
285                                            securityServiceClassName = "org.granite.messaging.service.security.Jetty7SecurityService";
286                                    }
287                                    catch (ClassNotFoundException e1b) {
288                                            try {
289                                                    TypeUtil.forName("weblogic.servlet.security.ServletAuthentication");
290                                                    securityServiceClassName = "org.granite.messaging.service.security.WebLogicSecurityService";
291                                            }
292                                            catch (ClassNotFoundException e2) {
293                                                    try {
294                                                            TypeUtil.forName("com.sun.appserv.server.LifecycleEvent");
295                                                    securityServiceClassName = "org.granite.messaging.service.security.GlassFishV3SecurityService";
296                                                    }
297                                                    catch (ClassNotFoundException e3) {
298                                                            securityServiceClassName = "org.granite.messaging.service.security.Tomcat7SecurityService";
299                                                    }
300                                            }
301                                            try {
302                                                    graniteConfig.setSecurityService((SecurityService)TypeUtil.newInstance(securityServiceClassName));
303                                    }
304                                    catch (Exception e) {
305                                            throw new ServletException("Could not setup security service " + securityServiceClassName, e);
306                                    }
307                                    }
308                            }
309                    }
310                    
311                    if (!serverFilter.amf3MessageInterceptor().equals(AMF3MessageInterceptor.class)) {
312                            try {
313                                    graniteConfig.setAmf3MessageInterceptor(TypeUtil.newInstance(serverFilter.amf3MessageInterceptor(), AMF3MessageInterceptor.class));
314                            }
315                            catch (Exception e) {
316                                    throw new ServletException("Could not setup amf3 message interceptor", e);
317                            }
318                    }
319                    else if (graniteConfig.getAmf3MessageInterceptor() == null && configProvider != null) {
320                            AMF3MessageInterceptor amf3MessageInterceptor = configProvider.findInstance(AMF3MessageInterceptor.class);
321                                    graniteConfig.setAmf3MessageInterceptor(amf3MessageInterceptor);
322                    }
323                    
324                    Channel channel = servicesConfig.findChannelById("graniteamf");
325                    if (channel == null) {
326                            channel = new Channel("graniteamf", "mx.messaging.channels.AMFChannel", 
327                                    new EndPoint("http://{server.name}:{server.port}/{context.root}/graniteamf/amf", "flex.messaging.endpoints.AMFEndpoint"), 
328                                    new XMap());
329                            servicesConfig.addChannel(channel);
330                    }
331    
332                    XMap factoryProperties = new XMap();
333                    String lookup = null;
334                    if (useTide) {
335                            lookup = "java:global/{context.root}/{capitalized.component.name}Bean";
336                            if (isJBoss6())
337                                    lookup = "{capitalized.component.name}Bean/local";
338                            if (!("".equals(serverFilter.ejbLookup())))
339                                    lookup = serverFilter.ejbLookup();
340                    }
341                    else {
342                            lookup = "java:global/{context.root}/{capitalized.destination.id}Bean";
343                            if (isJBoss6())
344                                    lookup = "{capitalized.destination.id}Bean/local";
345                            if (!("".equals(serverFilter.ejbLookup())))
346                                    lookup = serverFilter.ejbLookup();
347                    }
348                    if (lookup.indexOf("{context.root}") >= 0) {
349                            try {
350                                    // Call by reflection because of JDK 1.4
351                                    Method m = servletContext.getClass().getMethod("getContextPath");
352                        m.setAccessible(true);
353                                    String contextPath = (String)m.invoke(servletContext);
354                                    lookup = lookup.replace("{context.root}", contextPath.substring(1));
355                            }
356                            catch (Exception e) {
357                                    log.error(e, "Could not get context path, please define lookup manually in @ServerFilter");
358                            }
359                    }
360                    factoryProperties.put("lookup", lookup);
361    
362                    if (useTide) {
363                            Factory factory = servicesConfig.findFactoryById("tide-" + type + "-factory");
364                            if (factory == null) {
365                                    factory = new Factory("tide-" + type + "-factory", factoryClass.getName(), factoryProperties);
366                                    servicesConfig.addFactory(factory);
367                            }
368                            
369                            Service service = servicesConfig.findServiceById("granite-service");
370                            if (service == null) {
371                                    service = new Service("granite-service", "flex.messaging.services.RemotingService", 
372                                                    "flex.messaging.messages.RemotingMessage", null, null, new HashMap<String, Destination>());
373                                    List<String> channelIds = new ArrayList<String>();
374                                    channelIds.add("graniteamf");
375                                    List<String> tideRoles = serverFilter.tideRoles().length == 0 ? null : Arrays.asList(serverFilter.tideRoles());
376                                    Destination destination = new Destination(type, channelIds, new XMap(), tideRoles, null, null);
377                                    destination.getProperties().put("factory", "tide-" + type + "-factory");
378                                    if (!("".equals(serverFilter.entityManagerFactoryJndiName())))
379                                            destination.getProperties().put("entity-manager-factory-jndi-name", serverFilter.entityManagerFactoryJndiName());
380                                    else if (!("".equals(serverFilter.entityManagerJndiName())))
381                                            destination.getProperties().put("entity-manager-jndi-name", serverFilter.entityManagerJndiName());
382                                    if (!("".equals(serverFilter.validatorClassName())))
383                                            destination.getProperties().put("validator-class-name", serverFilter.validatorClassName());
384                                    service.getDestinations().put(type, destination);
385                                    
386                                    if (destination.getSecurizer() == null && configProvider != null) {
387                                    RemotingDestinationSecurizer securizer = configProvider.findInstance(RemotingDestinationSecurizer.class);
388                                    destination.setSecurizer(securizer);
389                                    }
390                                    
391                                    servicesConfig.addService(service);
392                            }
393                        
394                            if (factoryClass.getName().equals("org.granite.tide.ejb.EjbServiceFactory"))
395                                    servicesConfig.scan(null);
396                            
397                            log.info("Registered Tide " + factoryClass + " service factory and " + type + " destination");
398                    }
399                    else {
400                            Factory factory = new Factory(type + "-factory", factoryClass.getName(), factoryProperties);
401                            servicesConfig.addFactory(factory);
402                            
403                            Service service = new Service("granite-service", "flex.messaging.services.RemotingService", 
404                                    "flex.messaging.messages.RemotingMessage", null, null, new HashMap<String, Destination>());
405                            servicesConfig.addService(service);
406                        
407                        servicesConfig.scan(null);
408                            
409                            log.info("Registered " + factoryClass + " service factory");
410                    }
411                }
412                
413                    private static boolean isJBoss6() {
414                            try {
415                                    InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("/org/jboss/version.properties");
416                                    if (is != null)
417                                            return true;
418                            }
419                            catch (Throwable t) {
420                            }
421                            return false;
422                    }               
423                
424                private static void initSecurizer(AbstractMessagingDestination messagingDestination, Class<? extends GravityDestinationSecurizer> securizerClass, ConfigProvider configProvider) {
425                            if (securizerClass != GravityDestinationSecurizer.class) {
426                                    if (configProvider != null)
427                                            messagingDestination.setSecurizer(configProvider.findInstance(securizerClass));
428                                    if (messagingDestination.getSecurizer() == null)
429                                            messagingDestination.setSecurizerClassName(securizerClass.getName());
430                            }
431                }
432    
433                    
434                    public void configureGravityServices(ServletContext servletContext) throws ServletException {
435                    ServicesConfig servicesConfig = ServletServicesConfig.loadConfig(servletContext);
436                    
437                    ConfigProvider configProvider = (ConfigProvider)servletContext.getAttribute(GraniteConfigListener.GRANITE_CONFIG_PROVIDER_ATTRIBUTE);
438                    
439                    for (Field field : serverFilterClass.getDeclaredFields()) {
440                            if (field.isAnnotationPresent(MessagingDestination.class)) {
441                                    MessagingDestination md = field.getAnnotation(MessagingDestination.class);
442                                    AbstractMessagingDestination messagingDestination = new AbstractMessagingDestination();
443                                    messagingDestination.setId(field.getName());
444                                    messagingDestination.setNoLocal(md.noLocal());
445                                    messagingDestination.setSessionSelector(md.sessionSelector());
446                                    initSecurizer(messagingDestination, md.securizer(), configProvider);
447                                    messagingDestination.initServices(servicesConfig);
448                            }
449                            else if (field.isAnnotationPresent(JmsTopicDestination.class)) {
450                                    JmsTopicDestination md = field.getAnnotation(JmsTopicDestination.class);
451                                    AbstractJmsTopicDestination messagingDestination = new AbstractJmsTopicDestination();
452                                    messagingDestination.setId(field.getName());
453                                    messagingDestination.setNoLocal(md.noLocal());
454                                    messagingDestination.setSessionSelector(md.sessionSelector());
455                                    initSecurizer(messagingDestination, md.securizer(), configProvider);
456                                    messagingDestination.initServices(servicesConfig);
457                                    messagingDestination.setName(md.name());
458                                    messagingDestination.setTextMessages(md.textMessages());
459                                    messagingDestination.setAcknowledgeMode(md.acknowledgeMode());
460                                    messagingDestination.setConnectionFactory(md.connectionFactory());
461                                    messagingDestination.setTransactedSessions(md.transactedSessions());
462                                    messagingDestination.setJndiName(md.topicJndiName());
463                                    messagingDestination.initServices(servicesConfig);
464                            }
465                            else if (field.isAnnotationPresent(ActiveMQTopicDestination.class)) {
466                                    ActiveMQTopicDestination md = field.getAnnotation(ActiveMQTopicDestination.class);
467                                    AbstractActiveMQTopicDestination messagingDestination = new AbstractActiveMQTopicDestination();
468                                    messagingDestination.setId(field.getName());
469                                    messagingDestination.setNoLocal(md.noLocal());
470                                    messagingDestination.setSessionSelector(md.sessionSelector());
471                                    initSecurizer(messagingDestination, md.securizer(), configProvider);
472                                    messagingDestination.initServices(servicesConfig);
473                                    messagingDestination.setName(md.name());
474                                    messagingDestination.setTextMessages(md.textMessages());
475                                    messagingDestination.setAcknowledgeMode(md.acknowledgeMode());
476                                    messagingDestination.setConnectionFactory(md.connectionFactory());
477                                    messagingDestination.setTransactedSessions(md.transactedSessions());
478                                    messagingDestination.setJndiName(md.topicJndiName());
479                                    messagingDestination.setBrokerUrl(md.brokerUrl());
480                                    messagingDestination.setCreateBroker(md.createBroker());
481                                    messagingDestination.setDurable(md.durable());
482                                    messagingDestination.setWaitForStart(md.waitForStart());
483                                    messagingDestination.setFileStoreRoot(md.fileStoreRoot());
484                                    messagingDestination.initServices(servicesConfig);
485                            }
486                    }
487                    }
488            }
489    }