class JobLogFormatter extends Formatter {
        @Override
        public String format(LogRecord record) {
            StringBuilder sb = new StringBuilder();
            sb
              .append(new DateTime(record.getMillis()))
              .append(" ")
              .append(record.getLevel())
              .append(" ")
              .append(record.getMessage())
              .append("\n");
            return  sb.toString();
        }
    }

    /**
     * Return all config files included via 'import' statements in the
     * primary config (or other included configs). 
     * 
     * @param xml File to examine
     * @return List&lt;File&gt; of all transitively-imported Files
     */
    @SuppressWarnings("unchecked")
    public List<File> getImportedConfigs(File xml) {
        List<File> imports = new LinkedList<File>(); 
        Document doc = getDomDocument(xml);
        if(doc==null) {
            return ListUtils.EMPTY_LIST;
        }
        NodeList importElements = doc.getElementsByTagName("import");
        for(int i = 0; i < importElements.getLength(); i++) {
            File imported = new File(
                    getJobDir(),
                    importElements.item(i).getAttributes().getNamedItem("resource").getTextContent());
            imports.add(imported);
            imports.addAll(getImportedConfigs(imported));
        }
        return imports; 
    }
    
    /**
     * Return all known ConfigPaths, as an aid to viewing or editing.
     * 
     * @return all ConfigPaths known to the ApplicationContext, in a 
     * map by name, or an empty map if no ApplicationContext
     */
    @SuppressWarnings("unchecked")
    public synchronized Map<String, ConfigPath> getConfigPaths() {
        if(ac==null) {
            return MapUtils.EMPTY_MAP;
        }
        ConfigPathConfigurer cpc = 
            (ConfigPathConfigurer)ac.getBean("configPathConfigurer");
        return cpc.getAllConfigPaths();        
    }

    /**
     * Compute a path relative to the job directory for all contained 
     * files, or null if the File is not inside the job directory. 
     * 
     * @param f File
     * @return path relative to the job directory, or null if File not 
     * inside job dir
     */
    public String jobDirRelativePath(File f) {
        try {
            String filePath = f.getCanonicalPath();
            String jobPath = getJobDir().getCanonicalPath();
            if(filePath.startsWith(jobPath)) {
                String jobRelative = filePath.substring(jobPath.length()).replace(File.separatorChar, '/');
                if(jobRelative.startsWith("/")) {
                    jobRelative = jobRelative.substring(1); 
                }
                return jobRelative;
            }
        } catch (IOException e) {
            getJobLogger().log(Level.WARNING,"bad file: "+f);
        }
        return null; 
    }

    /** 
     * Log note of all ApplicationEvents.
     * 
     * @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent)
     */
    public void onApplicationEvent(ApplicationEvent event) {
        if(event instanceof CrawlStateEvent) {
            getJobLogger().log(Level.INFO, ((CrawlStateEvent)event).getState() + 
                    (ac.getCurrentLaunchId() != null ? " " + ac.getCurrentLaunchId() : ""));
        }
        
        if (event instanceof StopCompleteEvent) {
            synchronized (this) {
                if (needTeardown) {
                    doTeardown();
                }
            }
        }
        
        if(event instanceof CheckpointSuccessEvent) {
            getJobLogger().log(Level.INFO, "CHECKPOINTED "+((CheckpointSuccessEvent)event).getCheckpoint().getName());
        }
    }

    /**
     * Is it reasonable to offer a launch button
     * @return true if launchable
     */
    public boolean isLaunchable() {<FILL_FUNCTION_BODY>

        if (!hasApplicationContext()) {
            // ok to try launch if not yet built
            return true; 
        }
        if (!hasValidApplicationContext()) {
            // never launch if specifically invalid
            return false;
        }
        // launchable if cc not yet instantiated or not yet started
        CrawlController cc = getCrawlController();        
        return cc == null || !cc.hasStarted();
    
<no_super_class>