initial
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/*.hoth
|
||||
@@ -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' )#'" );
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user