001/*
002 *   Copyright (C) 2015 Christian Schulte <cs@schulte.it>
003 *   All rights reserved.
004 *
005 *   Redistribution and use in source and binary forms, with or without
006 *   modification, are permitted provided that the following conditions
007 *   are met:
008 *
009 *     o Redistributions of source code must retain the above copyright
010 *       notice, this list of conditions and the following disclaimer.
011 *
012 *     o Redistributions in binary form must reproduce the above copyright
013 *       notice, this list of conditions and the following disclaimer in
014 *       the documentation and/or other materials provided with the
015 *       distribution.
016 *
017 *   THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
018 *   INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
019 *   AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
020 *   THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,
021 *   INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
022 *   NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
023 *   DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
024 *   THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
025 *   (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
026 *   THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
027 *
028 *   $JOMC: DefaultServiceFactory.java 5293 2016-08-29 17:41:51Z schulte $
029 *
030 */
031package org.jomc.modlet;
032
033import java.lang.reflect.InvocationTargetException;
034import java.lang.reflect.Method;
035import java.lang.reflect.Modifier;
036import java.lang.reflect.UndeclaredThrowableException;
037import java.text.MessageFormat;
038import java.util.ArrayList;
039import java.util.List;
040import java.util.ResourceBundle;
041import java.util.concurrent.Callable;
042import java.util.concurrent.CancellationException;
043import java.util.concurrent.ExecutionException;
044import java.util.concurrent.Future;
045import java.util.logging.Level;
046
047/**
048 * Default {@code ServiceFactory} implementation.
049 *
050 * @author <a href="mailto:cs@schulte.it">Christian Schulte</a>
051 * @version $JOMC: DefaultServiceFactory.java 5293 2016-08-29 17:41:51Z schulte $
052 * @see ModelContext#createServiceObjects(java.lang.String, java.lang.String, java.lang.Class)
053 * @since 1.9
054 */
055public class DefaultServiceFactory implements ServiceFactory
056{
057
058    /**
059     * Constant for the name of the system property controlling property {@code defaultOrdinal}.
060     *
061     * @see #getDefaultOrdinal()
062     */
063    private static final String DEFAULT_ORDINAL_PROPERTY_NAME =
064        "org.jomc.modlet.DefaultServiceFactory.defaultOrdinal";
065
066    /**
067     * Default value of the ordinal number of the factory.
068     *
069     * @see #getDefaultOrdinal()
070     */
071    private static final Integer DEFAULT_ORDINAL = 0;
072
073    /**
074     * Default ordinal number of the factory.
075     */
076    private static volatile Integer defaultOrdinal;
077
078    /**
079     * Ordinal number of the factory.
080     */
081    private volatile Integer ordinal;
082
083    /**
084     * Creates a new {@code DefaultServiceFactory} instance.
085     */
086    public DefaultServiceFactory()
087    {
088        super();
089    }
090
091    /**
092     * Gets the default ordinal number of the factory.
093     * <p>
094     * The default ordinal number is controlled by system property
095     * {@code org.jomc.modlet.DefaultServiceFactory.defaultOrdinal} holding the default ordinal number of the
096     * factory. If that property is not set, the {@code 0} default is returned.
097     * </p>
098     *
099     * @return The default ordinal number of the factory.
100     *
101     * @see #setDefaultOrdinal(java.lang.Integer)
102     */
103    public static int getDefaultOrdinal()
104    {
105        if ( defaultOrdinal == null )
106        {
107            defaultOrdinal = Integer.getInteger( DEFAULT_ORDINAL_PROPERTY_NAME, DEFAULT_ORDINAL );
108        }
109
110        return defaultOrdinal;
111    }
112
113    /**
114     * Sets the default ordinal number of the factory.
115     *
116     * @param value The new default ordinal number of the factory or {@code null}.
117     *
118     * @see #getDefaultOrdinal()
119     */
120    public static void setDefaultOrdinal( final Integer value )
121    {
122        defaultOrdinal = value;
123    }
124
125    /**
126     * Gets the ordinal number of the factory.
127     *
128     * @return The ordinal number of the factory.
129     *
130     * @see #getDefaultOrdinal()
131     * @see #setOrdinal(java.lang.Integer)
132     */
133    public final int getOrdinal()
134    {
135        if ( this.ordinal == null )
136        {
137            this.ordinal = getDefaultOrdinal();
138        }
139
140        return this.ordinal;
141    }
142
143    /**
144     * Sets the ordinal number of the factory.
145     *
146     * @param value The new ordinal number of the factory or {@code null}.
147     *
148     * @see #getOrdinal()
149     */
150    public final void setOrdinal( final Integer value )
151    {
152        this.ordinal = value;
153    }
154
155    public <T> T createServiceObject( final ModelContext context, final Service service, final Class<T> type )
156        throws ModelException
157    {
158        if ( context == null )
159        {
160            throw new NullPointerException( "context" );
161        }
162        if ( service == null )
163        {
164            throw new NullPointerException( "service" );
165        }
166        if ( type == null )
167        {
168            throw new NullPointerException( "type" );
169        }
170
171        try
172        {
173            final Class<?> clazz = context.findClass( service.getClazz() );
174
175            if ( clazz == null )
176            {
177                throw new ModelException( getMessage( "serviceNotFound", service.getOrdinal(), service.
178                                                      getIdentifier(),
179                                                      service.getClazz() ) );
180
181            }
182
183            if ( !type.isAssignableFrom( clazz ) )
184            {
185                throw new ModelException( getMessage( "illegalService", service.getOrdinal(), service.
186                                                      getIdentifier(),
187                                                      service.getClazz(), type.getName() ) );
188
189            }
190
191            final T serviceObject = clazz.asSubclass( type ).newInstance();
192
193            if ( !service.getProperty().isEmpty() )
194            {
195                if ( context.getExecutorService() != null && service.getProperty().size() > 1 )
196                {
197                    final List<Callable<Void>> tasks =
198                        new ArrayList<Callable<Void>>( service.getProperty().size() );
199
200                    for ( int i = 0, s0 = service.getProperty().size(); i < s0; i++ )
201                    {
202                        final Property p = service.getProperty().get( i );
203
204                        tasks.add( new Callable<Void>()
205                        {
206
207                            public Void call() throws ModelException
208                            {
209                                initProperty( context, serviceObject, p.getName(), p.getValue() );
210                                return null;
211                            }
212
213                        } );
214                    }
215
216                    for ( final Future<Void> task : context.getExecutorService().invokeAll( tasks ) )
217                    {
218                        task.get();
219                    }
220                }
221                else
222                {
223                    for ( int i = 0, s0 = service.getProperty().size(); i < s0; i++ )
224                    {
225                        final Property p = service.getProperty().get( i );
226                        this.initProperty( context, serviceObject, p.getName(), p.getValue() );
227                    }
228                }
229            }
230
231            return serviceObject;
232        }
233        catch ( final CancellationException e )
234        {
235            throw new ModelException( getMessage( "failedCreatingObject", service.getClazz() ), e );
236        }
237        catch ( final InterruptedException e )
238        {
239            throw new ModelException( getMessage( "failedCreatingObject", service.getClazz() ), e );
240        }
241        catch ( final InstantiationException e )
242        {
243            throw new ModelException( getMessage( "failedCreatingObject", service.getClazz() ), e );
244        }
245        catch ( final IllegalAccessException e )
246        {
247            throw new ModelException( getMessage( "failedCreatingObject", service.getClazz() ), e );
248        }
249        catch ( final ExecutionException e )
250        {
251            if ( e.getCause() instanceof ModelException )
252            {
253                throw (ModelException) e.getCause();
254            }
255            else if ( e.getCause() instanceof RuntimeException )
256            {
257                // The fork-join framework breaks the exception handling contract of Callable by re-throwing any
258                // exception caught using a runtime exception.
259                if ( e.getCause().getCause() instanceof ModelException )
260                {
261                    throw (ModelException) e.getCause().getCause();
262                }
263                else if ( e.getCause().getCause() instanceof RuntimeException )
264                {
265                    throw (RuntimeException) e.getCause().getCause();
266                }
267                else if ( e.getCause().getCause() instanceof Error )
268                {
269                    throw (Error) e.getCause().getCause();
270                }
271                else if ( e.getCause().getCause() instanceof Exception )
272                {
273                    // Checked exception not declared to be thrown by the Callable's 'call' method.
274                    throw new UndeclaredThrowableException( e.getCause().getCause() );
275                }
276                else
277                {
278                    throw (RuntimeException) e.getCause();
279                }
280            }
281            else if ( e.getCause() instanceof Error )
282            {
283                throw (Error) e.getCause();
284            }
285            else
286            {
287                // Checked exception not declared to be thrown by the Callable's 'call' method.
288                throw new UndeclaredThrowableException( e.getCause() );
289            }
290        }
291    }
292
293    private <T> void initProperty( final ModelContext context, final T object, final String propertyName,
294                                   final String propertyValue )
295        throws ModelException
296    {
297        if ( object == null )
298        {
299            throw new NullPointerException( "object" );
300        }
301        if ( propertyName == null )
302        {
303            throw new NullPointerException( "propertyName" );
304        }
305
306        try
307        {
308            final char[] chars = propertyName.toCharArray();
309
310            if ( Character.isLowerCase( chars[0] ) )
311            {
312                chars[0] = Character.toUpperCase( chars[0] );
313            }
314
315            final String methodNameSuffix = String.valueOf( chars );
316            Method getterMethod = null;
317
318            try
319            {
320                getterMethod = object.getClass().getMethod( "get" + methodNameSuffix );
321            }
322            catch ( final NoSuchMethodException e )
323            {
324                if ( context.isLoggable( Level.FINEST ) )
325                {
326                    context.log( Level.FINEST, null, e );
327                }
328
329                getterMethod = null;
330            }
331
332            if ( getterMethod == null )
333            {
334                try
335                {
336                    getterMethod = object.getClass().getMethod( "is" + methodNameSuffix );
337                }
338                catch ( final NoSuchMethodException e )
339                {
340                    if ( context.isLoggable( Level.FINEST ) )
341                    {
342                        context.log( Level.FINEST, null, e );
343                    }
344
345                    getterMethod = null;
346                }
347            }
348
349            if ( getterMethod == null )
350            {
351                throw new ModelException( getMessage( "getterMethodNotFound", object.getClass().getName(),
352                                                      propertyName ) );
353
354            }
355
356            final Class<?> propertyType = getterMethod.getReturnType();
357            Class<?> boxedPropertyType = propertyType;
358            Class<?> unboxedPropertyType = propertyType;
359
360            if ( Boolean.TYPE.equals( propertyType ) )
361            {
362                boxedPropertyType = Boolean.class;
363            }
364            else if ( Character.TYPE.equals( propertyType ) )
365            {
366                boxedPropertyType = Character.class;
367            }
368            else if ( Byte.TYPE.equals( propertyType ) )
369            {
370                boxedPropertyType = Byte.class;
371            }
372            else if ( Short.TYPE.equals( propertyType ) )
373            {
374                boxedPropertyType = Short.class;
375            }
376            else if ( Integer.TYPE.equals( propertyType ) )
377            {
378                boxedPropertyType = Integer.class;
379            }
380            else if ( Long.TYPE.equals( propertyType ) )
381            {
382                boxedPropertyType = Long.class;
383            }
384            else if ( Float.TYPE.equals( propertyType ) )
385            {
386                boxedPropertyType = Float.class;
387            }
388            else if ( Double.TYPE.equals( propertyType ) )
389            {
390                boxedPropertyType = Double.class;
391            }
392
393            if ( Boolean.class.equals( propertyType ) )
394            {
395                unboxedPropertyType = Boolean.TYPE;
396            }
397            else if ( Character.class.equals( propertyType ) )
398            {
399                unboxedPropertyType = Character.TYPE;
400            }
401            else if ( Byte.class.equals( propertyType ) )
402            {
403                unboxedPropertyType = Byte.TYPE;
404            }
405            else if ( Short.class.equals( propertyType ) )
406            {
407                unboxedPropertyType = Short.TYPE;
408            }
409            else if ( Integer.class.equals( propertyType ) )
410            {
411                unboxedPropertyType = Integer.TYPE;
412            }
413            else if ( Long.class.equals( propertyType ) )
414            {
415                unboxedPropertyType = Long.TYPE;
416            }
417            else if ( Float.class.equals( propertyType ) )
418            {
419                unboxedPropertyType = Float.TYPE;
420            }
421            else if ( Double.class.equals( propertyType ) )
422            {
423                unboxedPropertyType = Double.TYPE;
424            }
425
426            Method setterMethod = null;
427
428            try
429            {
430                setterMethod = object.getClass().getMethod( "set" + methodNameSuffix, boxedPropertyType );
431            }
432            catch ( final NoSuchMethodException e )
433            {
434                if ( context.isLoggable( Level.FINEST ) )
435                {
436                    context.log( Level.FINEST, null, e );
437                }
438
439                setterMethod = null;
440            }
441
442            if ( setterMethod == null && !boxedPropertyType.equals( unboxedPropertyType ) )
443            {
444                try
445                {
446                    setterMethod = object.getClass().getMethod( "set" + methodNameSuffix, unboxedPropertyType );
447                }
448                catch ( final NoSuchMethodException e )
449                {
450                    if ( context.isLoggable( Level.FINEST ) )
451                    {
452                        context.log( Level.FINEST, null, e );
453                    }
454
455                    setterMethod = null;
456                }
457            }
458
459            if ( setterMethod == null )
460            {
461                throw new ModelException( getMessage( "setterMethodNotFound", object.getClass().getName(),
462                                                      propertyName ) );
463
464            }
465
466            if ( boxedPropertyType.equals( Character.class ) )
467            {
468                if ( propertyValue == null || propertyValue.length() != 1 )
469                {
470                    throw new ModelException( getMessage( "unsupportedCharacterValue", object.getClass().getName(),
471                                                          propertyName ) );
472
473                }
474
475                setterMethod.invoke( object, propertyValue.charAt( 0 ) );
476            }
477            else if ( propertyValue != null )
478            {
479                invocation:
480                {
481                    if ( boxedPropertyType.equals( String.class ) )
482                    {
483                        setterMethod.invoke( object, propertyValue );
484                        break invocation;
485                    }
486
487                    try
488                    {
489                        setterMethod.invoke( object, boxedPropertyType.getConstructor( String.class ).
490                                             newInstance( propertyValue ) );
491
492                        break invocation;
493                    }
494                    catch ( final NoSuchMethodException e1 )
495                    {
496                        if ( context.isLoggable( Level.FINEST ) )
497                        {
498                            context.log( Level.FINEST, null, e1 );
499                        }
500                    }
501
502                    try
503                    {
504                        final Method valueOf = boxedPropertyType.getMethod( "valueOf", String.class );
505
506                        if ( Modifier.isStatic( valueOf.getModifiers() )
507                                 && ( valueOf.getReturnType().equals( boxedPropertyType )
508                                      || valueOf.getReturnType().equals( unboxedPropertyType ) ) )
509                        {
510                            setterMethod.invoke( object, valueOf.invoke( null, propertyValue ) );
511                            break invocation;
512                        }
513                    }
514                    catch ( final NoSuchMethodException e2 )
515                    {
516                        if ( context.isLoggable( Level.FINEST ) )
517                        {
518                            context.log( Level.FINEST, null, e2 );
519                        }
520                    }
521
522                    throw new ModelException( getMessage( "unsupportedPropertyType", object.getClass().getName(),
523                                                          propertyName, propertyType.getName() ) );
524
525                }
526            }
527            else
528            {
529                setterMethod.invoke( object, (Object) null );
530            }
531        }
532        catch ( final IllegalAccessException e )
533        {
534            throw new ModelException( getMessage( "failedSettingProperty", propertyName, object.toString(),
535                                                  object.getClass().getName() ), e );
536
537        }
538        catch ( final InvocationTargetException e )
539        {
540            throw new ModelException( getMessage( "failedSettingProperty", propertyName, object.toString(),
541                                                  object.getClass().getName() ), e );
542
543        }
544        catch ( final InstantiationException e )
545        {
546            throw new ModelException( getMessage( "failedSettingProperty", propertyName, object.toString(),
547                                                  object.getClass().getName() ), e );
548
549        }
550    }
551
552    private static String getMessage( final String key, final Object... arguments )
553    {
554        return MessageFormat.format( ResourceBundle.getBundle(
555            DefaultServiceFactory.class.getName().replace( '.', '/' ) ).getString( key ), arguments );
556
557    }
558
559}