001    /*
002     * $Id: ExceptionFactory.java,v 1.11 2010/07/17 12:04:48 oboehm Exp $
003     *
004     * Copyright (c) 2009 by Oliver Boehm
005     *
006     * Licensed under the Apache License, Version 2.0 (the "License");
007     * you may not use this file except in compliance with the License.
008     * 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     * (c)reated 05.03.2009 by oliver (ob@aosd.de)
019     */
020    package patterntesting.exception;
021    
022    import javax.management.JMException;
023    
024    import org.apache.commons.logging.*;
025    import org.aspectj.lang.JoinPoint;
026    import org.aspectj.lang.reflect.CodeSignature;
027    
028    import patterntesting.runtime.jmx.MBeanHelper;
029    import patterntesting.runtime.util.*;
030    
031    /**
032     * This class can create for testing purpose any desired checked exception.
033     * Implemented with the help of Thomas.Darimont@web.de (via mailing list of
034     * aspectj-users@eclipse.org).
035     * <br/>
036     * It is realized as Singleton to be able to extract an MBean interface for the
037     * use via JMX.
038     * <br/>
039     * The exceptions are controlled by AbstractTestExceptionFactory.
040     * You will see the ExceptionFactory not before the first
041     * method marked as @TestException has finished (because it is realized as
042     * after advice). If you want to see it before
043     * call <tt>ExceptionFactory.getInstance()</tt> (with the creation of
044     * the instance it is also registered as MBean).
045     *
046     * @author <a href="boehm@javatux.de">oliver</a>
047     * @since 05.03.2009
048     * @version $Revision: 1.11 $
049     */
050    public final class ExceptionFactory implements ExceptionFactoryMBean {
051    
052        private static final Log log = LogFactory.getLog(ExceptionFactory.class);
053        private static final ExceptionFactory instance;
054        /** number of provoked exceptions. */
055        private long numberOfProvoked = 0L;
056        /** maximal number of provoked exceptions. */
057        private long maxNumberOfProvoked = 0L;
058            /** the last provoked exception which was thrown. */
059        private Throwable lastProvoked;
060        /** only classes and subclasses of 'limitedTo' can be provoked. */
061        private Class<? extends Throwable> limitedTo = Throwable.class;
062        /** to exception to be fired */
063        private Class<? extends Throwable> fire = null;
064        /** the scope can be limited to a single class. */
065        private Class<?> scope;
066    
067        static {
068            instance = new ExceptionFactory();
069            try {
070                if (Assertions.enabled) {
071                    instance.registerMeAsMBean();
072                    if (log.isDebugEnabled()) {
073                        log.debug("ExceptionFactory registered as MBean");
074                    }
075                } else {
076                    if (log.isDebugEnabled()) {
077                        log.debug("ExceptionFactory is disabled (no assertions)");
078                    }
079                }
080                    } catch (JMException e) {
081                            log.info("can't register " + instance + " as MBean", e);
082                    }
083        }
084    
085        private ExceptionFactory() {
086            if (log.isTraceEnabled()) {
087                    log.trace(this + " created");
088            }
089        }
090    
091        /**
092         * We implement registerAsMBean ourself and not via MBeanRegistry
093         * interface because we don't need the other methods declared in this
094         * interface. And because the Ajdoc generation via maven failed
095         * (it does not recognize patterntesting-rt as AspectJ lib).
096         *
097         * @throws JMException if it can't be registered to JMX
098         */
099        private void registerMeAsMBean() throws JMException {
100            MBeanHelper.registerMBean(this);
101        }
102    
103        /**
104         * Normally the ExceptionFactory register itself at JMX. But only if the
105         * class is loaded. To force the classloader to load the ExceptionFactory
106         * you can use the getInstance() method or this method here.
107         *
108         * @throws JMException if it can't be registerd at JMX
109         * @since 1.0
110         */
111        public static void registerAsMBean() throws JMException {
112            ExceptionFactory factory = ExceptionFactory.getInstance();
113            synchronized (factory) {
114                if (!MBeanHelper.isRegistered(factory)) {
115                    factory.registerMeAsMBean();
116                }
117            }
118        }
119    
120        /**
121         * It is realized as singleton because of the MBean interface.
122         *
123         * @return the only instance
124         */
125        public static ExceptionFactory getInstance() {
126            return instance;
127        }
128    
129        /**
130         * Gets the maximal number of provoked exceptions
131         * (default is Long.MAX_VALUE).
132         *
133         * @return the number of provoked exceptions
134         * @see ExceptionFactoryMBean#getMaxNumberOfProvoked()
135         */
136        public synchronized long getMaxNumberOfProvoked() {
137            return this.maxNumberOfProvoked;
138        }
139    
140        /**
141         * Gets the number of provoked exceptions.
142         *
143         * @return the number of provoked exceptions
144         * @see ExceptionFactoryMBean#getNumberOfProvoked()
145         */
146        public synchronized long getNumberOfProvoked() {
147            return this.numberOfProvoked;
148        }
149    
150        /**
151         * Limit the number of provoked maximal total number of provoked exceptions
152         * to n (default is Long.MAX_VALUE).
153         *
154         * @param n the maximal number of provoked exceptions
155         * @see ExceptionFactoryMBean#setMaxNumberOfProvoked(long)
156         */
157        public synchronized void setMaxNumberOfProvoked(final long n) {
158            this.maxNumberOfProvoked = n;
159        }
160    
161        /**
162         * Checks if is active.
163         *
164         * @return true if active
165         * @see ExceptionFactoryMBean#isActive()
166         */
167        public synchronized boolean isActive() {
168                    return this.maxNumberOfProvoked > this.numberOfProvoked;
169            }
170    
171        /**
172         * You can only provoke Exceptions if the active flag is set.
173         *
174         * @param active true or false
175             * @see ExceptionFactoryMBean#setActive(boolean)
176             */
177        public synchronized void setActive(final boolean active) {
178                    this.maxNumberOfProvoked = active ? Long.MAX_VALUE
179                    : this.numberOfProvoked;
180            }
181    
182        /**
183         * Activate.
184         *
185         * @see ExceptionFactoryMBean#activate()
186         */
187        public synchronized void activate() {
188                    this.setActive(true);
189            }
190    
191        /**
192         * Activate once.
193         *
194         * @see ExceptionFactoryMBean#activateOnce()
195         */
196        public synchronized void activateOnce() {
197            this.maxNumberOfProvoked = this.numberOfProvoked + 1;
198        }
199    
200        /**
201         * Deactivate.
202         *
203         * @see ExceptionFactoryMBean#deactivate()
204         */
205        public synchronized void deactivate() {
206                    this.setActive(false);
207            }
208    
209            /**
210         * To see if the (Sun) Java-VM was called with the option "-ea"
211         * ("enable asserts") this getter shows it.
212         *
213         * @return true if asserts are enabled
214             * @see ExceptionFactoryMBean#isAssertsEnabled()
215             */
216            public boolean isAssertsEnabled() {
217                    return Assertions.enabled;
218            }
219    
220            /**
221         * Gets the last provoked exception.
222         *
223         * @return the last exception which was thrown by one of the provoke methods
224             * @see ExceptionFactoryMBean#getLastProvoked()
225             */
226            public synchronized Throwable getLastProvoked() {
227                    return lastProvoked;
228            }
229    
230            /**
231         * Only exceptions of this returned type (or subclasses of it) are thrown.
232         * The return value is a String because of the use as MBean (works better
233         * in the JConsole).
234         *
235         * @return the limitedTo (e.g. "java.lang.Throwable")
236         * @deprecated replaced by {@link #getFire()} (will be removed with 1.2)
237             */
238            public synchronized String getLimitedTo() {
239                    return limitedTo.getName();
240            }
241    
242        /**
243         * You want to limit the provoked exceptions to IOException (or subclass)?
244         * Then give it as parameter to this setter method.
245         * </br>
246         * If you want to reset it and allow all Exceptions call
247         * <tt>setLimitedTo(Trowable.class)</tt>.
248         *
249         * @param limitedTo
250         *            the limitedTo to set (e.g. IOException.class)
251         * @deprecated replaced by {@link #setFire(String)} (will be removed with 1.2)
252         */
253        public synchronized void setLimitedTo(
254                final Class<? extends Throwable> limitedTo) {
255            this.limitedTo = limitedTo;
256        }
257    
258            /**
259         * You want to limit the provoked exceptions to IOException (or subclass)?
260         * Then give it as parameter to this setter method.
261         * </br>
262         * If you want to reset it and allow all Exceptions call
263         * <tt>setLimitedTo("java.lang.Throwable")</tt>.
264         * <br>
265         * For better use with the JConsole the class could given as String.
266         * But don't forget to give the complete classname (with package) as
267         * String.
268         *
269         * @param limitedTo e.g. "java.io.IOException"
270         * @throws ClassNotFoundException the class not found exception
271         * @deprecated replaced by {@link #setFire(String)} (will be removed with 1.2)
272             */
273            @SuppressWarnings("unchecked")
274        public synchronized void setLimitedTo(final String limitedTo)
275                throws ClassNotFoundException {
276            this.limitedTo = (Class<? extends Throwable>) Class.forName(limitedTo);
277        }
278    
279            /**
280             * "Not limited" means, limitedTo is a super class of the given type.
281             * @param type the class type
282             * @return true or false
283             */
284            // TODO remove in v1.2
285            private synchronized boolean isNotLimited(final Class<?> type) {
286                    if (type.equals(limitedTo)) {
287                            return true;
288                    }
289                    if (type.equals(Throwable.class)) {
290                            return false;
291                    }
292                    return isNotLimited(type.getSuperclass());
293            }
294            
295            /**
296             * "can be fired" means, that the given type is a subclass of "fire".
297             * @param type the exception type
298             * @return true or false
299             */
300            private boolean canBeFired(final Class<? extends Throwable> type) {
301    //          return this.fire.isAssignableFrom(type);
302                if (this.fire == null) {
303                    return true;
304                }
305            return type.isAssignableFrom(this.fire);
306            }
307    
308        /**
309         * Be careful - you can provoke any Exception with the method without the
310         * need to declare it with a throws statement. For example
311         * <pre>provoke(IOException.class)</pre>
312         * would throw an IOException.
313         *
314         * @param type e.g. IOException.class
315         * @see ExceptionThrower#provoke(Class)
316         */
317        public synchronized void provoke(final Class<? extends Throwable> type) {
318            if (Assertions.enabled && this.isActive() && this.isNotLimited(type)
319                    && this.canBeFired(type)) {
320                fire(type);
321            } else {
322                    if (log.isTraceEnabled()) {
323                    log.trace("active flag not set or " + this.getFire()
324                            + " cannot be fired here");
325                    }
326            }
327        }
328    
329        private void fire(final Class<? extends Throwable> type) {
330            this.numberOfProvoked++;
331            if (this.fire == null) {
332                ExceptionThrower.provoke(type);
333            } else {
334                ExceptionThrower.provoke(this.fire);
335            }
336        }
337        
338        /**
339         * This methods throws one of the exception which is possible for the given
340         * joinpoint. But only if the joinpoint matches one of the registered
341         * objects, classes or threads.
342         * 
343         * @param jp the joinpoint for which an exception should be provoked
344         */
345        @SuppressWarnings("unchecked")
346        public synchronized void provokeFor(JoinPoint jp) {
347            if ((this.scope == null) || this.matchScope(jp.getThis())) {
348                CodeSignature sig = (CodeSignature) jp.getSignature();
349                this.provokeOneOf(sig.getExceptionTypes());
350            } else {
351                if (log.isDebugEnabled()) {
352                    log.debug("no excecption provoked for "
353                            + JoinPointHelper.getAsShortString(jp)
354                            + " because scope = " + this.getScope());
355                }
356            }
357        }
358        
359        private boolean matchScope(Object target) {
360            return matchScope(target.getClass());
361        }
362        
363        /**
364         * Checks if a scope is limited to the class itself. Subclasses and
365         * interfaces are also supported as parameter.
366         * 
367         * @param target the target class
368         * @return true if target and scope are the same class
369         */
370        private boolean matchScope(Class<?> target) {
371            return this.scope.isAssignableFrom(target);
372        }
373        
374        /**
375         * This method throws the first given Throwable type. If this fails the
376         * next element array is tried to be created as Throwable.
377         * <br/>
378         * You can only provoke an Exception if the active flag is set.
379         * <br/>
380         *
381         * @param types a class array with exception types
382         */
383        protected synchronized void provokeOneOf(final Class<? extends Throwable>[] types) {
384            if (this.isActive()) {
385                    for (int i = 0; i < types.length; i++) {
386                            if (isNotLimited(types[i]) && this.canBeFired(types[i])) {
387                                fire(types[i]);
388                                break;
389                            }
390                    }
391            } else {
392                    if (log.isTraceEnabled()) {
393                    log.trace("active flag not set or not a subclass of "
394                            + this.limitedTo + " -> no "
395                            + Converter.toString(types) + " thrown");
396                    }
397            }
398        }
399    
400        /**
401         * To limit the exception to be thrown for a given class you can use
402         * this method here.
403         * <br/>
404         * This setter is not part of the ExceptionFactoryMBean interface because
405         * JMX allows only one setter for an attribute. Otherwise you'll get an
406         * <i>javax.management.NotCompliantMBeanException:
407         * Attribute Scope has more than one setter</i>.
408         *
409         * @param target the target
410         * @see patterntesting.exception.ExceptionFactoryMBean#setScope(java.lang.String)
411         */
412        public synchronized void setScope(final Class<?> target) {
413            this.scope = target;
414        }
415    
416        /**
417         * To limit the exception to be thrown for a given class you can use
418         * this method here.
419         * <br/>
420         * A string is excepted instead of a Class object because of the jconsole.
421         * With the jconsole only basic types or strings are available as input
422         * fields.
423         * 
424         * @since 1.1
425         * @param classname for which an exception should be thrown
426         * @see patterntesting.exception.ExceptionFactoryMBean#setScope(java.lang.String)
427         */
428        public synchronized void setScope(final String classname) {
429            try {
430                Class<?> clazz = Class.forName(classname);
431                this.setScope(clazz);
432            } catch (ClassNotFoundException e) {
433                throw new IllegalArgumentException(classname + " not found", e);
434            }
435        }
436        
437        /**
438         * To set the scope back to "all classes" use this method here.
439         * 
440         * @since 1.1
441         */
442        public synchronized void resetScope() {
443            this.scope = null;
444            
445        }
446    
447        /**
448         * Returns the scope for which the exceptions will be thrown.
449         * The default value is "all classes" if the scope is not set.
450         * 
451         * @since 1.1
452         * @return class for which an exception will be thrown.
453         * @see patterntesting.exception.ExceptionFactoryMBean#getScope()
454         */
455        public synchronized String getScope() {
456            if (this.scope == null) {
457                return ALL_CLASSES;
458            }
459            return this.scope.getName();
460        }
461    
462        /**
463         * Only exceptions of this returned type will be fired.
464         * The return value is a String because of the use as MBean (works better
465         * in the JConsole).
466         *
467         * @return the exeption to be fired (e.g. "java.lang.Throwable")
468         * @see patterntesting.exception.ExceptionFactoryMBean#getFire()
469         * @since 1.1
470         */
471        public synchronized String getFire() {
472            if (this.fire == null) {
473                return "all exceptions";
474            }
475            return this.fire.getName();
476        }
477    
478        /**
479         * You want to provoked an SocketException whenever it is possible?
480         * Then give it as parameter to this setter method.
481         * <br>
482         * For better use with the JConsole the class could given as String.
483         * But don't forget to give the complete classname (with package) as
484         * String.
485         *
486         * @param classname e.g. "java.net.SocketException"
487         * @throws ClassNotFoundException if parameter is not a class name
488         * @see patterntesting.exception.ExceptionFactoryMBean#setFire(java.lang.String)
489         * @since 1.1
490         */
491        @SuppressWarnings("unchecked")
492        public synchronized void setFire(String classname) throws ClassNotFoundException {
493            this.fire = (Class<? extends Throwable>) Class.forName(classname);
494        }
495    
496        /**
497         * You want to provoked an SocketException whenever it is possible?
498         * Then give it as parameter to this setter method.
499         *
500         * @param fire e.g. SocketException.class
501         * @since 1.1
502         */
503        public synchronized void setFire(Class<? extends Throwable> fire) {
504            this.fire = fire;
505        }
506    
507        /**
508         * Allows again that all exceptions would be fired.
509         * @see patterntesting.exception.ExceptionFactoryMBean#resetFire()
510         * @since 1.1
511         */
512        public synchronized void resetFire() {
513            this.fire = null;
514        }
515    
516        /**
517         * Resets all preferences and deactivates ExceptionFactory.
518         * @see patterntesting.exception.ExceptionFactoryMBean#reset()
519         * @since 1.1
520         */
521        public synchronized void reset() {
522            this.resetScope();
523            this.resetFire();
524            this.deactivate();
525            this.limitedTo = Throwable.class;
526        }
527    
528    }