This commit is contained in:
msyu
2024-10-23 11:17:42 +03:00
commit ab5c944862
380 changed files with 55823 additions and 0 deletions
+147
View File
@@ -0,0 +1,147 @@
/**
Aaron Greenlee
http://aarongreenlee.com/
This work is licensed under a Creative Commons Attribution-Share-Alike 3.0
Unported License.
// Original Info -----------------------------------------------------------
Author : Aaron Greenlee
Created : 01/12/2011
HothReporter offers a simple Web UI to report the errors observed by Hoth.
// Modifications :---------------------------------------------------------
*/
component
name='HothReporter'
accessors=false
output="false"
{
public Hoth.HothReporter function init (HothConfig)
{
// If a config object was not provided we
// will use our default.
variables.Config = (structKeyExists(arguments, 'HothConfig'))
? arguments.HothConfig
: new Hoth.config.HothConfig();
VARIABLES._NAME = 'Hoth_' & variables.Config.getApplicationName();
variables.exceptionKeys = ['detail','type','tagcontext','stacktrace','message'];// Required exception keys
variables.logPathIsRelative = variables.Config.getLogPathIsRelative();
variables.paths.LogPath = variables.Config.getLogPathExpanded(); // Get the root location for our logging.
variables.paths.Exceptions = variables.Config.getPath('exceptions'); // Track the unique exceptions.
variables.paths.Incidents = variables.Config.getPath('incidents'); // Track the hits per exception.
variables.paths.Report = variables.Config.getPath('exceptionReport'); // The actual report
variables.paths.Activity = variables.Config.getPath('exceptionReportActivity'); // Track when we save things. Helps understand volume.
//variables.paths.Index = variables.Config.getPath('exceptionIndex'); // Tracks the exception keys to prevent duplication
return this;
}
/** Quick report. Really a work in process.
* @exception Accepts a hash value or 'all'
**/
public struct function report (required string exception)
{
if (arguments.exception=='all')
{
return generateExceptionIndex();
} else {
local.filepath = variables.paths.Exceptions & '/' & arguments.exception;
local.exception = (fileExists(local.filepath))
? fileRead(local.filepath)
: serializeJSON ({'message'="That report no longer exists."});
return deserializeJSON (local.exception);
}
}
/** Quick report. Really a work in process.
* @exception Accepts a hash value or 'all'
**/
public array function delete (required string exception)
{
local.response = [];
if (arguments.exception == 'all')
{
lock name=VARIABLES._NAME timeout=variables.Config.getTimeToLock() type="exclusive" {
directoryDelete(variables.paths.Exceptions, true);
directoryDelete(variables.paths.Incidents, true);
directoryCreate(variables.paths.Exceptions);
directoryCreate(variables.paths.Incidents);
}
} else { local.exceptionPath = variables.paths.Exceptions & '/' & arguments.exception;
local.incidentPath = variables.paths.Incidents & '/' & arguments.exception;
local.response = [];
if (fileExists(local.exceptionPath))
{
fileDelete(local.exceptionPath);
arrayAppend(local.response, "Exception file deleted.");
} else {
arrayAppend(local.response, "Exception file did not exist!");
}
if (fileExists(local.incidentPath))
{
fileDelete(local.incidentPath);
arrayAppend(local.response, "Incident record file deleted.");
} else {
arrayAppend(local.response, "Incident record file not exist!");
}
return local.response;
}
}
/** Return the report's view **/
public string function getReportView () {
local.view = fileRead(expandPath('/Hoth') & '/views/report.html');
// Replace the Hoth URL
return replaceNoCase(
local.view
,'${HOTH_REPORT_URL}'
,variables.Config.getHothReportURL());
}
// -------------------------------------------------------------------------
private struct function generateExceptionIndex() {
// Read our file system
local.exceptions = directoryList (variables.paths.Exceptions,false);
local.incidents = directoryList (variables.paths.Exceptions,false);
local.report = {};
for (i=1;i LTE ArrayLen(local.exceptions);i=i+1) {
local.instance = {};
local.instance.filename =
listLast(local.exceptions[i],'\/');
if (left(local.instance.filename, 1) != '_')
{
//local.instance.exceptionDetail =
//fileRead (local.exceptions[i]);
if (!fileExists(variables.paths.Incidents & '/' & local.instance.filename))
{
local.instances = '';
} else {
local.instances =
fileRead(variables.paths.Incidents & '/' & local.instance.filename);
}
local.instance.incidentcount = listLen(local.instances,chr(10));
// Save our report
local.report[local.instance.filename] = local.instance;
}
}
return local.report;
}
}
+264
View File
@@ -0,0 +1,264 @@
/**
Aaron Greenlee
http://aarongreenlee.com/
This work is licensed under a Creative Commons Attribution-Share-Alike 3.0
Unported License.
// Original Info -----------------------------------------------------------
Author : Aaron Greenlee
Created : 10/01/2010
HothTracker is responsible for accepting a exception, tracking the frequency
of the exception and alerting developers for new, unique exceptions.
// Modifications :---------------------------------------------------------
Modified : 12/13/2010 9:52:41 AM by Aaron Greenlee.
- Now supporting ColdBox 3.0 RC1
*/
component
name='HothTracker'
accessors=false
{
public Hoth.HothTracker function init (HothConfig) {
// If a config object was not provided we
// will use our default.
variables.Config = (structKeyExists(arguments, 'HothConfig'))
? arguments.HothConfig
: new Hoth.object.HothConfig();
VARIABLES._NAME = 'Hoth_' & variables.Config.getApplicationName();
variables.exceptionKeys = ['detail','type','tagcontext','stacktrace','message']; // Required exception keys
variables.paths.LogPath = variables.Config.getLogPathExpanded(); // Get the root location for our logging.
variables.paths.Exceptions = variables.Config.getPath('exceptions'); // Track the unique exceptions.
variables.paths.Incidents = variables.Config.getPath('incidents'); // Track the hits per exception.
variables.paths.Report = variables.Config.getPath('exceptionReport'); // The actual report
variables.paths.Activity = variables.Config.getPath('exceptionReportActivity'); // Track when we save things. Helps understand volume.
//variables.paths.Index = variables.Config.getPath('exceptionIndex'); // Tracks the exception keys to prevent duplication
verifyDirectoryStructure();
try {
// For the life of this HothTracker, keep the ApplicationManager.
variables.HothApplicationManager = new Hoth.object.HothApplicationManager(HothConfig);
// Because the entire application db file is locked, we only want to
// learn about this application when the HothTracker is created. The
// actual amount of work done within the manager is small, but, it is
// does block all other processes for all other Hoth instances, so, we
// want to reduce calls to it to this constructor.
variables.HothApplicationManager.learnApplication(arguments.HothConfig);
} catch (any e) {
//writeDump(e);abort;
}
return this;
}
/** Track an exception.
@ExceptionStructure A ColdFusion cfcatch or a supported object from a Framework or Application. */
public boolean function track (any Exception) {
local.ExceptionStructure = parseException(arguments.Exception);
// If we did not parse what we are supposed to
// track, we will abort.
if (!local.ExceptionStructure.validException)
return false;
// ------------------------------------------------------------------------------
try {
// ------------------------------------------------------------------------------
local.e = {
detail = structKeyExists(local.ExceptionStructure,'detail') ? local.ExceptionStructure.detail : '_noDetail'
,message = structKeyExists(local.ExceptionStructure,'message') ? local.ExceptionStructure.message : '_noMessage'
,stack = structKeyExists(local.ExceptionStructure,'stacktrace') ? local.ExceptionStructure.stacktrace : '_no_stacktrace'
,context = structKeyExists(local.ExceptionStructure,'tagcontext') ? local.ExceptionStructure.tagcontext : '_no_tagcontext'
,format = structKeyExists(local.ExceptionStructure,'format') ? local.ExceptionStructure.format: '_no_format'
,url = CGI.HTTP_HOST & CGI.path_info
,client = CGI.HTTP_USER_AGENT
};
// Generate JSON for hashing
local.json = {};
local.k = '';
for(local.k in local.e)
local.json[k] = serializeJSON(local.e[local.k]);
// Hash a unique key for the content of each property within the exception
local.index = {};
local.index.stack = ( len(local.e.stack) > 0) ? hash(lcase(local.e.stack),'SHA') : '_no_stack';
local.index.key = lcase(local.index.stack);
local.saveDetails = false;
// Index the exception, count occurances and save details.
// Lock is unique to the exception.
lock name=local.index.key timeout=variables.Config.getTimeToLock() type="exclusive" {
local.filename = local.index.key & '.log';
local.exceptionFile = variables.paths.Exceptions & '/' & local.filename;
local.incidentsFile = variables.paths.Incidents & '/' & local.filename;
local.exceptionIsKnown = fileExists(local.exceptionFile);
if (!local.exceptionIsKnown)
fileWrite(local.exceptionFile ,serializeJSON(local.e),'UTF-8');
// Create an incident if the file does not exist
if (!fileExists(local.incidentsFile)) {
fileWrite(local.incidentsFile ,now() & '#chr(13)#','UTF-8');
} else {
local.file = fileOpen(local.incidentsFile,'append','utf-8');
fileWriteLine(local.file, now() & '|' & local.e.url );
fileClose(local.file);
}
}
// Outside the lock, send mail if requested
if (!local.exceptionIsKnown && variables.Config.getEmailNewExceptions() ) {
local.INetAddress = createObject( 'java', 'java.net.InetAddress' );
local.url = (len(CGI.QUERY_STRING) > 0)
? CGI.http_host & CGI.path_info & '?' & Cgi.QUERY_STRING
: CGI.http_host & CGI.path_info;
local.emailBody = [
"Hoth tracked a new exception (" & local.index.key & ")."
,"Message: " & local.e.message
,"Machine Name: " & local.INetAddress.getLocalHost().getHostName()
,"Address: " & local.url
];
local.Mail = new Mail( subject='Hoth Exception (' & variables.Config.getApplicationName() & ') ' & local.index.key
,to=variables.Config.getEmailNewExceptionsTo()
,from=variables.Config.getEmailNewExceptionsFrom());
// Attach the file
if ( variables.Config.getEmailNewExceptionsFile() ) {
local.Mail.addParam(file=local.exceptionFile);
ArrayAppend( local.emailBody, "To view the attached exception info, copy and paste into FireBug's console (x = exception) and press CRTL+Enter." );
}
// Show exception as HTML inline?
if ( variables.Config.getEmailExceptionsAsHTML() ) {
local.Mail.setType( "html" );
savecontent variable="local.emailBody" {
writeOutput( arrayToList( local.emailBody, "<br />" ) );
writeOutput( "<br />Exception Details:<br />");
writeDump( local.e );
}
local.Mail.setBody( local.emailBody );
}
else {
local.Mail.setBody( arrayToList( local.emailBody, "#chr(10)##chr(13)#" ) );
}
local.mail.Send();
}
// ------------------------------------------------------------------------------
} catch (any e) {
return false;
}
// ------------------------------------------------------------------------------
return true;
}
// Private Methods Follow -------------------------------------------------------
/** Parse an exception provided by a framework or supported application.
Hoth is easy to support. Just provide a Struct with at least the following keys:
'detail,type,tagContext,StackTrace,Message'
Or an object with the same information that Hoth can extract. */
private struct function parseException(any Exception) {
local.result = { validException = false };
// Return the meta data for our passed exception
local.md = getMetaData(Exception);
// Inspect the class tree to understand what type of object
// we are working with.
local.classTree = GetClassHeirarchy(local.md);
// Was our object built with ColdFusion? If so, it is custom.
// If not, the object is considered to be native and generated by CF.
local.exceptionType = (
listContainsNoCase(local.classTree, 'coldfusion.runtime.AttributeCollection', ' ')
) ? 'Custom' : 'Native';
if (local.exceptionType == 'Native')
{
local.result.detail = (structKeyExists(arguments.Exception, 'detail')) ? arguments.Exception.detail : 'undefined';
local.result.message = (structKeyExists(arguments.Exception, 'message')) ? arguments.Exception.message : 'undefined';
local.result.stacktrace = (structKeyExists(arguments.Exception, 'stacktrace')) ? arguments.Exception.stacktrace : 'undefined';
local.result.tagcontext = (structKeyExists(arguments.Exception, 'tagContext')) ? arguments.Exception.tagContext : 'undefined';
local.result.validException = true;
local.result.format = 'Native';
// ADDED by Benoit Hediard to get real detail and message in FW1
if (local.result.message == "Event handler exception." && structKeyExists(arguments.Exception, "Cause")) {
local.result.detail = (structKeyExists(arguments.Exception.Cause, 'detail')) ? arguments.Exception.Cause.detail : 'undefined';
local.result.message = (structKeyExists(arguments.Exception.Cause, 'message')) ? arguments.Exception.Cause.message : 'undefined';
};
return local.result;
} else {
//detail,type,tagcontext,stacktrace,message
switch(local.md.fullname) {
// ColdBox Excepiont
case 'coldbox.system.beans.ExceptionBean' :
case 'coldbox.system.web.context.ExceptionBean' :
local.result.detail = arguments.Exception.getDetail();
local.result.message = arguments.Exception.getMessage();
local.result.stacktrace = arguments.Exception.getStackTrace();
local.result.tagcontext = arguments.Exception.getTagContext();
local.result.validException = true;
local.result.format = 'ColdBox';
break;
// Unknown Exception Passed
default :
local.result.validException = false;
local.result.format = 'Unknown';
break;
/*case 'MachII.util.Exception' :
local.result = arguments.Exception.getCaughtException();
local.result.validException = true;
break;*/
}
}
return local.result;
}
/** Inspect the class heirarchy to return a string showing the class tree.
Original code is borrowed from Dominic Watson at the following address.
Rewritten for for CF9+/Railo features.
http://fusion.dominicwatson.co.uk/2007/09/coldfusion-objects-are-java-objects.html **/
private function GetClassHeirarchy(obj) {
local.thisClass = obj.GetClass();
local.sReturn = thisClass.GetName();
do{
local.thisClass = local.thisClass.GetSuperClass();
local.sReturn &= " " & thisClass.GetName();
} while(CompareNoCase(thisClass.GetName(), 'java.lang.Object'));
return local.sReturn;
}
/** Verify the desired directory structure exsits. */
private void function verifyDirectoryStructure() {
// Verify our index diectory exists
/* Ensure our directory structure is as expected. */
lock name=VARIABLES._NAME timeout=variables.Config.getTimeToLock() type="exclusive" {
if (!directoryExists(variables.paths.Exceptions)) {
directoryCreate(variables.paths.Exceptions);
fileWrite(variables.paths.Exceptions & '/_readme.txt','Hoth: The files within this directory contain the complete details for each unique exception.');
}
if (!directoryExists(variables.paths.Incidents)) {
directoryCreate(variables.paths.Incidents);
fileWrite(variables.paths.Incidents & '/_readme.txt','Hoth: The files within this directory contain the details about the volume of errors for each unique exception.');
}
}
return;
}
}
+41
View File
@@ -0,0 +1,41 @@
// This CFC offers no protection--thus, anyone can access this.
// You can implement this by renaming the CFC with a UUID--that can secure
// this through obscurity. But, I prefer the approach shown in the ColdBox
// example. That is what I use. I have not tried this code :(
component
{
/** Loads the Web UI (HTML) **/
remote function index () returnformat='plain' {
local.HothReport = new Hoth.HothReporter( new config.HothConfig() );
return local.HothReport.getReportView();
}
/** Access Hoth report data as JSON.
@exception If not provided a list of exceptions will be returned.
If provided, the value should be an exception hash which
modified the behavior to return information for only
that exception. **/
remote function report (string exception) returnformat='JSON' {
local.report = (structKeyExists(arguments, 'exception')
? arguments.exception
: 'all');
local.HothReport = new Hoth.HothReporter( new config.HothConfig() );
return local.HothReport.report(local.report);
}
/** Delete a report. **/
remote function delete (string exception)returnformat='JSON' {
if (!structKeyExists(arguments, 'exception'))
{
// We can delete all exceptions at once!
arguments.exception = 'all';
}
local.HothReport = new Hoth.HothReporter( new config.HothConfig() );
// Delete!
return local.HothReporter.delete(arguments.exception);
}
}
@@ -0,0 +1,79 @@
/**
Aaron Greenlee
http://aarongreenlee.com/
This work is licensed under a Creative Commons Attribution-Share-Alike 3.0
Unported License.
// Original Info -----------------------------------------------------------
Author : Aaron Greenlee
Created : 10/01/2010
Default configuration for Hoth.
*/
component
implements='Hoth.object.iHothConfig'
extends='Hoth.object.CoreConfig'
accessors=true {
// -------------------------------------------------------------------------
// BASIC HOTH SETTINGS (required)
// -------------------------------------------------------------------------
/** What is the name of your application? */
property name='applicationName' default='HothDefaultConfig';
/** How many seconds should we lock file operations?
For most operations this is exclusive to a unique exception. */
property name='timeToLock' default='1';
/** Where would you like Hoth to save exception data?
This folder should be empty when you start. */
property name='logPath' default='/Hoth/examples/Example_Logs';
/** Is the log file location relative to the webroot?
This folder should be empty when you start. */
property name='logPathIsRelative' default='true';
// -------------------------------------------------------------------------
// HOTH EMAIL SETTINGS (required)
// -------------------------------------------------------------------------
/** Would you like new exceptions to be emailed to you? */
property name='EmailNewExceptions' default='false';
/** What address(es) should receive these e-mails? */
property name='EmailNewExceptionsTo' default='you@yourdomain.com';
/** What address would you like these emails sent from? */
property name='EmailNewExceptionsFrom' default='hoth@yourdomain.com';
/** Would you like the raw JSON attached to the e-mail? */
property name='EmailNewExceptionsFile' default='false';
/** Would you like HTML emails which contain the exception? */
property name='EmailExceptionsAsHTML' default='false';
// -------------------------------------------------------------------------
// HOTH REPORT SETTINGS (required)
// -------------------------------------------------------------------------
/** How you access the Hoth reports is up to you. When the reports are
generated Hoth needs to know how to build links so you can navigate
the report and get information from the server.
You are responsible for deciding how you wish to access reports. Once
you have figured that out, please, write the full URL here.
**/
property
name='HothReportURL'
default='http://office/lib/Hoth/examples/ColdFusion/HothReportUI.cfc';
// -------------------------------------------------------------------------
// Constructor
// -------------------------------------------------------------------------
public function init()
{
super.init();
return this;
}
}
+1
View File
@@ -0,0 +1 @@
/*.hoth
+3
View File
@@ -0,0 +1,3 @@
This folder is populated by Hoth to keep track of all applications that
use this instance of Hoth so Hoth can find the log files for your
applications when generating reports.
@@ -0,0 +1,98 @@
/*
--------------------------------------------------------------------
I configure and load Hoth into the ColdBox cache
--------------------------------------------------------------------
Define settings in ColdBox.cfc for example:
interceptors = [
//Autowire
{
class="coldbox.system.interceptors.Autowire",
properties={}
},
//SES
{
class="coldbox.system.interceptors.SES",
properties={}
},
//HothTracker
{
class="hoth.extras.ColdBoxInterceptor",
properties={
EmailNewExceptions = true,
EmailNewExceptionsFile = true,
EmailNewExceptionsTo = "kaboom@mysite.com",
EmailNewExceptionsFrom = "server@mysite.com"
}
}
];
--------------------------------------------------------------------
*/
component
{
/* CONSTRUCTOR
------------------------------------------- */
/**
* This is the configuration method for the interceptor
*/
void function Configure()
{
// check for required settings
if ( !propertyExists( "EmailNewExceptionsTo" ) )
{
getPlugin( "logger" ).fatal( "hoth.extras.ColdBoxInterceptor.Configure", "The required 'EmailNewExceptionsTo' property has not been defined" );
throw(
type="hoth.extras.ColdBoxInterceptor"
, message="The required 'EmailNewExceptionsTo' property has not been defined"
);
}
// Use ColdBox settings if not specified
if ( !propertyExists( "applicationName" ) )
{
setProperty( "applicationName", getSetting( "AppName" ) & " (" & getSetting( "Environment" ) & ")" );
}
if ( !propertyExists( "logPath" ) )
{
setProperty( "logPath", getSetting( "ApplicationPath" ) & "logs/hoth" );
setProperty( "logPathIsRelative", false );
}
// Optional settings
if ( !propertyExists( "cacheKeyName" ) )
{
setProperty( "cacheKeyName", "hothtracker" );
}
}
/* INTERCEPTION POINTS
------------------------------------------- */
void function afterAspectsLoad( required any event, required struct interceptData )
{
var HothConfig = "";
var HothTracker = "";
var key = "";
var properties = getProperties();
HothConfig = new hoth.config.HothConfig();
for ( key in properties )
{
// use evaluate to dynamically called setters
if ( StructKeyExists( HothConfig, "set" & key ) )
{
evaluate( "HothConfig.set#key#( properties[ key ] )" );
}
}
HothTracker = new Hoth.HothTracker( HothConfig );
// store in ColdBox Cache
getColdboxOCM().set( getProperty( 'cacheKeyName' ), HothTracker, 0 );
getPlugin( "logger" ).info( "hoth.extras.ColdBoxInterceptor", "HothTracker configured and loaded in ColdBox cache using key '#getProperty( 'cacheKeyName' )#'" );
}
}
+4
View File
@@ -0,0 +1,4 @@
This is the default location for Hoth logs.
You should provide a better location in your Hoth Config object that
is unique to your application.
@@ -0,0 +1,74 @@
/**
Aaron Greenlee
http://aarongreenlee.com/
This work is licensed under a Creative Commons Attribution-Share-Alike 3.0
Unported License.
// Original Info -----------------------------------------------------------
Author : Aaron Greenlee
Created : 10/01/2010
Core configuration object for Hoth. This object provides all the
functionality for a Hoth Config Object. Individual implementations should
extend this object and provide their own values within the property
declaration.
// -------------------------------------------------------------------------
Modified : 12/13/2010 10:04:06 AM by Aaron Greenlee.
- Added support for global settings.
*/
component
accessors='true'
{
/** Construct a configuration object for Hoth. */
public function init ()
{
local.md = getMetadata(this);
local.n = arrayLen(local.md.properties);
for (local.i=1; local.i <= local.n; local.i++) {
local.fn = this['set' & local.md.properties[local.i].name];
local.fn(local.md.properties[local.i]['default']);
}
return this;
}
public void function setGlobalDatabasePath(required string path) {
variables.GlobalDatabasePath = arguments.path;
}
public string function getGlobalDatabasePath() {
return (structKeyExists(variables, 'GlobalDatabasePath'))
? variables.GlobalDatabasePath
: '/Hoth/db/';
}
/** Expands a path **/
public string function getLogPathExpanded ()
{
if (!structKeyExists(variables, 'logPathIsRelative')) {
variables.logPathIsRelative = false;
}
return variables.logPathIsRelative ? expandPath( getLogPath() ) : getLogPath();
}
/** Return a path for Hoth. */
public string function getPath (name)
{
switch (arguments.name) {
// Sub-Directories
case 'incidents' :
case 'exceptions' :
return getLogPathExpanded() & '/' & lcase(arguments.name);
break;
// Files
case 'exceptionIndex' :
case 'exceptionReport' :
case 'exceptionReportActivity' :
return getLogPathExpanded() & '/' & lcase(arguments.name) & '.log';
break;
}
}
}
@@ -0,0 +1,118 @@
/**
Aaron Greenlee
http://aarongreenlee.com/
This work is licensed under a Creative Commons Attribution-Share-Alike 3.0
Unported License.
// Original Info -----------------------------------------------------------
Author : Aaron Greenlee
Created : 12/13/2010
Responsible for helping Hoth report for multiple applications.
*/
component
name = 'HothApplicationManager'
accessors = true
{
property name='applications' type='array';
// The filename that holds application knowledge.
VARIABLES.APP_LOCK_KEY = 'HothApplicationDBFileLock';
/** Constructor.
* @HothConfig Requires a HothConfig Object.
*/
public Hoth.object.HothApplicationManager function init(
required HothConfig
){
setApplications( loadApplicationsFromDisk(HothConfig) );
return this;
}
/** To allow for easy reporting, Hoth needs to be able to track new
applications (or really, new log paths). When a new instance of
HothTracker is created, this method will be called. **/
public boolean function learnApplication(
required HothConfig
){
// Prevent any other threads from saving our new application
lock
name=VARIABLES.APP_LOCK_KEY
timeout=arguments.HothConfig.getTimeToLock()
type="exclusive"
{
local.knownApplications =
loadApplicationsFromDisk(argumentCollection=arguments);
for(local.index in local.knownApplications)
{
// Confirm our keys exist
if (structKeyExists(local.index, 'logPath'))
{
// If we find the same logPath, we know about this
// application and need to exit.
if (local.index.logPath == arguments.HothConfig.getLogPath())
{
return false;
}
}
}
// We did not find out application path within the file.
// Create the struct and append it to our array.
local.applicationDataToSave =
{
'applicationName' = arguments.HothConfig.getApplicationName()
,'logPath' = arguments.HothConfig.getLogPath()
,'created' = now()
};
arrayAppend(
local.knownApplications
,local.applicationDataToSave
);
// Save the contents of the entire array, replacing the entire file
local.path = arguments.HothConfig.getGlobalDatabasePath();
fileWrite(
expandPath(arguments.HothConfig.getGlobalDatabasePath() & 'applications.hoth')
,serializeJSON(local.knownApplications)
,'UTF-8'
);
}
return true;
}
/** Loads applications already observed by Hoth from disk.
* This process should be locked by the calling method.
* @HothConfig Requires a HothConfig Object. */
private array function loadApplicationsFromDisk (
required HothConfig
){
local.path = arguments.HothConfig.getGlobalDatabasePath();
local.appfile = expandPath(arguments.HothConfig.getGlobalDatabasePath() & 'applications.hoth');
// Return an empty array if the appFile does not exist.
if (!fileExists( local.appfile ) )
{
return [];
}
local.applicationsFound = fileRead( local.appfile );
// Delete the database if the file is invalid.
if (!isJSON(local.applicationsFound))
{
fileDelete( local.appfile );
return [];
}
// Convert the file to our desired structure
local.parsedDB = deserializeJSON(local.applicationsFound);
return local.parsedDB;
}
}
@@ -0,0 +1,30 @@
component
{
/**
* Returns the number of seconds since UTC January 1, 1970, 00:00:00
* (Epoch time).
*
* @param DateTime Date/time object you want converted to
* Epoch time.(Required)
*
* @return Returns a numeric value.
* @author Rob Brooks-Bilson (rbils@amkor.com)
* @version 1, June 21, 2002
*/
function GetEpochTimeFromLocal() {
local.datetime = 0;
if ( arrayLen(arguments) == 0)
{
local.datetime = Now();
} else {
local.datetime = arguments[1];
}
return
DateDiff(
"s"
,DateConvert("utc2Local", "January 1 1970 00:00")
,datetime);
}
}
@@ -0,0 +1,19 @@
/**
Aaron Greenlee
http://aarongreenlee.com/
This work is licensed under a Creative Commons Attribution-Share-Alike 3.0
Unported License.
// Original Info -----------------------------------------------------------
Author : Aaron Greenlee
Created : 10/01/2010
Interface for a Hoth Config Object.
*/
interface {
public function init ();
public string function getLogPathExpanded();
public string function getPath (name);
}
@@ -0,0 +1,457 @@
<cfcomponent displayname="bugLogListener">
<!---
bugLogListener.cfc
This is the main point of entry into the bugLog API. This component is the one that
actually processes the bug reports, it adds them to the database and is responsible
for processing any defined rules.
Created: 2007 - Oscar Arevalo - http://www.oscararevalo.com
--->
<cfset variables.startedOn = 0>
<cfset variables.oDAOFactory = 0>
<cfset variables.oRuleProcessor = 0>
<cfset variables.oConfig = 0>
<cfset variables.msgLog = arrayNew(1)>
<cfset variables.maxLogSize = 10>
<cfset variables.instanceName = "">
<cfset variables.autoCreateDefault = true>
<cffunction name="init" access="public" returntype="bugLogListener" hint="This is the constructor">
<cfargument name="config" required="true" type="config">
<cfargument name="instanceName" type="string" required="true">
<cfscript>
var cacheTTL = 300; // timeout in minutes for cache entries
variables.oConfig = arguments.config; // global configuration
variables.instanceName = arguments.instanceName;
logMessage("Starting BugLogListener (#instanceName#) service...");
// load settings
variables.maxLogSize = arguments.config.getSetting("service.maxLogSize");
// load DAO Factory
variables.oDAOFactory = createObject("component","bugLog.components.DAOFactory").init( variables.oConfig );
// load the finder objects
variables.oAppFinder = createObject("component","bugLog.components.appFinder").init( variables.oDAOFactory.getDAO("application") );
variables.oHostFinder = createObject("component","bugLog.components.hostFinder").init( variables.oDAOFactory.getDAO("host") );
variables.oSeverityFinder = createObject("component","bugLog.components.severityFinder").init( variables.oDAOFactory.getDAO("severity") );
variables.oSourceFinder = createObject("component","bugLog.components.sourceFinder").init( variables.oDAOFactory.getDAO("source") );
variables.oUserFinder = createObject("component","bugLog.components.userFinder").init( variables.oDAOFactory.getDAO("user") );
// load the rule processor
variables.oRuleProcessor = createObject("component","bugLog.components.ruleProcessor").init();
// create cache instances
variables.oAppCache = createObject("component","bugLog.components.lib.cache.cacheService").init(50, cacheTTL, false);
variables.oHostCache = createObject("component","bugLog.components.lib.cache.cacheService").init(50, cacheTTL, false);
variables.oSeverityCache = createObject("component","bugLog.components.lib.cache.cacheService").init(10, cacheTTL, false);
variables.oSourceCache = createObject("component","bugLog.components.lib.cache.cacheService").init(5, cacheTTL, false);
variables.oUserCache = createObject("component","bugLog.components.lib.cache.cacheService").init(50, cacheTTL, false);
// load scheduler
variables.scheduler = createObject("component","bugLog.components.schedulerService").init( variables.oConfig, variables.instanceName );
// load the mailer service
variables.mailerService = createObject("component","bugLog.components.MailerService").init( variables.oConfig );
// load rules
loadRules();
// configure history purging
configureHistoryPurging();
// configure the digest sender
configureDigest();
// record the date at which the service started
variables.startedOn = Now();
logMessage("BugLogListener Service (#instanceName#) Started");
</cfscript>
<cfreturn this>
</cffunction>
<cffunction name="logEntry" access="public" returntype="void" hint="This method adds a bug report entry into BugLog. Bug reports must be passed as RawEntryBeans">
<cfargument name="entryBean" type="rawEntryBean" required="true">
<cfscript>
var bean = arguments.entryBean;
var oEntry = 0;
var oApp = 0;
var oHost = 0;
var oSeverity = 0;
var oSource = 0;
var oDF = variables.oDAOFactory;
// get autocreate settings
var autoCreateApp = allowAutoCreate("application");
var autoCreateHost = allowAutoCreate("host");
var autoCreateSeverity = allowAutoCreate("severity");
var autoCreateSource = allowAutoCreate("source");
// extract related objects from bean
oApp = getApplicationFromBean( bean, autoCreateApp );
oHost = getHostFromBean( bean, autoCreateHost );
oSeverity = getSeverityFromBean( bean, autoCreateSeverity );
oSource = getSourceFromBean( bean, autoCreateSource );
// create entry
oEntry = createObject("component","bugLog.components.entry").init( oDF.getDAO("entry") );
oEntry.setDateTime(bean.getdateTime());
oEntry.setMessage(bean.getmessage());
oEntry.setApplicationID(oApp.getApplicationID());
oEntry.setSourceID(oSource.getSourceID());
oEntry.setSeverityID(oSeverity.getSeverityID());
oEntry.setHostID(oHost.getHostID());
oEntry.setExceptionMessage(bean.getexceptionMessage());
oEntry.setExceptionDetails(bean.getexceptionDetails());
oEntry.setCFID(bean.getcfid());
oEntry.setCFTOKEN(bean.getcftoken());
oEntry.setUserAgent(bean.getuserAgent());
oEntry.setTemplatePath(bean.gettemplate_Path());
oEntry.setHTMLReport(bean.getHTMLReport());
oEntry.setCreatedOn(bean.getReceivedOn());
// save entry
oEntry.save();
// process rules
variables.oRuleProcessor.processRules(bean, oEntry);
</cfscript>
</cffunction>
<cffunction name="getStartedOn" access="public" returntype="date" hint="Returns the date and time where this instance of BugLogListener was created">
<cfreturn variables.startedOn>
</cffunction>
<cffunction name="shutDown" access="public" returntype="void" hint="Performs any clean up action required">
<cfset logMessage("BugLogListener service stopped.")>
</cffunction>
<cffunction name="logMessage" access="public" output="false" returntype="void" hint="this method appends an entry to the messages log as well as displays the message on the console">
<cfargument name="msg" type="string" required="true" />
<cfset var System = CreateObject('java','java.lang.System') />
<cfset var txt = timeFormat(now(), 'HH:mm:ss') & ": " & msg>
<cfset System.out.println("BugLogListener: " & txt) />
<cflock name="bugLogListener_logMessage" type="exclusive" timeout="10">
<cfif arrayLen(variables.msgLog) gt variables.maxLogSize>
<cfset arrayDeleteAt(variables.msgLog, ArrayLen(variables.msgLog))>
</cfif>
<cfset arrayPrepend(variables.msgLog,txt)>
</cflock>
</cffunction>
<cffunction name="getConfig" returnType="any" access="public" hint="returns the config settings">
<cfreturn variables.oConfig>
</cffunction>
<cffunction name="getInstanceName" returnType="any" access="public" hint="returns the current instance name">
<cfreturn variables.instanceName>
</cffunction>
<cffunction name="validate" returntype="boolean" access="public" hint="Validates that a bug report is valid, if not throws an error. This only applies when the requireAPIKey setting is true, otherwise returns True always">
<cfargument name="entryBean" type="rawEntryBean" required="true">
<cfargument name="apiKey" type="string" required="true">
<cfscript>
// validate API
if(getConfig().getSetting("service.requireAPIKey",false)) {
if(arguments.apiKey eq "") {
throw(message="Invalid API Key",type="bugLog.invalidAPIKey");
}
var masterKey = getConfig().getSetting("service.APIKey");
if(arguments.apiKey neq masterKey) {
var user = getUserByAPIKey(arguments.apiKey);
if(!user.getIsAdmin() and arrayLen(user.getAllowedApplications())) {
// key is good, but since the user is a non-admin
// we need to validate the user can post bugs to the requested
// application.
var app = getApplicationFromBean( entryBean, false );
if(!user.isApplicationAllowed(app)) {
throw(message="Application not allowed",type="applicationNotAllowed");
}
}
}
}
// validate application
if(!allowAutoCreate("application")) {
getApplicationFromBean( entryBean, false );
}
// validate host
if(!allowAutoCreate("host")) {
getHostFromBean( entryBean, false );
}
// validte severity
if(!allowAutoCreate("severity")) {
getSeverityFromBean( entryBean, false );
}
// validate source
if(!allowAutoCreate("source")) {
getSourceFromBean( entryBean, false );
}
return true;
</cfscript>
</cffunction>
<cffunction name="reloadRules" access="public" returntype="void" hint="Reloads all rules">
<cfset loadRules()>
</cffunction>
<cffunction name="getMessageLog" access="public" returntype="array">
<cfreturn variables.msgLog>
</cffunction>
<!---- Private Methods ---->
<cffunction name="getApplicationFromBean" access="private" returntype="app" hint="Uses the information on the rawEntryBean to retrieve the corresponding Application object">
<cfargument name="entryBean" type="rawEntryBean" required="true">
<cfargument name="createIfNeeded" type="boolean" default="false">
<cfscript>
var key = "";
var bean = arguments.entryBean;
var oApp = 0;
var oDF = variables.oDAOFactory;
key = bean.getApplicationCode();
try {
// first we try to get it from the cache
oApp = variables.oAppCache.retrieve( key );
} catch(cacheService.itemNotFound e) {
// entry not in cache, so we get it from DB
try {
oApp = variables.oAppFinder.findByCode( key );
} catch(appFinderException.ApplicationCodeNotFound e) {
// code does not exist, so we need to create it (if autocreate enabled)
if(!arguments.createIfNeeded) throw(message="Invalid Application",type="invalidApplication");
oApp = createObject("component","bugLog.components.app").init( oDF.getDAO("application") );
oApp.setCode( key );
oApp.setName( key );
oApp.save();
}
// store entry in cache
variables.oAppCache.store( key, oApp );
}
</cfscript>
<cfreturn oApp>
</cffunction>
<cffunction name="getHostFromBean" access="private" returntype="host" hint="Uses the information on the rawEntryBean to retrieve the corresponding Host object">
<cfargument name="entryBean" type="rawEntryBean" required="true">
<cfargument name="createIfNeeded" type="boolean" default="false">
<cfscript>
var key = "";
var bean = arguments.entryBean;
var oHost = 0;
var oDF = variables.oDAOFactory;
key = bean.getHostName();
try {
// first we try to get it from the cache
oHost = variables.oHostCache.retrieve( key );
} catch(cacheService.itemNotFound e) {
// entry not in cache, so we get it from DB
try {
oHost = variables.oHostFinder.findByName( key );
} catch(hostFinderException.HostNameNotFound e) {
// code does not exist, so we need to create it (if autocreate enabled)
if(!arguments.createIfNeeded) throw(message="Invalid Host",type="invalidHost");
oHost = createObject("component","bugLog.components.host").init( oDF.getDAO("host") );
oHost.setHostName(key);
oHost.save();
}
// store entry in cache
variables.oHostCache.store( key, oHost );
}
</cfscript>
<cfreturn oHost>
</cffunction>
<cffunction name="getSeverityFromBean" access="private" returntype="severity" hint="Uses the information on the rawEntryBean to retrieve the corresponding Severity object">
<cfargument name="entryBean" type="rawEntryBean" required="true">
<cfargument name="createIfNeeded" type="boolean" default="false">
<cfscript>
var key = "";
var bean = arguments.entryBean;
var oSeverity = 0;
var oDF = variables.oDAOFactory;
key = bean.getSeverityCode();
try {
// first we try to get it from the cache
oSeverity = variables.oSeverityCache.retrieve( key );
} catch(cacheService.itemNotFound e) {
// entry not in cache, so we get it from DB
try {
oSeverity = variables.oSeverityFinder.findByCode( key );
} catch(severityFinderException.codeNotFound e) {
// code does not exist, so we need to create it (if autocreate enabled)
if(!arguments.createIfNeeded) throw(message="Invalid Severity",type="invalidSeverity");
oSeverity = createObject("component","bugLog.components.severity").init( oDF.getDAO("severity") );
oSeverity.setCode( key );
oSeverity.setName( key );
oSeverity.save();
}
// store entry in cache
variables.oSeverityCache.store( key, oSeverity );
}
</cfscript>
<cfreturn oSeverity>
</cffunction>
<cffunction name="getUserByAPIKey" access="private" returntype="user" hint="Finds a user object using by its API Key">
<cfargument name="apiKey" type="string" required="true">
<cfscript>
var oUser = 0;
try {
// first we try to get it from the cache
oUser = variables.oUserCache.retrieve( apiKey );
} catch(cacheService.itemNotFound e) {
// entry not in cache, so we get it from DB
try {
oUser = variables.oUserFinder.findByAPIKey( apiKey );
var qryUserApps = oDAOFactory.getDAO("userApplication").search(userID = oUser.getUserID());
var apps = oAppFinder.findByIDList(valueList(qryUserApps.applicationID));
oUser.setAllowedApplications(apps);
} catch(sourceFinderException.usernameNotFound e) {
// code does not exist, so we need to create it (if autocreate enabled)
throw(message="Invalid API Key",type="bugLog.invalidAPIKey");
}
// store entry in cache
variables.oUserCache.store( key, oUser );
}
return oUser;
</cfscript>
</cffunction>
<cffunction name="getSourceFromBean" access="private" returntype="source" hint="Uses the information on the rawEntryBean to retrieve the corresponding Source object">
<cfargument name="entryBean" type="rawEntryBean" required="true">
<cfargument name="createIfNeeded" type="boolean" default="false">
<cfscript>
var key = "";
var bean = arguments.entryBean;
var oSource = 0;
var oDF = variables.oDAOFactory;
key = bean.getSourceName();
try {
// first we try to get it from the cache
oSource = variables.oSourceCache.retrieve( key );
} catch(cacheService.itemNotFound e) {
// entry not in cache, so we get it from DB
try {
oSource = variables.oSourceFinder.findByName( key );
} catch(sourceFinderException.codeNotFound e) {
// code does not exist, so we need to create it (if autocreate enabled)
if(!arguments.createIfNeeded) throw(message="Invalid Source",type="invalidSource");
oSource = createObject("component","bugLog.components.source").init( oDF.getDAO("source") );
oSource.setName( key );
oSource.save();
}
// store entry in cache
variables.oSourceCache.store( key, oSource );
}
</cfscript>
<cfreturn oSource>
</cffunction>
<cffunction name="loadRules" access="private" returntype="void" hint="this method loads the rules into the rule processor">
<cfscript>
var oRule = 0;
var oExtensionsService = 0;
var aRules = arrayNew(1);
var i = 0;
var dao = 0;
var thisRule = 0;
// clear all existing rules
variables.oRuleProcessor.flushRules();
// get the rule definitions from the extensions service
dao = variables.oDAOFactory.getDAO("extension");
oExtensionsService = createObject("component","bugLog.components.extensionsService").init( dao );
aRules = oExtensionsService.getRules();
// create rule objects and load them into the rule processor
for(i=1; i lte arrayLen(aRules);i=i+1) {
thisRule = aRules[i];
if(thisRule.enabled) {
oRule =
thisRule.instance
.setListener(this)
.setDAOFactory( variables.oDAOFactory )
.setMailerService( variables.mailerService );
// add rule to processor
variables.oRuleProcessor.addRule(oRule);
}
}
</cfscript>
</cffunction>
<cffunction name="configureHistoryPurging" access="private" output="false" returntype="void">
<cfset var enabled = getConfig().getSetting("purging.enabled")>
<cfif enabled>
<cfset scheduler.setupTask("bugLogPurgeHistory",
"util/purgeHistory.cfm",
"03:00",
"daily") />
<cfelse>
<cfset scheduler.removeTask("bugLogPurgeHistory") />
</cfif>
</cffunction>
<cffunction name="configureDigest" access="private" output="false" returntype="void">
<cfset var config = getConfig()>
<cfset var enabled = config.getSetting("digest.enabled")>
<cfset var interval = config.getSetting("digest.schedulerIntervalHours")>
<cfset var startTime = config.getSetting("digest.schedulerStartTime")>
<cfif enabled>
<cfset scheduler.setupTask("bugLogSendDigest",
"util/sendDigest.cfm",
startTime,
interval*3600) />
<cfelse>
<cfset scheduler.removeTask("bugLogSendDigest") />
</cfif>
</cffunction>
<cffunction name="allowAutoCreate" returnType="boolean" access="private">
<cfargument name="entityType" type="string" required="true">
<cfreturn getConfig().getSetting("autocreate." & arguments.entityType, variables.autoCreateDefault)>
</cffunction>
</cfcomponent>
+874
View File
@@ -0,0 +1,874 @@
component {
variables._fw1_version = "3.5.0";
variables._di1_version = "1.1.1";
/*
Copyright (c) 2010-2015, Sean Corfield
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// CONSTRUCTOR
public any function init( any folders, struct config = { } ) {
variables.folderList = folders;
variables.folderArray = folders;
if ( isSimpleValue( folders ) ) {
variables.folderArray = listToArray( folders );
} else {
variables.folderList = arrayToList( folders );
}
var n = arrayLen( variables.folderArray );
for ( var i = 1; i <= n; ++i ) {
variables.folderArray[ i ] = trim( variables.folderArray[ i ] );
}
variables.config = config;
variables.beanInfo = { };
variables.beanCache = { };
variables.resolutionCache = { };
variables.initMethodCache = { };
variables.settersInfo = { };
variables.autoExclude = [
'/WEB-INF', '/Application.cfc', // never manage these!
'/Application.lc', '/Application.lucee',
// assume default name for intermediary:
'/MyApplication.cfc',
'/MyApplication.lc', '/MyApplication.lucee',
'framework.cfc', 'ioc.cfc', // legacy FW/1 / DI/1
// recent FW/1 + DI/1 + AOP/1 exclusions:
'/framework/aop.cfc', '/framework/beanProxy.cfc',
'/framework/ioc.cfc', '/framework/WireBoxAdapter.cfc',
'/framework/one.cfc',
// and Clojure-related exclusions:
'/framework/cfmljure.cfc', '/framework/cljcontroller.cfc',
'/framework/ioclj.cfc'
];
variables.listeners = 0;
setupFrameworkDefaults();
if ( structKeyExists( variables.config, 'loadListener' ) ) {
this.onLoad( variables.config.loadListener );
}
return this;
}
// PUBLIC METHODS
// programmatically register an alias
public any function addAlias( string aliasName, string beanName ) {
discoverBeans();
variables.beanInfo[ aliasName ] = variables.beanInfo[ beanName ];
return this;
}
// programmatically register new beans with the factory (add a singleton name/value pair)
public any function addBean( string beanName, any beanValue ) {
discoverBeans();
variables.beanInfo[ beanName ] = {
name = beanName, value = beanValue, isSingleton = true
};
return this;
}
// return true if the factory (or a parent factory) knows about the requested bean
public boolean function containsBean( string beanName ) {
discoverBeans();
return structKeyExists( variables.beanInfo, beanName ) ||
( structKeyExists( variables, 'parent' ) && variables.parent.containsBean( beanName ) );
}
// programmatically register new beans with the factory (add an actual CFC)
public any function declareBean( string beanName, string dottedPath, boolean isSingleton = true, struct overrides = { } ) {
discoverBeans();
var singleDir = '';
if ( listLen( dottedPath, '.' ) > 1 ) {
var cfc = listLast( dottedPath, '.' );
var dottedPart = left( dottedPath, len( dottedPath ) - len( cfc ) - 1 );
singleDir = singular( listLast( dottedPart, '.' ) );
}
var basePath = replace( dottedPath, '.', '/', 'all' );
var cfcPath = expandPath( '/' & basePath & '.cfc' );
var expPath = cfcPath;
if ( !fileExists( expPath ) ) expPath = expandPath( '/' & basePath & '.lc' );
if ( !fileExists( expPath ) ) expPath = expandPath( '/' & basePath & '.lucee' );
if ( !fileExists( expPath ) ) throw "Unable to find source file for #dottedPath#: expands to #cfcPath#";
var cfcPath = replace( expPath, chr(92), '/', 'all' );
var metadata = {
name = beanName, qualifier = singleDir, isSingleton = isSingleton,
path = cfcPath, cfc = dottedPath, metadata = cleanMetadata( dottedPath ),
overrides = overrides
};
variables.beanInfo[ beanName ] = metadata;
return this;
}
public any function factoryBean( string beanName, any factory, string methodName, array args = [ ], struct overrides = { } ) {
discoverBeans();
var metadata = {
name = beanName, isSingleton = false, // really?
factory = factory, method = methodName, args = args,
overrides = overrides
};
variables.beanInfo[ beanName ] = metadata;
return this;
}
// return the requested bean, fully populated
public any function getBean( string beanName ) {
discoverBeans();
if ( structKeyExists( variables.beanInfo, beanName ) ) {
return resolveBean( beanName );
} else if ( structKeyExists( variables, 'parent' ) ) {
return variables.parent.getBean( beanName );
} else {
throw 'bean not found: #beanName#';
}
}
// convenience API for metaprogramming perhaps?
public any function getBeanInfo( string beanName = '', boolean flatten = false,
string regex = '' ) {
discoverBeans();
if ( len( beanName ) ) {
// ask about a specific bean:
if ( structKeyExists( variables.beanInfo, beanName ) ) {
return variables.beanInfo[ beanName ];
}
if ( structKeyExists( variables, 'parent' ) ) {
return parentBeanInfo( beanName );
}
throw 'bean not found: #beanName#';
} else {
var result = { beanInfo = { } };
if ( structKeyExists( variables, 'parent' ) ) {
if ( flatten || len( regex ) ) {
structAppend( result.beanInfo, parentBeanInfoList( flatten ).beanInfo );
structAppend( result.beanInfo, variables.beanInfo );
} else {
result.beanInfo = variables.beanInfo;
result.parent = parentBeanInfoList( flatten );
};
} else {
result.beanInfo = variables.beanInfo;
}
if ( len( regex ) ) {
var matched = { };
for ( var name in result.beanInfo ) {
if ( REFind( regex, name ) ) {
matched[ name ] = result.beanInfo[ name ];
}
}
result.beanInfo = matched;
}
return result;
}
}
// return a copy of the DI/1 configuration
public struct function getConfig() {
// note: we only make a shallow copy
return structCopy( variables.config );
}
// return the DI/1 version
public string function getVersion() {
return variables.config.version;
}
// return true iff bean is known to be a singleton
public boolean function isSingleton( string beanName ) {
discoverBeans();
if ( structKeyExists( variables.beanInfo, beanName ) ) {
return variables.beanInfo[ beanName ].isSingleton;
} else if ( structKeyExists( variables, 'parent' ) ) {
try {
return variables.parent.isSingleton( beanName );
} catch ( any e ) {
return false; // parent doesn't know the bean therefore is it not singleton
}
} else {
return false; // we don't know the bean therefore it is not a managed singleton
}
}
// given a bean (by name, by type or by value), call the named
// setters with the specified property values
public any function injectProperties( any bean, struct properties ) {
if ( isSimpleValue( bean ) ) {
if ( containsBean( bean ) ) bean = getBean( bean );
else bean = construct( bean );
}
for ( var property in properties ) {
if ( !isNull( properties[ property ] ) ) {
var args = { };
args[ property ] = properties[ property ];
evaluate( 'bean.set#property#( argumentCollection = args )' );
}
}
return bean;
}
// empty the cache and reload all the singleton beans
// note: this does not reload the parent - if you have parent/child factories you
// are responsible for dealing with that logic (it's safe to reload a child but
// if you reload the parent, you must reload *all* child factories to ensure
// things stay consistent!)
public any function load() {
discoverBeans();
variables.beanCache = { };
variables.resolutionCache = { };
variables.initMethodCache = { };
for ( var key in variables.beanInfo ) {
if ( !structKeyExists( variables.beanInfo[ key ], "isSingleton" ) )
throw "internal error: bean #key# has no isSingleton flag!";
if ( variables.beanInfo[ key ].isSingleton ) {
getBean( key );
}
}
return this;
}
// add a listener for processing after a (re)load of the factory
// called with just the factory, should be a plain function
public any function onLoad( any listener ) {
var head = { next = variables.listeners, listener = listener };
variables.listeners = head;
return this;
}
// set the parent bean factory
public any function setParent( any parent ) {
variables.parent = parent;
return this;
}
// PRIVATE METHODS
private boolean function beanIsTransient( string singleDir, string dir, string beanName ) {
return singleDir == 'bean' ||
structKeyExists( variables.transients, dir ) ||
( structKeyExists( variables.config, "singletonPattern" ) &&
refindNoCase( variables.config.singletonPattern, beanName ) == 0 ) ||
( structKeyExists( variables.config, "transientPattern" ) &&
refindNoCase( variables.config.transientPattern, beanName ) > 0 );
}
private any function cachable( string beanName) {
var newObject = false;
var info = variables.beanInfo[ beanName ];
if ( info.isSingleton ) {
// cache on the qualified bean name:
var qualifiedName = beanName;
if ( structKeyExists( info, 'name' ) && structKeyExists( info, 'qualifier' ) ) {
qualifiedName = info.name & info.qualifier;
}
if ( !structKeyExists( variables.beanCache, qualifiedName ) ) {
variables.beanCache[ qualifiedName ] = construct( info.cfc );
newObject = true;
}
return { bean = variables.beanCache[ qualifiedName ], newObject = newObject };
} else {
return { bean = construct( info.cfc ), newObject = true };
}
}
private struct function cleanMetadata( string cfc ) {
var baseMetadata = metadata( cfc );
var iocMeta = { setters = { }, pruned = false, type = baseMetadata.type };
var md = { extends = baseMetadata };
do {
md = md.extends;
// gather up setters based on metadata:
var implicitSetters = false;
// we have implicit setters if: accessors="true" or persistent="true"
if ( structKeyExists( md, 'persistent' ) && isBoolean( md.persistent ) ) {
implicitSetters = md.persistent;
}
if ( structKeyExists( md, 'accessors' ) && isBoolean( md.accessors ) ) {
implicitSetters = implicitSetters || md.accessors;
}
if ( structKeyExists( md, 'properties' ) ) {
// due to a bug in ACF9.0.1, we cannot use var property in md.properties,
// instead we must use an explicit loop index... ugh!
var n = arrayLen( md.properties );
for ( var i = 1; i <= n; ++i ) {
var property = md.properties[ i ];
if ( implicitSetters &&
( !structKeyExists( property, 'setter' ) ||
isBoolean( property.setter ) && property.setter ) ) {
if ( structKeyExists( property, 'type' ) &&
property.type != 'any' ) {
iocMeta.setters[ property.name ] = 'typed';
} else {
iocMeta.setters[ property.name ] = 'implicit';
}
}
}
}
// still looking for a constructor?
if ( !structKeyExists( iocMeta, 'constructor' ) ) {
if ( structKeyExists( md, 'functions' ) ) {
// due to a bug in ACF9.0.1, we cannot use var property in md.functions,
// instead we must use an explicit loop index... ugh!
var n = arrayLen( md.functions );
for ( var i = 1; i <= n; ++i ) {
var func = md.functions[ i ];
if ( func.name == 'init' ) {
iocMeta.constructor = { };
if ( structKeyExists( func, 'parameters' ) ) {
// due to a bug in ACF9.0.1, we cannot use var arg in func.parameters,
// instead we must use an explicit loop index... ugh!
var m = arrayLen( func.parameters );
for ( var j = 1; j <= m; ++j ) {
var arg = func.parameters[ j ];
iocMeta.constructor[ arg.name ] = structKeyExists( arg, 'required' ) ? arg.required : false;
}
}
}
}
}
}
} while ( structKeyExists( md, 'extends' ) );
return iocMeta;
}
// in case an extension point wants to override actual construction:
private any function construct( string dottedPath ) {
return createObject( 'component', dottedPath );
}
// in case an extension point wants to override actual metadata retrieval:
private any function metadata( string dottedPath ) {
try {
return getComponentMetadata( dottedPath );
} catch ( any e ) {
var except = "Unable to getComponentMetadata(#dottedPath#) because: " &
e.message & ( len( e.detail ) ? " (#e.detail#)" : "" );
throw except;
}
}
private string function deduceDottedPath( string baseMapping, string basePath ) {
if ( right( basePath, 1 ) == '/' && len( basePath ) > 1 ) {
basePath = left( basePath, len( basePath ) - 1 );
}
var cfcPath = left( baseMapping, 1 ) == '/' ?
( len( baseMapping ) > 1 ? right( baseMapping, len( baseMapping ) - 1 ) : '' ) :
getFileFromPath( baseMapping );
if ( right( cfcPath, 1 ) == '/' && len( cfcPath ) > 1 ) {
cfcPath = left( cfcPath, len( cfcPath ) - 1 );
}
var expPath = basePath;
var notFound = true;
var dotted = '';
do {
var mapped = cfcPath;
if ( len( mapped ) && left( mapped, 1 ) != '.' ) mapped = '/' & mapped;
var mappedPath = replace( expandpath( mapped ), chr(92), '/', 'all' );
if ( mappedPath == basePath ) {
dotted = replace( cfcPath, '/', '.', 'all' );
notFound = false;
break;
}
var prevPath = expPath;
expPath = replace( getDirectoryFromPath( expPath ), chr(92), '/', 'all' );
if ( right( expPath, 1 ) == '/' && len( expPath ) > 1 ) {
expPath = left( expPath, len( expPath ) - 1 );
}
var progress = prevPath != expPath;
var piece = listLast( expPath, '/' );
cfcPath = piece & '/' & cfcPath;
} while ( progress );
if ( notFound ) {
throw 'unable to deduce dot-relative path for: #baseMapping# (#basePath#) root #expandPath("/")#';
}
return dotted;
}
private void function discoverBeans() {
if ( structKeyExists( variables, 'discoveryComplete' ) ) return;
lock name="#application.applicationName#_ioc1_#variables.folderList#" type="exclusive" timeout="30" {
if ( structKeyExists( variables, 'discoveryComplete' ) ) return;
variables.pathMapCache = { };
for ( var f in variables.folderArray ) {
discoverBeansInFolder( replace( f, chr(92), '/', 'all' ) );
}
variables.discoveryComplete = true;
}
onLoadEvent();
}
private void function discoverBeansInFolder( string mapping ) {
var folder = replace( expandPath( mapping ), chr(92), '/', 'all' );
var dotted = deduceDottedPath( mapping, folder );
var cfcs = [ ];
try {
cfcs = directoryList( folder, variables.config.recurse, 'path', '*.cfc' );
var lcs = directoryList( folder, variables.config.recurse, 'path', '*.lc' );
for ( var l in lcs ) arrayAppend( cfcs, l );
lcs = directoryList( folder, variables.config.recurse, 'path', '*.lucee' );
for ( l in lcs ) arrayAppend( cfcs, l );
} catch ( any e ) {
// assume bad path - ignore it, cfcs is empty list
}
for ( var cfcOSPath in cfcs ) {
var cfcPath = replace( cfcOSPath, chr(92), '/', 'all' );
// watch out for excluded paths:
var excludePath = false;
for ( var pattern in variables.config.exclude ) {
if ( findNoCase( pattern, cfcPath ) ) {
excludePath = true;
break;
}
}
if ( excludePath ) continue;
var relPath = right( cfcPath, len( cfcPath ) - len( folder ) );
var extN = 1 + len( listLast( cfcPath, "." ) );
relPath = left( relPath, len( relPath ) - extN );
var dir = listLast( getDirectoryFromPath( cfcPath ), '/' );
var singleDir = singular( dir );
var beanName = listLast( relPath, '/' );
var dottedPath = dotted & replace( relPath, '/', '.', 'all' );
try {
var metadata = {
name = beanName, qualifier = singleDir, isSingleton = !beanIsTransient( singleDir, dir, beanName ),
path = cfcPath, cfc = dottedPath, metadata = cleanMetadata( dottedPath )
};
if ( structKeyExists( metadata.metadata, "type" ) && metadata.metadata.type == "interface" ) {
continue;
}
if ( structKeyExists( variables.beanInfo, beanName ) ) {
if ( variables.config.omitDirectoryAliases ) {
throw '#beanName# is not unique (and omitDirectoryAliases is true)';
}
structDelete( variables.beanInfo, beanName );
variables.beanInfo[ beanName & singleDir ] = metadata;
} else {
variables.beanInfo[ beanName ] = metadata;
if ( !variables.config.omitDirectoryAliases ) {
variables.beanInfo[ beanName & singleDir ] = metadata;
}
}
} catch ( any e ) {
// wrap the exception so we can add bean name for debugging
// this trades off any stack trace information for the bean name but
// since we are only trying to get metadata, the latter should be
// more useful than the former
var except = "Problem with metadata for #beanName# (#dottedPath#) because: " &
e.message & ( len( e.detail ) ? " (#e.detail#)" : "" );
}
}
}
private struct function findSetters( any cfc, struct iocMeta ) {
var liveMeta = { setters = iocMeta.setters };
if ( !iocMeta.pruned ) {
// need to prune known setters of transients:
var prunable = { };
for ( var known in iocMeta.setters ) {
if ( !isSingleton( known ) ) {
prunable[ known ] = true;
}
}
for ( known in prunable ) {
structDelete( iocMeta.setters, known );
}
iocMeta.pruned = true;
}
// gather up explicit setters:
for ( var member in cfc ) {
var method = cfc[ member ];
var n = len( member );
if ( isCustomFunction( method ) && left( member, 3 ) == 'set' && n > 3 ) {
var property = right( member, n - 3 );
if ( !isSingleton( property ) ) {
// ignore properties that we know to be transients...
continue;
}
if ( !structKeyExists( liveMeta.setters, property ) ) {
liveMeta.setters[ property ] = 'explicit';
}
}
}
return liveMeta;
}
private any function forceCache( any bean, string beanName) {
var info = variables.beanInfo[ beanName ];
if ( info.isSingleton ) {
// cache on the qualified bean name:
var qualifiedName = beanName;
if ( structKeyExists( info, 'name' ) && structKeyExists( info, 'qualifier' ) ) {
qualifiedName = info.name & info.qualifier;
}
variables.beanCache[ qualifiedName ] = bean;
}
}
private boolean function isConstant ( string beanName ) {
return structKeyExists( variables.beanInfo, beanName ) &&
structKeyExists( variables.beanInfo[ beanName ], 'value' );
}
private void function logMissingBean( string beanName, string resolvingBeanName = '' ) {
var sys = createObject( 'java', 'java.lang.System' );
if ( len( resolvingBeanName ) ) {
sys.out.println( 'bean not found: #beanName#; while resolving #resolvingBeanName#' );
} else {
sys.out.println( 'bean not found: #beanName#' );
}
}
private void function missingBean( string beanName, string resolvingBeanName = '' ) {
if ( variables.config.strict ) {
if ( len( resolvingBeanName ) ) {
throw 'bean not found: #beanName#; while resolving #resolvingBeanName#';
} else {
throw 'bean not found: #beanName#';
}
} else {
logMissingBean( beanName, resolvingBeanName );
}
}
private void function onLoadEvent() {
var head = variables.listeners;
while ( isStruct( head ) ) {
if ( isCustomFunction( head.listener ) ||
( listFirst( server.coldfusion.productVersion ) >= 10 &&
isClosure( head.listener ) ) ) {
head.listener( this );
} else if ( isObject( head.listener ) ) {
head.listener.onLoad( this );
} else if ( isSimpleValue( head.listener ) &&
containsBean( head.listener ) ) {
getBean( head.listener ).onLoad( this );
} else {
throw "invalid onLoad listener registered: #head.listener.toString()#";
}
head = head.next;
}
}
private any function parentBeanInfo( string beanName ) {
// intended to be adaptable to whatever the parent is:
if ( structKeyExists( variables.parent, 'getBeanInfo' ) ) {
// smells like DI/1 or compatible:
return variables.parent.getBeanInfo( beanName );
}
if ( structKeyExists( variables.parent, 'getBeanDefinition' ) ) {
// smells like ColdSpring or compatible:
return variables.parent.getBeanDefinition( beanName );
}
// unknown:
return { };
}
private any function parentBeanInfoList( boolean flatten ) {
// intended to be adaptable to whatever the parent is:
if ( structKeyExists( variables.parent, 'getBeanInfo' ) ) {
// smells like DI/1 or compatible:
return variables.parent.getBeanInfo( flatten = flatten );
}
if ( structKeyExists( variables.parent, 'getBeanDefinitionList' ) ) {
// smells like ColdSpring or compatible:
return variables.parent.getBeanDefinitionList();
}
// unknown
return { };
}
private any function resolveBean( string beanName ) {
// do enough resolution to create and initialization this bean
// returns a struct of the bean and a struct of beans and setters still to run
// construction phase:
var partialBean = resolveBeanCreate( beanName, { injection = { }, dependencies = { } } );
if ( structKeyExists( variables.resolutionCache, beanName ) &&
variables.resolutionCache[ beanName ] ) {
// fully resolved, no action needed this time
} else {
var checkForPostInjection = structKeyExists( variables.config, 'initMethod' );
var initMethod = checkForPostInjection ? variables.config.initMethod : '';
var postInjectables = { };
// injection phase:
// now perform all of the injection:
for ( var name in partialBean.injection ) {
var injection = partialBean.injection[ name ];
if ( checkForPostInjection && !isConstant( name ) && structKeyExists( injection.bean, initMethod ) ) {
postInjectables[ name ] = true;
}
for ( var property in injection.setters ) {
if ( injection.setters[ property ] == 'typed' &&
variables.config.omitTypedProperties ) {
// we do not inject typed properties!
continue;
}
var args = { };
if ( structKeyExists( injection.overrides, property ) ) {
args[ property ] = injection.overrides[ property ];
} else if ( structKeyExists( partialBean.injection, property ) ) {
args[ property ] = partialBean.injection[ property ].bean;
} else if ( structKeyExists( variables, 'parent' ) && variables.parent.containsBean( property ) ) {
args[ property ] = variables.parent.getBean( property );
} else {
missingBean( property, beanName );
continue;
}
evaluate( 'injection.bean.set#property#( argumentCollection = args )' );
}
}
// post-injection, pre-init-method phase:
for ( name in partialBean.injection ) {
injection = partialBean.injection[ name ];
setupInitMethod( name, injection.bean );
}
// see if anything needs post-injection, init-method calls:
for ( var postName in postInjectables ) {
callInitMethod( postName, postInjectables, partialBean, initMethod );
}
variables.resolutionCache[ beanName ] = isSingleton( beanName );
}
return partialBean.bean;
}
private void function callInitMethod( string name, struct injectables, struct info, string method ) {
if ( injectables[ name ] ) {
injectables[ name ] = false; // this ensures we don't try to init the same
// bean twice - and also breaks circular dependencies...
if ( structKeyExists( info.dependencies, name ) ) {
for ( var depName in info.dependencies[ name ] ) {
if ( structKeyExists( injectables, depName ) &&
injectables[ depName ] ) {
callInitMethod( depName, injectables, info, method );
}
}
}
if ( structKeyExists( variables.initMethodCache, name ) &&
variables.initMethodCache[ name ] ) {
} else {
variables.initMethodCache[ name ] = isSingleton( name );
var bean = info.injection[ name ].bean;
evaluate( 'bean.#method#()' );
}
}
}
private struct function resolveBeanCreate( string beanName, struct accumulator ) {
var bean = 0;
if ( structKeyExists( variables.beanInfo, beanName ) ) {
var info = variables.beanInfo[ beanName ];
accumulator.dependencies[ beanName ] = { };
if ( structKeyExists( info, 'cfc' ) ) {
/*******************************************************/
var metaBean = cachable( beanName );
var overrides = structKeyExists( info, 'overrides' ) ? info.overrides : { };
bean = metaBean.bean;
if ( metaBean.newObject ) {
if ( structKeyExists( info.metadata, 'constructor' ) ) {
var args = { };
for ( var arg in info.metadata.constructor ) {
accumulator.dependencies[ beanName ][ arg ] = true;
var argBean = { };
// handle known required arguments
if ( info.metadata.constructor[ arg ] ) {
var beanMissing = true;
if ( structKeyExists( overrides, arg ) ) {
args[ arg ] = overrides[ arg ];
beanMissing = false;
} else if ( containsBean( arg ) ) {
argBean = resolveBeanCreate( arg, accumulator );
if ( structKeyExists( argBean, 'bean' ) ) {
args[ arg ] = argBean.bean;
beanMissing = false;
}
}
if ( beanMissing ) {
throw 'bean not found: #arg#; while resolving constructor arguments for #beanName#';
}
} else {
if ( structKeyExists( overrides, arg ) ) {
args[ arg ] = overrides[ arg ];
} else if ( containsBean( arg ) ) {
// optional but present
argBean = resolveBeanCreate( arg, accumulator );
if ( structKeyExists( argBean, 'bean' ) ) {
args[ arg ] = argBean.bean;
}
} else {
// optional but not present
}
}
}
var __ioc_newBean = evaluate( 'bean.init( argumentCollection = args )' );
// if the constructor returns anything, it becomes the bean
// this allows for smart constructors that return things other
// than the CFC being created, such as implicit factory beans
// and automatic singletons etc (rare practices in CFML but...)
if ( !isNull( __ioc_newBean ) ) {
bean = __ioc_newBean;
forceCache( bean, beanName );
}
}
}
/*******************************************************/
if ( !structKeyExists( accumulator.injection, beanName ) ) {
if ( !structKeyExists( variables.settersInfo, beanName ) ) {
variables.settersInfo[ beanName ] = findSetters( bean, info.metadata );
}
var setterMeta = {
setters = variables.settersInfo[ beanName ].setters,
bean = bean,
overrides = overrides
};
accumulator.injection[ beanName ] = setterMeta;
for ( var property in setterMeta.setters ) {
accumulator.dependencies[ beanName ][ property ] = true;
if ( structKeyExists( overrides, property ) ) {
// skip resolution because we'll inject override
} else {
resolveBeanCreate( property, accumulator );
}
}
}
accumulator.bean = bean;
} else if ( isConstant( beanName ) ) {
accumulator.bean = info.value;
accumulator.injection[ beanName ] = { bean = info.value, setters = { } };
} else if ( structKeyExists( info, 'factory' ) ) {
var fmBean = isSimpleValue( info.factory ) ? this.getBean( info.factory ) : info.factory;
var nArgs = arrayLen( info.args );
var argStruct = { };
for ( var i = 1; i <= nArgs; ++i ) {
var argName = info.args[ i ];
if ( structKeyExists( info.overrides, argName ) ) {
argStruct[ i ] = info.overrides[ argName ];
} else {
argStruct[ i ] = this.getBean( argName );
}
}
accumulator.bean = evaluate( 'fmBean.#info.method#(argumentCollection=argStruct)' );
accumulator.injection[ beanName ] = { bean = accumulator.bean, setters = { } };
} else {
throw 'internal error: invalid metadata for #beanName#';
}
} else if ( structKeyExists( variables, 'parent' ) && variables.parent.containsBean( beanName ) ) {
bean = variables.parent.getBean( beanName );
accumulator.injection[ beanName ] = { bean = bean, setters = { } };
accumulator.bean = bean;
} else {
missingBean( beanName );
}
return accumulator;
}
private void function setupFrameworkDefaults() {
param name = "variables.config.recurse" default = true;
param name = "variables.config.strict" default = false;
if ( !structKeyExists( variables.config, 'exclude' ) ) {
variables.config.exclude = [ ];
}
for ( var elem in variables.autoExclude ) {
arrayAppend( variables.config.exclude, replace( elem, chr(92), '/', 'all' ) );
}
// install bean factory constant:
variables.beanInfo.beanFactory = { value = this, isSingleton = true };
if ( structKeyExists( variables.config, 'constants' ) ) {
for ( var beanName in variables.config.constants ) {
variables.beanInfo[ beanName ] = { value = variables.config.constants[ beanName ], isSingleton = true };
}
}
variables.transients = { };
if ( structKeyExists( variables.config, 'transients' ) ) {
for ( var transientFolder in variables.config.transients ) {
variables.transients[ transientFolder ] = true;
}
}
if ( structKeyExists( variables.config, 'singletonPattern' ) &&
structKeyExists( variables.config, 'transientPattern' ) ) {
throw 'singletonPattern and transientPattern are mutually exclusive';
}
if ( !structKeyExists( variables.config, 'omitTypedProperties' ) ) {
variables.config.omitTypedProperties = false;
}
if ( !structKeyExists( variables.config, 'omitDirectoryAliases' ) ) {
variables.config.omitDirectoryAliases = false;
}
if ( !structKeyExists( variables.config, 'singulars' ) ) {
variables.config.singulars = { };
}
if ( !structKeyExists( variables.config, 'liberal' ) ) {
variables.config.liberal = false;
}
variables.config.version = variables._di1_version;
}
// hook for extension points to process beans after they have been
// constructed and injected, but before init-method is called on anything
private void function setupInitMethod( string name, any bean ) {
}
private string function singular( string plural ) {
if ( structKeyExists( variables.config.singulars, plural ) ) {
return variables.config.singulars[ plural ];
}
var single = plural;
var n = len( plural );
var last = right( plural, 1 );
if ( last == 's' ) {
if ( variables.config.liberal && n > 3 && right( plural, 3 ) == 'ies' ) {
single = left( plural, n - 3 ) & 'y';
} else {
single = left( plural, n - 1 );
}
}
return single;
}
}