/* * HeadsUp Agile * Copyright 2009-2013 Heads Up Development Ltd. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.headsupdev.agile.app.ci; import org.headsupdev.agile.api.rest.Api; import org.headsupdev.agile.app.ci.builders.BuildHandlerFactory; import org.headsupdev.agile.app.ci.rest.BuildStatusApi; import org.headsupdev.support.java.FileUtil; import org.headsupdev.agile.app.ci.irc.BuildCommand; import org.headsupdev.agile.storage.StoredProject; import org.headsupdev.agile.storage.hibernate.IdProjectId; import org.headsupdev.agile.storage.ci.Build; import org.headsupdev.agile.storage.HibernateStorage; import org.headsupdev.agile.storage.HibernateUtil; import org.headsupdev.agile.app.ci.event.BuildFailedEvent; import org.headsupdev.agile.app.ci.event.BuildSucceededEvent; import org.headsupdev.agile.app.ci.event.UploadApplicationEvent; import org.headsupdev.agile.app.ci.permission.BuildViewPermission; import org.headsupdev.agile.app.ci.permission.BuildListPermission; import org.headsupdev.agile.app.ci.permission.BuildForcePermission; import org.headsupdev.agile.web.WebApplication; import org.headsupdev.agile.api.*; import org.headsupdev.irc.IRCCommand; import org.hibernate.Session; import org.hibernate.Transaction; import org.hibernate.Query; import org.osgi.framework.BundleContext; import java.util.List; import java.util.LinkedList; import java.io.File; /** * The application descriptor for continuous integration * * @author Andrew Williams * @version $Id$ * @since 1.0 */ public class CIApplication extends WebApplication { public static final String ID = "builds"; protected static BuildHandlerFactory handlerFactory = new BuildHandlerFactory(); public static final ConfigurationItem CONFIGURATION_NOTIFY_REPEAT_PASS = new ConfigurationItem( "notify.pass.repeat", false, "Send notifications for continual success", "Set this to true if you wish every build to cause a notification, otherwise only failed " + "or newly passed builds will send notifications" ); public static final ConfigurationItem CONFIGURATION_MAVEN_HOME = new ConfigurationItem( "maven.home", "", "Maven Home (not including /bin/mvn)", "Change this parameter if you wish to use a particular version of maven " + "or if your maven installation is not in the system path" ); // { // public boolean test( String value ) // { // File mvn = new File( new File( value, "bin" ), "mvn" ); // return mvn.exists(); // } // } ); public static final ConfigurationItem CONFIGURATION_ANT_HOME = new ConfigurationItem( "ant.home", "", "Ant Home Directory", "Change this parameter if you wish to use a particular version of ant " + "or if your ant installation is not in the system path" ); public static final ConfigurationItem CONFIGURATION_GRADLE_HOME = new ConfigurationItem( "gradle.home", "", "Gradle Home Directory", "Change this parameter if you wish to use a particular version of gradle " + "or if your gradle installation is not in the system path" ); public static final ConfigurationItem CONFIGURATION_ECLIPSE_HOME = new ConfigurationItem( "eclipse.home", "", "Eclipse home directory", "The directory where Eclipse is installed" ); public static final ConfigurationItem CONFIGURATION_ANT_TASKS = new ConfigurationItem( "ant.tasks", "", "Ant build tasks", "A space separated list of ant tasks to run when building" ); public static final ConfigurationItem CONFIGURATION_GRADLE_TASKS = new ConfigurationItem( "gradle.tasks", "", "Gradle build tasks", "A space separated list of gradle tasks to run when building" ); public static final ConfigurationItem CONFIGURATION_MAVEN_GOALS = new ConfigurationItem( "maven.goals", "clean install", "Maven build goals", "A space separated list of maven goals and phases to run when building" ); public static final ConfigurationItem CONFIGURATION_MAVEN_PROFILES = new ConfigurationItem( "maven.profiles", "", "Maven build profiles", "A space separated list of maven profiles to activate when building" ); public final ConfigurationItem CONFIGURATION_MAVEN_HOME_OVERRIDE = new ConfigurationItem( "maven.home", "", "Maven Home (not including /bin/mvn)", "Use this option to specify a different maven installation for " + "this build schedule" ) { @Override public Object getDefault() { PropertyTree appConfig = CIApplication.this.getConfiguration(); return appConfig.getProperty( CONFIGURATION_MAVEN_HOME.getKey(), (String) CONFIGURATION_MAVEN_HOME.getDefault() ); } }; public final ConfigurationItem CONFIGURATION_ANT_HOME_OVERRIDE = new ConfigurationItem( "ant.home", "", "Ant Home Directory", "Use this option to specify a different ant installation for " + "this build schedule" ) { @Override public Object getDefault() { PropertyTree appConfig = CIApplication.this.getConfiguration(); return appConfig.getProperty( CONFIGURATION_ANT_HOME.getKey(), (String) CONFIGURATION_ANT_HOME.getDefault() ); } }; public final ConfigurationItem CONFIGURATION_GRADLE_HOME_OVERRIDE = new ConfigurationItem( "gradle.home", "", "Gradle Home Directory", "Use this option to specify a different gradle installation for " + "this build schedule" ) { @Override public Object getDefault() { PropertyTree appConfig = CIApplication.this.getConfiguration(); return appConfig.getProperty( CONFIGURATION_GRADLE_HOME.getKey(), (String) CONFIGURATION_GRADLE_HOME.getDefault() ); } }; public final ConfigurationItem CONFIGURATION_ECLIPSE_HOME_OVERRIDE = new ConfigurationItem( "eclipse.home", "", "Eclipse home directory", "Use this option to specify a different eclipse installation for " + "this build schedule" ) { @Override public Object getDefault() { PropertyTree appConfig = CIApplication.this.getConfiguration(); return appConfig.getProperty( CONFIGURATION_ECLIPSE_HOME.getKey(), (String) CONFIGURATION_ECLIPSE_HOME.getDefault() ); } }; public static final ConfigurationItem CONFIGURATION_COMMAND_LINE = new ConfigurationItem( "commandline", "make", "Command line", "The command used to start the build process for this project" ); public static final ConfigurationItem CONFIGURATION_XCODE_BUILD_WORKSPACE = new ConfigurationItem( "xcode.workspacebuild", false, "Build using an Xcode Workspace", "Should this configuration build using an Xcode workspace rather than a project setup" ); public static final ConfigurationItem CONFIGURATION_XCODE_WORKSPACE = new ConfigurationItem( "xcode.workspace", "", "Workspace Name", "The xcode workspace to load for this build (optional)" ); public static final ConfigurationItem CONFIGURATION_XCODE_SCHEME = new ConfigurationItem( "xcode.scheme", "", "Build Scheme (This will need to be marked as a 'shared' scheme from Xcode)", "The xcode scheme to build this project (optional - default is same as workspace)" ); public static final ConfigurationItem CONFIGURATION_XCODE_TARGET = new ConfigurationItem( "xcode.target", "", "Project Target", "The xcode target to build this project (optional)" ); public static final ConfigurationItem CONFIGURATION_XCODE_CONFIG = new ConfigurationItem( "xcode.config", "Release", "Project Configuration", "The xcode configuation to build this project" ); public static final ConfigurationItem CONFIGURATION_XCODE_SDK = new ConfigurationItem( "xcode.sdk", "", "Project SDK", "The SDK to build this project against - mainly used for testing (optional)" ); public static final ConfigurationItem CONFIGURATION_XCODE_RUN_TESTS = new ConfigurationItem( "xcode.runtests", false, "Run Tests", "Compile and run any tests defined in the project configuration (requires a scheme name)" ); public static final ConfigurationItem CONFIGURATION_ANALYZE = new ConfigurationItem( "analyze", false, "Analyze", "Check for common coding errors" ); public static final ConfigurationItem CONFIGURATION_BUILD_NAME = new ConfigurationItem( "name", "", "Configuration Name", "A name used to identify this build configuration" ); public static final ConfigurationItem CONFIGURATION_BUILD_DISABLED = new ConfigurationItem( "disabled", false, "Disable Configuration", "Disable this build from being included in any automated running" ); public static final ConfigurationItem CONFIGURATION_CRON_EXPRESSION = new ConfigurationItem( ConfigurationItem.TYPE_CRON, "cron", "0 15 23 * * ?", "Cron Expression", "A cron expression to specify when the schedule should run (use format s m h DoM M DoW) or 'never' to disable the schedule" ); private List<MenuLink> links; private List<String> eventTypes; protected List<ConfigurationItem> globalItems = new LinkedList<ConfigurationItem>(); protected List<ConfigurationItem> antProjectItems = new LinkedList<ConfigurationItem>(); protected List<ConfigurationItem> gradleProjectItems = new LinkedList<ConfigurationItem>(); protected List<ConfigurationItem> eclipseProjectItems = new LinkedList<ConfigurationItem>(); protected List<ConfigurationItem> cmdProjectItems = new LinkedList<ConfigurationItem>(); protected List<ConfigurationItem> xcodeProjectItems = new LinkedList<ConfigurationItem>(); protected List<ConfigurationItem> otherProjectItems = new LinkedList<ConfigurationItem>(); private static CIBuilder builder = new CIBuilder(); private static CIScheduler scheduler = new CIScheduler(); public static CIBuilder getBuilder() { return builder; } public static BuildHandlerFactory getHandlerFactory() { return handlerFactory; } public static void setHandlerFactory(BuildHandlerFactory handlerFactory) { CIApplication.handlerFactory = handlerFactory; } public static CIScheduler getScheduler() { return scheduler; } public void start( BundleContext bc ) { super.start( bc ); links = new LinkedList<MenuLink>(); eventTypes = new LinkedList<String>(); eventTypes.add( "buildfailed" ); eventTypes.add( "buildsucceeded" ); builder.setApplication( this ); Manager.getInstance().addProjectListener( builder ); globalItems.add( CONFIGURATION_MAVEN_HOME ); globalItems.add( CONFIGURATION_ANT_HOME ); globalItems.add( CONFIGURATION_ECLIPSE_HOME ); globalItems.add( CONFIGURATION_NOTIFY_REPEAT_PASS ); List<ConfigurationItem> items = new LinkedList<ConfigurationItem>(); items.add( CONFIGURATION_BUILD_NAME ); items.add( CONFIGURATION_ANT_TASKS ); items.add( CONFIGURATION_ANT_HOME_OVERRIDE ); items.add( CONFIGURATION_BUILD_DISABLED ); antProjectItems.add( new ConfigurationItem( "schedule.default", "Default Build Schedule", items ) ); items = new LinkedList<ConfigurationItem>(); items.add( CONFIGURATION_BUILD_NAME ); items.add( CONFIGURATION_ANT_TASKS ); items.add( CONFIGURATION_ANT_HOME_OVERRIDE ); items.add( CONFIGURATION_CRON_EXPRESSION ); items.add( CONFIGURATION_BUILD_DISABLED ); antProjectItems.add( new ConfigurationItem( "schedule", "Build Schedule", new ConfigurationItem( "schedule", "Build Schedule", items ) ) ); items = new LinkedList<ConfigurationItem>(); items.add( CONFIGURATION_BUILD_NAME ); items.add( CONFIGURATION_GRADLE_TASKS ); items.add( CONFIGURATION_GRADLE_HOME_OVERRIDE ); items.add( CONFIGURATION_BUILD_DISABLED ); gradleProjectItems.add( new ConfigurationItem( "schedule.default", "Default Build Schedule", items ) ); items = new LinkedList<ConfigurationItem>(); items.add( CONFIGURATION_BUILD_NAME ); items.add( CONFIGURATION_GRADLE_TASKS ); items.add( CONFIGURATION_GRADLE_HOME_OVERRIDE ); items.add( CONFIGURATION_CRON_EXPRESSION ); items.add( CONFIGURATION_BUILD_DISABLED ); gradleProjectItems.add( new ConfigurationItem( "schedule", "Build Schedule", new ConfigurationItem( "schedule", "Build Schedule", items ) ) ); items = new LinkedList<ConfigurationItem>(); items.add( CONFIGURATION_BUILD_NAME ); items.add( CONFIGURATION_ECLIPSE_HOME_OVERRIDE ); items.add( CONFIGURATION_BUILD_DISABLED ); eclipseProjectItems.add( new ConfigurationItem( "schedule.default", "Default Build Schedule", items ) ); items = new LinkedList<ConfigurationItem>(); items.add( CONFIGURATION_BUILD_NAME ); items.add( CONFIGURATION_ECLIPSE_HOME_OVERRIDE ); items.add( CONFIGURATION_CRON_EXPRESSION ); items.add( CONFIGURATION_BUILD_DISABLED ); eclipseProjectItems.add( new ConfigurationItem( "schedule", "Build Schedule", new ConfigurationItem( "schedule", "Build Schedule", items ) ) ); items = new LinkedList<ConfigurationItem>(); items.add( CONFIGURATION_BUILD_NAME ); items.add( CONFIGURATION_COMMAND_LINE ); items.add( CONFIGURATION_BUILD_DISABLED ); cmdProjectItems.add( new ConfigurationItem( "schedule.default", "Default Build Schedule", items ) ); items = new LinkedList<ConfigurationItem>(); items.add( CONFIGURATION_BUILD_NAME ); items.add( CONFIGURATION_COMMAND_LINE ); items.add( CONFIGURATION_CRON_EXPRESSION ); items.add( CONFIGURATION_BUILD_DISABLED ); cmdProjectItems.add( new ConfigurationItem( "schedule", "Build Schedule", new ConfigurationItem( "schedule", "Build Schedule", items ) ) ); items = new LinkedList<ConfigurationItem>(); items.add( CONFIGURATION_BUILD_NAME ); // type of build items.add( CONFIGURATION_XCODE_BUILD_WORKSPACE ); // workspace build... items.add( CONFIGURATION_XCODE_WORKSPACE ); items.add( CONFIGURATION_XCODE_SCHEME ); // ...or project build items.add( CONFIGURATION_XCODE_TARGET ); items.add( CONFIGURATION_XCODE_CONFIG ); items.add( CONFIGURATION_XCODE_SDK ) ; items.add( CONFIGURATION_XCODE_RUN_TESTS ); items.add( CONFIGURATION_ANALYZE ); items.add( CONFIGURATION_BUILD_DISABLED ); xcodeProjectItems.add( new ConfigurationItem( "schedule.default", "Default Build Schedule", items ) ); items = new LinkedList<ConfigurationItem>(); items.add( CONFIGURATION_BUILD_NAME ); // type of build items.add( CONFIGURATION_XCODE_BUILD_WORKSPACE ); // workspace build... items.add( CONFIGURATION_XCODE_WORKSPACE ); items.add( CONFIGURATION_XCODE_SCHEME ); // ...or project build items.add( CONFIGURATION_XCODE_TARGET ); items.add( CONFIGURATION_XCODE_CONFIG ); items.add( CONFIGURATION_XCODE_SDK ); items.add( CONFIGURATION_XCODE_RUN_TESTS ); items.add( CONFIGURATION_ANALYZE ); items.add( CONFIGURATION_CRON_EXPRESSION ); items.add( CONFIGURATION_BUILD_DISABLED ); xcodeProjectItems.add( new ConfigurationItem( "schedule", "Build Schedule", new ConfigurationItem( "schedule", "Build Schedule", items ) ) ); // a fallback for other project types. Allow naming and then a named schedule. // This could do anything based on the builder code loaded at the time... items = new LinkedList<ConfigurationItem>(); items.add( CONFIGURATION_BUILD_NAME ); items.add( CONFIGURATION_BUILD_DISABLED ); otherProjectItems.add( new ConfigurationItem( "schedule.default", "Default Build Schedule", items ) ); items = new LinkedList<ConfigurationItem>(); items.add( CONFIGURATION_BUILD_NAME ); items.add( CONFIGURATION_CRON_EXPRESSION ); items.add( CONFIGURATION_BUILD_DISABLED ); otherProjectItems.add( new ConfigurationItem( "schedule", "Build Schedule", new ConfigurationItem( "schedule", "Build Schedule", items ) ) ); for ( Project project : Manager.getStorageInstance().getProjects() ) { scheduler.resetProject( project ); } // find a sensible default for maven.home File maven = FileUtil.lookupGrandparentInPath( "mvn" ); if ( maven == null ) { maven = FileUtil.lookupGrandparentInPath( "mvn.bat" ); } if ( maven != null ) { CONFIGURATION_MAVEN_HOME.setDefault( maven.getAbsolutePath() ); } // find a sensible default for ant.home File ant = FileUtil.lookupParentInPath( "ant" ); if ( ant == null ) { ant = FileUtil.lookupParentInPath( "ant.bat" ); } if ( ant != null ) { CONFIGURATION_ANT_HOME.setDefault( ant.getAbsolutePath() ); } // find a sensible default for gradle.home File gradle = FileUtil.lookupParentInPath( "gradle" ); if ( gradle == null ) { gradle = FileUtil.lookupParentInPath( "gradle.bat" ); } if ( gradle != null ) { CONFIGURATION_GRADLE_HOME.setDefault( gradle.getAbsolutePath() ); } // lookup the different binaries for eclipse... File eclipse = FileUtil.lookupParentInPath( "eclipse" ); if ( eclipse == null ) { eclipse = FileUtil.lookupParentInPath( "eclipse.exe" ); } if ( eclipse != null ) { CONFIGURATION_ECLIPSE_HOME.setDefault( eclipse.getAbsolutePath() ); } } public String getName() { return "Builds"; } public String getApplicationId() { return ID; } public String getDescription() { return "The " + Manager.getStorageInstance().getGlobalConfiguration().getProductName() + " continuous integration application"; } public List<MenuLink> getLinks( Project project ) { return links; } public List<String> getEventTypes() { return eventTypes; } public Class[] getPersistantClasses() { return new Class[] { BuildFailedEvent.class, BuildSucceededEvent.class, UploadApplicationEvent.class }; } @Override public Class<? extends Page>[] getPages() { return new Class[] { CI.class, Tests.class, View.class }; } @Override public Class<? extends Api>[] getApis() { return (Class<? extends Api>[]) new Class[]{ BuildStatusApi.class }; } @Override public Class<? extends Page> getHomePage() { return CI.class; } @Override public Permission[] getPermissions() { return new Permission[] { new BuildForcePermission(), new BuildListPermission(), new BuildViewPermission() }; } @Override public LinkProvider[] getLinkProviders() { return new LinkProvider[]{ new BuildLinkProvider() }; } @Override public IRCCommand[] getIRCCommands() { return new IRCCommand[]{ new BuildCommand() }; } @Override public List<ConfigurationItem> getConfigurationItems() { return globalItems; } protected List<ConfigurationItem> getMavenProjectConfiguration( String type ) { List<ConfigurationItem> mavenItems = new LinkedList<ConfigurationItem>(); List<ConfigurationItem> items = new LinkedList<ConfigurationItem>(); items.add( CONFIGURATION_BUILD_NAME ); items.add( CONFIGURATION_MAVEN_GOALS ); items.add( CONFIGURATION_MAVEN_PROFILES ); items.add( CONFIGURATION_MAVEN_HOME_OVERRIDE ); if ( type.startsWith( "apk" ) ) { items.add( CONFIGURATION_ANALYZE ); } items.add( CONFIGURATION_BUILD_DISABLED ); mavenItems.add( new ConfigurationItem( "schedule.default", "Default Build Schedule", items ) ); items = new LinkedList<ConfigurationItem>(); items.add( CONFIGURATION_BUILD_NAME ); items.add( CONFIGURATION_MAVEN_GOALS ); items.add( CONFIGURATION_MAVEN_PROFILES ); items.add( CONFIGURATION_MAVEN_HOME_OVERRIDE ); if ( type.startsWith( "apk" ) ) { items.add( CONFIGURATION_ANALYZE ); } items.add( CONFIGURATION_CRON_EXPRESSION ); items.add( CONFIGURATION_BUILD_DISABLED ); mavenItems.add( new ConfigurationItem( "schedule", "Build Schedule", new ConfigurationItem( "schedule", "Build Schedule", items ) ) ); return mavenItems; } @Override public List<ConfigurationItem> getProjectConfigurationItems( Project project ) { if ( project instanceof MavenTwoProject ) { return getMavenProjectConfiguration( ((MavenTwoProject) project).getPackaging() ); } else if ( project instanceof AntProject ) { return antProjectItems; } else if ( project instanceof GradleProject ) { return gradleProjectItems; } else if ( project instanceof EclipseProject ) { return eclipseProjectItems; } else if ( project instanceof CommandLineProject ) { return cmdProjectItems; } else if ( project instanceof XCodeProject ) { return xcodeProjectItems; } else if ( project.equals( StoredProject.getDefault() ) ) { return new LinkedList<ConfigurationItem>(); } else { return otherProjectItems; } } @Override public void onProjectConfigurationChanged( Project project ) { scheduler.resetProject( project ); } public static List<Build> getBuildsForProject( Project project ) { Session session = ( (HibernateStorage) Manager.getStorageInstance() ).getHibernateSession(); Query q = session.createQuery( "from Build b where id.project.id = :pid order by id.id desc" ); q.setString( "pid", project.getId() ); q.setMaxResults( 25 ); return q.list(); } public List<Build> getRunningBuilds() { Session session = ( (HibernateStorage) Manager.getStorageInstance() ).getHibernateSession(); Transaction tx = session.beginTransaction(); Query q = session.createQuery( "from Build b where status = " + Build.BUILD_RUNNING ); List<Build> ret = q.list(); tx.commit(); return ret; } static public Build getBuild( long id, Project project ) { Session session = ( (HibernateStorage) Manager.getStorageInstance() ).getHibernateSession(); Query q = session.createQuery( "from Build b where id.id = :id and id.project.id = :pid" ); q.setLong( "id", id ); q.setString( "pid", project.getId() ); return (Build) q.uniqueResult(); } public static Build getLatestBuildForProject( Project project ) { Build build = null; Session session = ( (HibernateStorage) Manager.getStorageInstance() ).getHibernateSession(); Query q = session.createQuery( "from Build b where id.project.id = :pid order by id.id desc" ); q.setString( "pid", project.getId() ); q.setMaxResults( 1 ); List<Build> builds = q.list(); if ( builds.size() > 0 ) { build = builds.get( 0 ); } return build; } public static Build getLatestPassedBuildForProject( Project project ) { Build build = null; Session session = ( (HibernateStorage) Manager.getStorageInstance() ).getHibernateSession(); Query q = session.createQuery( "from Build b where id.project.id = :pid and status = " + Build.BUILD_SUCCEEDED + " order by endTime desc" ); q.setString( "pid", project.getId() ); q.setMaxResults( 1 ); List<Build> builds = q.list(); if ( builds.size() > 0 ) { build = builds.get( 0 ); } return build; } public static String getLatestChangePassed( Project project ) { Build build = getLatestPassedBuildForProject( project ); if ( build == null ) { return ""; } return build.getRevision(); } public static Build getPreviousChangePassed( Build current, Project project ) { Build build = null; Session session = ( (HibernateStorage) Manager.getStorageInstance() ).getHibernateSession(); Query q = session.createQuery( "from Build b where id.project.id = :pid and status = " + Build.BUILD_SUCCEEDED + " and startTime < :beforeDate order by endTime desc" ); q.setString( "pid", project.getId() ); q.setTimestamp( "beforeDate", current.getStartTime() ); q.setMaxResults( 1 ); List<Build> builds = q.list(); if ( builds.size() > 0 ) { build = builds.get( 0 ); } return build; } public long addBuild( Build build ) { Session session = HibernateUtil.getCurrentSession(); Transaction tx = session.beginTransaction(); IdProjectId id = (IdProjectId) session.save( build ); long ret = id.getId(); tx.commit(); return ret; } public void saveBuild( Build build ) { Session session = HibernateUtil.getCurrentSession(); Transaction tx = session.beginTransaction(); session.saveOrUpdate( build ); tx.commit(); } public static File getProjectDir( Project project ) { File appDir = new File( Manager.getStorageInstance().getDataDirectory(), "builds" ); return new File( appDir, project.getId().replace( ':', '_' ) ); } public static UploadApplicationEvent getLatestUploadEvent( Project project ) { Session session = ( (HibernateStorage) Manager.getStorageInstance() ).getHibernateSession(); Query q = session.createQuery( "from UploadApplicationEvent e where project.id = :pid order by time desc" ); q.setString( "pid", project.getId() ); q.setMaxResults( 1 ); List<UploadApplicationEvent> events = q.list(); if ( events.size() == 0 ) { return null; } return events.get( 0 ); } }