dataGeneratorsByName,
            Application application,
            Long applicationId,
            Long disabledPathsId)
    {
        this.dataExtractorsByName = dataExtractorsByName;
        this.dataGeneratorsByName = dataGeneratorsByName;
        this.application = application;
        this.applicationName = application.getName();
        this.applicationKey = application.getKey();
        this.applicationId = applicationId;
        this.disabledPathsId = disabledPathsId;
        
        buildAuditPaths(application);
    }
    
    @Override
    public int hashCode()
    {
        return applicationName.hashCode();
    }
    
    @Override
    public boolean equals(Object obj)
    {
        if (this == obj)
        {
            return true;
        }
        else if (obj instanceof AuditApplication)
        {
            AuditApplication that = (AuditApplication) obj;
            return this.applicationName.equals(that.applicationName);
        }
        else
        {
            return false;
        }
    }
    
    @Override
    public String toString()
    {
        StringBuilder sb = new StringBuilder(56);
        sb.append("AuditApplication")
          .append("[ name=").append(applicationName)
          .append(", id=").append(applicationId)
          .append(", disabledPathsId=").append(disabledPathsId)
          .append("]");
        return sb.toString();
    }
    
    /**
     * Get the application name
     */
    public String getApplicationName()
    {
        return applicationName;
    }
    /**
     * Get the key (root path) for the application
     */
    public String getApplicationKey()
    {
        return applicationKey;
    }
    
    /**
     * Get the database ID for this application
     */
    public Long getApplicationId()
    {
        return applicationId;
    }
    /**
     * Get the property representing the set of disabled paths for the application
     * 
     * @return          Returns an ID of disabled paths
     */
    public Long getDisabledPathsId()
    {
        return disabledPathsId;
    }
    /**
     * Helper method to check that a path is correct for this application instance
     * 
     * @param path              the path in format /app-key/x/y/z
     * @throws AuditModelException      if the path is invalid
     * 
     * @see #AUDIT_PATH_PATTERN
     */
    public void checkPath(String path)
    {
        checkPathFormat(path);
        if (path == null || path.length() == 0)
        {
            generateException(path, "Empty or null audit path");
        }
        else if (!AUDIT_PATH_PATTERN.matcher(path).matches())
        {
            generateException(
                    path,
                    "An audit must match regular expression: " + AUDIT_PATH_PATTERN);
        }
        else if (path.indexOf(applicationKey, 0) != 1)
        {
            generateException(
                    path,
                    "An audit path's first element must be the application's key i.e. '" + applicationKey + "'.");
        }
    }
    
    /**
     * Helper method to check that a path is correct for this application instance
     * 
     * @param path              the path in format /app-key/x/y/z
     * @throws AuditModelException      if the path is invalid
     * 
     * @see #AUDIT_PATH_PATTERN
     */
    public static void checkPathFormat(String path)
    {
        if (path == null || path.length() == 0)
        {
            throw new AuditModelException("Empty or null audit path: " + path);
        }
        else if (!AUDIT_PATH_PATTERN.matcher(path).matches())
        {
            throw new AuditModelException(
                        "Audit path '" + path + "' does not match regular expression: " + AUDIT_PATH_PATTERN);
        }
    }
    
    /**
     * Compile a path or part of a path into a single string which always starts with the
     * {@link #AUDIT_PATH_SEPARATOR}.  This can be a relative path so need not always start with
     * the application root key.
     * 
     * If the path separator is present at the beginning of a path component, then it is not added,
     * so "/a", "b", "/c" becomes "/a/b/c" allowing path to be appended
     * to other paths.
     * 
     * The final result is checked against a {@link #AUDIT_PATH_PATTERN regular expression} to ensure
     * it is valid.
     * 
     * @param pathComponents      the elements of the path e.g. "a", "b", "c".
     * @return                  Returns the compiled path e.g "/a/b/c".
     */
    public static String buildPath(String ... pathComponents)
    {
        StringBuilder sb = new StringBuilder(pathComponents.length * 10);
        for (String pathComponent : pathComponents)
        {
            if (!pathComponent.startsWith(AUDIT_PATH_SEPARATOR))
            {
                sb.append(AUDIT_PATH_SEPARATOR);
            }
            sb.append(pathComponent);
        }
        String path = sb.toString();
        // Check the path format
        if (!AUDIT_PATH_PATTERN.matcher(path).matches())
        {
            StringBuffer msg = new StringBuffer();
            msg.append("The audit path is invalid and must be matched by regular expression: ").append(AUDIT_PATH_PATTERN).append("\n")
               .append("   Path elements: ");
            for (String pathComponent : pathComponents)
            {
                msg.append(pathComponent).append(", ");
            }
            msg.append("\n")
               .append("   Result:        ").append(path);
            throw new AuditModelException(msg.toString());
        }
        // Done
        return path;
    }
    
    /**
     * @param path              the audit path for form /abc/def
     * @return                  the root key of form abc
     */
    public static String getRootKey(String path)
    {
        if (!path.startsWith(AUDIT_PATH_SEPARATOR))
        {
            throw new AuditModelException(
                    "The path must start with the path separator '" + AUDIT_PATH_SEPARATOR + "'");
        }
        String rootPath;
        int index = path.indexOf(AUDIT_PATH_SEPARATOR, 1);
        if (index > 0)
        {
            rootPath = path.substring(1, index);
        }
        else
        {
            rootPath = path.substring(1);
        }
        // Done
        return rootPath;
    }
    
    /**
     * Utility class carrying information around a {@link DataExtractor}.
     * 
     * @author Derek Hulley
     * @since 3.4
     */
    public static class DataExtractorDefinition
    {
        private final String dataTrigger;
        private final String dataSource;
        private final String dataTarget;
        private final DataExtractor dataExtractor;
        /**
         * @param dataTrigger           the data path that must exist for this extractor to be triggered
         * @param dataSource            the path to get data from
         * @param dataTarget            the path to write data to
         * @param dataExtractor         the implementation to use
         */
        public DataExtractorDefinition(String dataTrigger, String dataSource, String dataTarget, DataExtractor dataExtractor)
        {
            this.dataTrigger = dataTrigger;
            this.dataSource = dataSource;
            this.dataTarget = dataTarget;
            this.dataExtractor = dataExtractor;
        }
        /**
         * The data path that must exist for the extractor to be triggered.
         */
        public String getDataTrigger()
        {
            return dataTrigger;
        }
        public String getDataSource()
        {
            return dataSource;
        }
        public String getDataTarget()
        {
            return dataTarget;
        }
        public DataExtractor getDataExtractor()
        {
            return dataExtractor;
        }
    }
    
    /**
     * Get all data extractors applicable to this application.
     * 
     * @return                  Returns all data extractors contained in the application
     */
    public List getDataExtractors()
    {
        List extractors = Collections.unmodifiableList(dataExtractors);
        
        // Done
        if (logger.isDebugEnabled())
        {
            logger.debug(
                    "Looked up data extractors: \n" +
                    "   Found: " + extractors);
        }
        return extractors;
    }
    
    /**
     * Get all data generators applicable to a given path and scope.
     * 
     * @param path              the audit path
     * @return                  Returns all data generators mapped to their key-path
     */
    public Map getDataGenerators(String path)
    {
        return getDataGenerators(Collections.singleton(path));
    }
    
    /**
     * Get all data generators applicable to a given path and scope.
     * 
     * @param paths             the audit paths
     * @return                  Returns all data generators mapped to their key-path
     */
    public Map getDataGenerators(Set paths)
    {
        Map amalgamatedGenerators = new HashMap(13);
        for (String path : paths)
        {
            Map generators = dataGenerators.get(path);
            if (generators != null)
            {
                // Copy values to combined map
                amalgamatedGenerators.putAll(generators);
            }
        }
        
        // Done
        if (logger.isDebugEnabled())
        {
            logger.debug(
                    "Looked up data generators: \n" +
                    "   Paths:  " + paths + "\n" +
                    "   Found: " + amalgamatedGenerators);
        }
        return amalgamatedGenerators;
    }
    
    /**
     * Internal helper method to kick off generator and extractor path mappings
     */
    private void buildAuditPaths(AuditPath auditPath)
    {
        buildAuditPaths(
                auditPath,
                null,
                new HashSet(37),
                new HashMap(13));
    }
    /**
     * Recursive method to build generator and extractor mappings
     */
    private void buildAuditPaths(
            AuditPath auditPath,
            String currentPath,
            Set existingPaths,
            Map upperGeneratorsByPath)
    {
        // Clone the upper maps to prevent pollution
        upperGeneratorsByPath = new HashMap(upperGeneratorsByPath);
        
        // Append the current audit path to the current path
        if (currentPath == null)
        {
            currentPath = AuditApplication.buildPath(auditPath.getKey());
        }
        else
        {
            currentPath = AuditApplication.buildPath(currentPath, auditPath.getKey());
        }
        // Make sure we have not processed it before
        if (!existingPaths.add(currentPath))
        {
            generateException(currentPath, "The audit path already exists.");
        }
        
        // Get the data extractors declared for this key
        for (RecordValue element : auditPath.getRecordValue())
        {
            String key = element.getKey();
            String extractorPath = AuditApplication.buildPath(currentPath, key);
            if (!existingPaths.add(extractorPath))
            {
                generateException(extractorPath, "The audit path already exists.");
            }
            
            String extractorName = element.getDataExtractor();
            DataExtractor extractor = dataExtractorsByName.get(extractorName);
            if (extractor == null)
            {
                generateException(extractorPath, "No data extractor exists for name: " + extractorName);
            }
            // The extractor may pull data from somewhere else
            String sourcePath = element.getDataSource();
            if (sourcePath == null)
            {
                sourcePath = currentPath;
            }
            // The extractor may be triggered by data from elsewhere
            String dataTrigger = element.getDataTrigger();
            if (dataTrigger == null)
            {
                dataTrigger = currentPath;
            }
            // Store the extractor definition
            DataExtractorDefinition extractorDef = new DataExtractorDefinition(dataTrigger, sourcePath, extractorPath, extractor);
            dataExtractors.add(extractorDef);
        }
        // Get the data generators declared for this key
        for (GenerateValue element : auditPath.getGenerateValue())
        {
            String key = element.getKey();
            String generatorPath = AuditApplication.buildPath(currentPath, key);
            if (!existingPaths.add(generatorPath))
            {
                generateException(generatorPath, "The audit path already exists.");
            }
            
            String generatorName = element.getDataGenerator();
            DataGenerator generator = dataGeneratorsByName.get(generatorName);
            if (generator == null)
            {
                generateException(generatorPath, "No data generator exists for name: " + generatorName);
            }
            // All generators that occur earlier in the path will also be applicable here
            upperGeneratorsByPath.put(generatorPath, generator);
        }
        // All the generators apply to the current path
        dataGenerators.put(currentPath, upperGeneratorsByPath);
        
        // Find all sub audit paths and recurse
        for (AuditPath element : auditPath.getAuditPath())
        {
            buildAuditPaths(element, currentPath, existingPaths, upperGeneratorsByPath);
        }
    }
    
    private void generateException(String path, String msg) throws AuditModelException
    {
        throw new AuditModelException("" +
                msg + "\n" +
                "   Application: " + applicationName + "\n" +
                "   Path:        " + path);
    }
    
    /**
     * Returns {@code true} if the application name has a prefix of {@code "PreCallData"}
     * that indicates that the only purpose of the Application is to generate data to be
     * passed to a post call audit application. In this situation the application's
     * audit data is not audited. This allows the post audit application to have access to
     * 'before' values including those created by extractors and generators. Some of which
     * will not be available (for example the node has been deleted) or will have changed
     * as a result of the call. 
     */
    public boolean isApplicationJustGeneratingPreCallData()
    {
        return applicationName != null && applicationName.startsWith(AUDIT_APPLICATION_PREFIX_FOR_PRE_DATA);
    }
}