From 324abbdd9899c1d5c682781781c0ad8ce93c093c Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Fri, 9 Apr 2021 10:50:44 -0500 Subject: [PATCH 01/40] fix acf typo --- system/async/proxies/Runnable.cfc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/async/proxies/Runnable.cfc b/system/async/proxies/Runnable.cfc index 6315d332d..50965f13d 100644 --- a/system/async/proxies/Runnable.cfc +++ b/system/async/proxies/Runnable.cfc @@ -16,7 +16,7 @@ component extends="BaseProxy" { required target, method = "run", boolean debug = false, - boolean loadAppContext = true, + boolean loadAppContext = true ){ super.init( arguments.target, From 8e8dd362c22cbe10f80856158e10c55ad37d4f96 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Fri, 9 Apr 2021 12:16:58 -0500 Subject: [PATCH 02/40] making sure thread class loading is not lost on ACF --- system/async/proxies/BaseProxy.cfc | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/system/async/proxies/BaseProxy.cfc b/system/async/proxies/BaseProxy.cfc index 826333b93..cecee5fb9 100644 --- a/system/async/proxies/BaseProxy.cfc +++ b/system/async/proxies/BaseProxy.cfc @@ -109,17 +109,28 @@ component accessors="true" { if ( server.keyExists( "lucee" ) ) { getCFMLContext().setApplicationContext( variables.cfContext ); } else { + // Set the current thread's class loader from the CF space to avoid + // No class defined issues in thread land. + getCurrentThread().setContextClassLoader( variables.originalFusionContext.getClass().getClassLoader() ); + + // Prepare a new context in ACF for the thread var fusionContext = variables.originalFusionContext.clone(); + // Create a new page context for the thread var pageContext = variables.originalPageContext.clone(); + // Reset it's scopes, else bad things happen pageContext.resetLocalScopes(); + // Set the cf context into it + pageContext.setFusionContext( fusionContext ); + fusionContext.pageContext = pageContext; + fusionContext.SymTab_setApplicationScope( variables.originalAppScope ); + + // Create a fake page to run this thread in and link it to the fake page context and fusion context var page = variables.originalPage._clone(); page.pageContext = pageContext; fusionContext.parent = page; + // Set the current context of execution now variables.fusionContextStatic.setCurrent( fusionContext ); - fusionContext.pageContext = pageContext; - fusionContext.SymTab_setApplicationScope( variables.originalAppScope ); - pageContext.setFusionContext( fusionContext ); pageContext.initializeWith( page, pageContext, From c931267b761b4b5b534885668b6c3168f5c48805 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Fri, 9 Apr 2021 12:20:46 -0500 Subject: [PATCH 03/40] full scoping --- system/async/util/TimeUnit.cfc | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/system/async/util/TimeUnit.cfc b/system/async/util/TimeUnit.cfc index cf7ef2eba..7b5fd9432 100644 --- a/system/async/util/TimeUnit.cfc +++ b/system/async/util/TimeUnit.cfc @@ -16,25 +16,25 @@ component singleton { function get( required timeUnit = "milliseconds" ){ switch ( arguments.timeUnit ) { case "days": { - return jTimeUnit.DAYS; + return variables.jTimeUnit.DAYS; } case "hours": { - return jTimeUnit.HOURS; + return variables.jTimeUnit.HOURS; } case "microseconds": { - return jTimeUnit.MICROSECONDS; + return variables.jTimeUnit.MICROSECONDS; } case "milliseconds": { - return jTimeUnit.MILLISECONDS; + return variables.jTimeUnit.MILLISECONDS; } case "minutes": { - return jTimeUnit.MINUTES; + return variables.jTimeUnit.MINUTES; } case "nanoseconds": { - return jTimeUnit.NANOSECONDS; + return variables.jTimeUnit.NANOSECONDS; } case "seconds": { - return jTimeUnit.SECONDS; + return variables.jTimeUnit.SECONDS; } } } From 93c1261d60a67f66964cad321d3aa0721a027662 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Fri, 9 Apr 2021 17:51:21 -0500 Subject: [PATCH 04/40] COLDBOX-992 #resolve Added CFML Duration and Periods to async manager so task executions can be nicer and pin point accuracy --- system/async/AsyncManager.cfc | 23 +- system/async/util/ChronoUnit.cfc | 68 ++++ system/async/util/Duration.cfc | 510 ++++++++++++++++++++++++ system/async/util/Period.cfc | 381 ++++++++++++++++++ tests/specs/async/AsyncManagerSpec.cfc | 11 +- tests/specs/async/util/DurationSpec.cfc | 107 +++++ tests/specs/async/util/PeriodSpec.cfc | 107 +++++ 7 files changed, 1195 insertions(+), 12 deletions(-) create mode 100644 system/async/util/ChronoUnit.cfc create mode 100644 system/async/util/Duration.cfc create mode 100644 system/async/util/Period.cfc create mode 100644 tests/specs/async/util/DurationSpec.cfc create mode 100644 tests/specs/async/util/PeriodSpec.cfc diff --git a/system/async/AsyncManager.cfc b/system/async/AsyncManager.cfc index e3967dc2a..a08092769 100644 --- a/system/async/AsyncManager.cfc +++ b/system/async/AsyncManager.cfc @@ -120,8 +120,7 @@ component accessors="true" singleton { arguments.executor = this.$executors.newScheduledThreadPool( arguments.threads ); return new tasks.ScheduledExecutor( argumentCollection = arguments ); } - default : { - + default: { } } throw( @@ -344,6 +343,10 @@ component accessors="true" singleton { * Utilities * ****************************************************************/ + Duration function duration(){ + return new util.Duration(); + } + /** * Build an array out of a range of numbers or using our range syntax. * You can also build negative ranges @@ -357,22 +360,20 @@ component accessors="true" singleton { * @from The initial index, defaults to 1 or you can use the {start}..{end} notation * @to The last index item */ - array function arrayRange( any from=1, numeric to ){ + array function arrayRange( any from = 1, numeric to ){ // shortcut notation - if( find( "..", arguments.from ) ){ - arguments.to = getToken( arguments.from, 2, ".." ); - arguments.from = getToken( arguments.from, 1, ".." ); + if ( find( "..", arguments.from ) ) { + arguments.to = getToken( arguments.from, 2, ".." ); + arguments.from = getToken( arguments.from, 1, ".." ); } // cap to if larger than from - if( arguments.to < arguments.from ){ + if ( arguments.to < arguments.from ) { arguments.to = arguments.from; } // build it up - return IntStream - .rangeClosed( arguments.from, arguments.to ) - .toArray(); + return IntStream.rangeClosed( arguments.from, arguments.to ).toArray(); } -} \ No newline at end of file +} diff --git a/system/async/util/ChronoUnit.cfc b/system/async/util/ChronoUnit.cfc new file mode 100644 index 000000000..1c0370556 --- /dev/null +++ b/system/async/util/ChronoUnit.cfc @@ -0,0 +1,68 @@ +component singleton { + + // A standard set of date periods units. + variables.jChronoUnit = createObject( "java", "java.time.temporal.ChronoUnit" ); + // Unit that represents the concept of a century. + this.CENTURIES = variables.jChronoUnit.CENTURIES; + // Unit that represents the concept of a day. + this.DAYS = variables.jChronoUnit.DAYS; + // Unit that represents the concept of a decade. + this.DECADES = variables.jChronoUnit.DECADES; + // Unit that represents the concept of an era. + this.ERAS = variables.jChronoUnit.ERAS; + // Artificial unit that represents the concept of forever. + this.FOREVER = variables.jChronoUnit.FOREVER; + // Unit that represents the concept of half a day, as used in AM/PM. + this.HALF_DAYS = variables.jChronoUnit.HALF_DAYS; + // Unit that represents the concept of an hour. + this.HOURS = variables.jChronoUnit.HOURS; + // Unit that represents the concept of a microsecond. + this.MICROS = variables.jChronoUnit.MICROS; + // Unit that represents the concept of a millennium. + this.MILLENNIA = variables.jChronoUnit.MILLENNIA; + // Unit that represents the concept of a millisecond. + this.MILLIS = variables.jChronoUnit.MILLIS; + // Unit that represents the concept of a minute. + this.MINUTES = variables.jChronoUnit.MINUTES; + // Unit that represents the concept of a month. + this.MONTHS = variables.jChronoUnit.MONTHS; + // Unit that represents the concept of a nanosecond, the smallest supported unit of time. + this.NANOS = variables.jChronoUnit.NANOS; + // Unit that represents the concept of a second. + this.SECONDS = variables.jChronoUnit.SECONDS; + // Unit that represents the concept of a week. + this.WEEKS = variables.jChronoUnit.WEEKS; + // Unit that represents the concept of a year. + this.YEARS = variables.jChronoUnit.YEARS; + + /** + * Convert any date/time or string date/time object to a Java instant temporal object + * + * @target The date/time or string object representing the date/time + * + * @return A Java temporal object as java.time.Instant + */ + function toInstant( required target ){ + // Is this a date/time object or a string? + if ( findNoCase( "string", arguments.target.getClass().getName() ) ) { + arguments.target = createODBCDateTime( arguments.target ); + } + return arguments.target.toInstant(); + } + + /** + * Convert any date/time or string date/time object to a Java LocalDate + * + * @target The date/time or string object representing the date/time + * + * @return A java LocalDate object + */ + function toLocalDate( required target ){ + // Is this a date/time object or a string? + if ( findNoCase( "string", arguments.target.getClass().getName() ) ) { + arguments.target = createODBCDateTime( arguments.target ); + } + return arguments.target; + } + +} diff --git a/system/async/util/Duration.cfc b/system/async/util/Duration.cfc new file mode 100644 index 000000000..d032368c9 --- /dev/null +++ b/system/async/util/Duration.cfc @@ -0,0 +1,510 @@ +/** + * A time-based amount of time, such as '34.5 seconds'. + * + * This class models a quantity or amount of time in terms of seconds and nanoseconds. + * It can be accessed using other duration-based units, such as minutes and hours. + * In addition, the DAYS unit can be used and is treated as exactly equal to 24 hours, thus ignoring daylight savings effects. + * See Period for the date-based equivalent to this class. + * + * Static class to map to the Java JDK Duration static class with some CF Goodness + * + * @see https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/Duration.html + */ +component accessors="true" { + + // The static java class we represent + variables.jDuration = createObject( "java", "java.time.Duration" ); + // A standard set of date periods units. + this.CHRONO_UNIT = new ChronoUnit(); + + /** + * Initialize to zero + */ + function init(){ + return this.of( 0 ); + } + + /** + * -------------------------------------------------------------------------- + * Utility Methods + * -------------------------------------------------------------------------- + */ + + /** + * Get the native java class we proxy to. + * + * @return java.time.Duration + */ + any function getNative(){ + return variables.jDuration; + } + + /** + * Returns a copy of this duration with a positive length. + */ + Duration function abs(){ + variables.jDuration = variables.jDuration.abs(); + return this; + } + + /** + * Checks if the duration is negative, excluding zero + */ + boolean function isNegative(){ + return variables.jDuration.isNegative(); + } + + /** + * Checks if the duration is zero length + */ + boolean function isZero(){ + return variables.jDuration.isZero(); + } + + /** + * Checks if this duration is equal to the specified Duration. + * + * @otherDuration The duration to compare against + */ + boolean function isEquals( required Duration otherDuration ){ + return variables.jDuration.equals( arguments.otherDuration.getNative() ); + } + + /** + * Returns a copy of this duration with the length negated. + */ + Duration function negated(){ + variables.jDuration = variables.jDuration.negated(); + return this; + } + + /** + * -------------------------------------------------------------------------- + * Retrieval Methods + * -------------------------------------------------------------------------- + */ + + /** + * Gets the value of the requested unit in seconds by default or by nanoseconds + * + * @unit Seconds or nano + * + * @throws UnsupportedTemporalTypeException This returns a value for each of the two supported units, SECONDS and NANOS. All other units throw an exception. + */ + numeric function get( unit = "seconds" ){ + return variables.jDuration.get( this.CHRONO_UNIT[ arguments.unit ] ); + } + + /** + * Gets the number of seconds in this duration. + */ + numeric function getSeconds(){ + return this.get(); + } + + /** + * Gets the number of nano seconds in this duration. + */ + numeric function getNano(){ + return this.get( "nanos" ); + } + + /** + * Gets the set (array) of units supported by this duration. + */ + array function getUnits(){ + return arrayMap( variables.jDuration.getUnits(), function( thisItem ){ + return arguments.thisItem.name(); + } ); + } + + /** + * Gets the number of days in this duration. + */ + numeric function toDays(){ + return variables.jDuration.toDays(); + } + + /** + * Extracts the number of days in the duration. + */ + numeric function toDaysPart(){ + return variables.jDuration.toDaysPart(); + } + + /** + * Gets the number of Hours in this duration. + */ + numeric function toHours(){ + return variables.jDuration.toHours(); + } + + /** + * Extracts the number of HoursPart in this duration. + */ + numeric function toHoursPart(){ + return variables.jDuration.toHoursPart(); + } + + /** + * Gets the number of Millis in this duration. + */ + numeric function toMillis(){ + return variables.jDuration.toMillis(); + } + + /** + * Extracts the number of MillisPart in this duration. + */ + numeric function toMillisPart(){ + return variables.jDuration.toMillisPart(); + } + + /** + * Gets the number of Minutes in this duration. + */ + numeric function toMinutes(){ + return variables.jDuration.toMinutes(); + } + + /** + * Extracts the number of MinutesPart in this duration. + */ + numeric function toMinutesPart(){ + return variables.jDuration.toMinutesPart(); + } + + /** + * Gets the number of Nanos in this duration. + */ + numeric function toNanos(){ + return variables.jDuration.toNanos(); + } + + /** + * Extracts the number of NanosPart in this duration. + */ + numeric function toNanosPart(){ + return variables.jDuration.toNanosPart(); + } + + /** + * Gets the number of Seconds in this duration. + */ + numeric function toSeconds(){ + return variables.jDuration.toSeconds(); + } + + /** + * Extracts the number of SecondsPart in this duration. + */ + numeric function toSecondsPart(){ + return variables.jDuration.toSecondsPart(); + } + + /** + * A string representation of this duration using ISO-8601 seconds based representation, such as PT8H6M12.345S. + */ + string function toString(){ + return variables.jDuration.toString(); + } + + /** + * -------------------------------------------------------------------------- + * Operations Methods + * -------------------------------------------------------------------------- + */ + + /** + * Adds this duration to the specified temporal object and return back to you a date/time object + * + * @target The date/time object or string to incorporate the duration into + * @asInstant Return the result either as a date/time string or a java.time.Instant object + * + * @return Return the result either as a date/time string or a java.time.Instant object + */ + function addTo( required target, boolean asInstant = false ){ + var results = variables.jDuration.addTo( this.CHRONO_UNIT.toInstant( arguments.target ) ); + return ( arguments.asInstant ? results : results.toString() ); + } + + /** + * Subtracts this duration to the specified temporal object and return back to you a date/time object + * + * @target The date/time object or string to subtract the duration from + * @asInstant Return the result either as a date/time string or a java.time.Instant object + * + * @return Return the result either as a date/time string or a java.time.Instant object + */ + function subtractFrom( required target, boolean asInstant = false ){ + var results = variables.jDuration.subtractFrom( this.CHRONO_UNIT.toInstant( arguments.target ) ); + return ( arguments.asInstant ? results : results.toString() ); + } + + /** + * Obtains a Duration representing the duration between two temporal date objects. + * + * This calculates the duration between two temporal objects. If the objects are of different types, + * then the duration is calculated based on the type of the first object. For example, if the first argument is a + * LocalTime then the second argument is converted to a LocalTime. + * + * @start The start date/time object + * @end The end date/time object + */ + Duration function between( required start, required end ){ + // Do it! + variables.jDuration = variables.jDuration.between( + this.CHRONO_UNIT.toInstant( arguments.start ), + this.CHRONO_UNIT.toInstant( arguments.end ) + ); + return this; + } + + /** + * Compares this duration to the specified Duration. + * + * The comparison is based on the total length of the durations. It is "consistent with equals", as defined by Comparable. + * + * @otherDuration the other duration to compare to + * + * @return -1 if the duration is less than the otherDuration, 0 if equals, and 1 if greater than + */ + function compareTo( required Duration otherDuration ){ + return variables.jDuration.compareTo( arguments.otherDuration.getNative() ); + } + + /** + * Returns a copy of this duration divided by the specified value. + * + * @divisor Divide by what? + */ + Duration function dividedBy( required divisor ){ + variables.jDuration = variables.jDuration.dividedBy( javacast( "long", arguments.divisor ) ); + return this; + } + + /** + * Returns a copy of this duration with the specified duration subtracted. + * + * @amountToSubtract The amount to subtract + * @unit The units to use + */ + Duration function minus( required amountToSubtract, unit = "seconds" ){ + variables.jDuration = variables.jDuration.minus( + javacast( "long", arguments.amountToSubtract ), + this.CHRONO_UNIT[ arguments.unit ] + ); + return this; + } + + Duration function minusDays( required daysToSubtract ){ + return this.minus( arguments.daysToSubtract, "days" ); + } + + Duration function minusHours( required hoursToSubtract ){ + return this.minus( arguments.hoursToSubtract, "hours" ); + } + + Duration function minusMillis( required millisToSubtract ){ + return this.minus( arguments.millisToSubtract, "millis" ); + } + + Duration function minusMinutes( required minutesToSubtract ){ + return this.minus( arguments.minutesToSubtract, "minutes" ); + } + + Duration function minusNanos( required nanosToSubtract ){ + return this.minus( arguments.nanosToSubtract, "nanos" ); + } + + Duration function minusSeconds( required secondsToSubtract ){ + return this.minus( arguments.secondsToSubtract, "seconds" ); + } + + Duration function multipliedBy( required multiplicand ){ + variables.jDuration = variables.jDuration.multipliedBy( javacast( "long", arguments.multiplicand ) ); + return this; + } + + /** + * Returns a copy of this duration with the specified duration added. + * + * @amountToAdd The amount to add + * @unit The units to use + */ + Duration function plus( required amountToAdd, unit = "seconds" ){ + variables.jDuration = variables.jDuration.plus( + javacast( "long", arguments.amountToAdd ), + this.CHRONO_UNIT[ arguments.unit ] + ); + return this; + } + + Duration function plusDays( required daysToAdd ){ + return this.plus( arguments.daysToAdd, "days" ); + } + + Duration function plusHours( required hoursToAdd ){ + return this.plus( arguments.hoursToAdd, "hours" ); + } + + Duration function plusMillis( required millisToAdd ){ + return this.plus( arguments.millisToAdd, "millis" ); + } + + Duration function plusMinutes( required minutesToAdd ){ + return this.plus( arguments.minutesToAdd, "minutes" ); + } + + Duration function plusNanos( required nanosToAdd ){ + return this.plus( arguments.nanosToAdd, "nanos" ); + } + + Duration function plusSeconds( required secondsToAdd ){ + return this.plus( arguments.secondsToAdd, "seconds" ); + } + + /** + * -------------------------------------------------------------------------- + * Creation Methods + * -------------------------------------------------------------------------- + */ + + /** + * Returns a copy of this duration with the specified nano-of-second. + * This returns a duration with the specified nano-of-second, retaining the seconds part of this duration. + * + * @nanoOfSecond the nano-of-second to represent, from 0 to 999,999,999 + */ + function withNanos( required nanoOfSecond ){ + variables.jDuration = variables.jDuration.withNanos( javacast( "long", arguments.nanoOfSecond ) ); + return this; + } + + /** + * Returns a copy of this duration with the specified amount of seconds. + * This returns a duration with the specified seconds, retaining the nano-of-second part of this duration. + * + * @seconds the seconds to represent, may be negative + */ + function withSeconds( required seconds ){ + variables.jDuration = variables.jDuration.withSeconds( javacast( "long", arguments.seconds ) ); + return this; + } + + /** + * Obtains a Duration from a text string such as PnDTnHnMn.nS. + * + * This will parse a textual representation of a duration, including the string produced by toString(). The formats accepted are based on the ISO-8601 duration format PnDTnHnMn.nS with days considered to be exactly 24 hours. + * + * Examples: + * "PT20.345S" -- parses as "20.345 seconds" + * "PT15M" -- parses as "15 minutes" (where a minute is 60 seconds) + * "PT10H" -- parses as "10 hours" (where an hour is 3600 seconds) + * "P2D" -- parses as "2 days" (where a day is 24 hours or 86400 seconds) + * "P2DT3H4M" -- parses as "2 days, 3 hours and 4 minutes" + * "PT-6H3M" -- parses as "-6 hours and +3 minutes" + * "-PT6H3M" -- parses as "-6 hours and -3 minutes" + * "-PT-6H+3M" -- parses as "+6 hours and -3 minutes" + * + * @see https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/Duration.html#parse(java.lang.CharSequence) + * + * @text The string to parse and build up to a duration + */ + Duration function parse( required text ){ + variables.jDuration = variables.jDuration.parse( arguments.text ); + return this; + } + + /** + * Obtains an instance of Duration from another duration + * + * @amount + */ + function from( required Duration amount ){ + variables.jDuration = variables.jDuration.from( arguments.amount.getNative() ); + return this; + } + + /** + * Obtains a Duration representing an amount in the specified unit (seconds) + * + * @amount the amount of the duration, measured in terms of the unit, positive or negative + * @unit The time unit: CENTURIES,DAYS,DECADES,ERAS,FOREVER,HALF_DAYS,HOURS,MICROS,MILLENNIA,MILLIS,MINUTES,MONTHS,NANOS,SECONDS,WEEKS,YEARS + */ + Duration function of( required amount, unit = "seconds" ){ + variables.jDuration = variables.jDuration.of( + javacast( "long", arguments.amount ), + this.CHRONO_UNIT[ arguments.unit ] + ); + return this; + } + + /** + * Obtains a Duration representing a number of standard 24 hour days. + * + * @days The number of days, positive or negative + */ + Duration function ofDays( required days ){ + return this.of( arguments.days, "days" ); + } + + /** + * Obtains a Duration representing a number of standard hours. + * + * @hours The number of hours, positive or negative + */ + Duration function ofHours( required hours ){ + return this.of( arguments.hours, "hours" ); + } + + /** + * Obtains a Duration representing a number of standard minutes + * + * @minutes The number of minutes, positive or negative + */ + Duration function ofMinutes( required minutes ){ + return this.of( arguments.minutes, "minutes" ); + } + + /** + * Obtains a Duration representing a number of seconds and/or an adjustment in nanoseconds. + * + * This method allows an arbitrary number of nanoseconds to be passed in. + * The factory will alter the values of the second and nanosecond in order to ensure that the stored nanosecond is in the range + * 0 to 999,999,999. For example, the following will result in exactly the same duration: + * + * @seconds The number of seconds, positive or negative + * @nanoAdjustment the nanosecond adjustment to the number of seconds, positive or negative + */ + Duration function ofSeconds( required seconds, nanoAdjustment ){ + if ( isNull( arguments.nanoAdjustment ) ) { + return this.of( arguments.seconds, "seconds" ); + } + // Width Adjustment + variables.jDuration = variables.jDuration.ofSeconds( + javacast( "long", arguments.seconds ), + javacast( "long", arguments.nanoAdjustment ) + ); + return this; + } + + /** + * Obtains a Duration representing a number of standard milliseconds + * + * @millis The number of millis, positive or negative + */ + Duration function ofMillis( required millis ){ + return this.of( arguments.millis, "millis" ); + } + + /** + * Obtains a Duration representing a number of standard nanoseconds + * + * @nanos The number of nanos, positive or negative + */ + Duration function ofNanos( required nanos ){ + return this.of( arguments.nanos, "nanos" ); + } + +} diff --git a/system/async/util/Period.cfc b/system/async/util/Period.cfc new file mode 100644 index 000000000..6bd6fd4ba --- /dev/null +++ b/system/async/util/Period.cfc @@ -0,0 +1,381 @@ +/** + * A date-based amount of time in the ISO-8601 calendar system, such as '2 years, 3 months and 4 days'. + * + * This class models a quantity or amount of time in terms of years, months and days. See Duration for the time-based equivalent to this class. + * + * Durations and periods differ in their treatment of daylight savings time when added to ZonedDateTime. + * A Duration will add an exact number of seconds, thus a duration of one day is always exactly 24 hours. By contrast, a Period will add a + * conceptual day, trying to maintain the local time. + * + * For example, consider adding a period of one day and a duration of one day to 18:00 on the evening before a daylight savings gap. + * The Period will add the conceptual day and result in a ZonedDateTime at 18:00 the following day. By contrast, the Duration will add exactly + * 24 hours, resulting in a ZonedDateTime at 19:00 the following day (assuming a one hour DST gap). + * + * @see https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/Period.html + */ +component accessors="true" { + + // The static java class we represent + variables.jPeriod = createObject( "java", "java.time.Period" ); + // A standard set of date periods units. + this.CHRONO_UNIT = new ChronoUnit(); + + /** + * Initialize to zero date base period + * + * @years The years + * @months The months + * @days The days + */ + Period function init( years = 0, months = 0, days = 0 ){ + return this.of( argumentCollection = arguments ); + } + + /** + * -------------------------------------------------------------------------- + * Utility Methods + * -------------------------------------------------------------------------- + */ + + /** + * Get the native java class we proxy to. + * + * @return java.time.Period + */ + any function getNative(){ + return variables.jPeriod; + } + + /** + * Checks if the period is negative, excluding zero + */ + boolean function isNegative(){ + return variables.jPeriod.isNegative(); + } + + /** + * Checks if the period is zero length + */ + boolean function isZero(){ + return variables.jPeriod.isZero(); + } + + /** + * Checks if this period is equal to the specified period. + * + * @otherPeriod The period to compare against + */ + boolean function isEquals( required Period otherPeriod ){ + return variables.jPeriod.equals( arguments.otherPeriod.getNative() ); + } + + /** + * Returns a copy of this duration with the length negated. + */ + Period function negated(){ + variables.jPeriod = variables.jPeriod.negated(); + return this; + } + + /** + * Returns a copy of this period with the years and months normalized. + */ + Period function normalized(){ + variables.jPeriod = variables.jPeriod.normalized(); + return this; + } + + /** + * -------------------------------------------------------------------------- + * Retrieval Methods + * -------------------------------------------------------------------------- + */ + + /** + * Gets the value of the requested unit in seconds by default (days) or years, months, days + * + * @unit years, months, days + * + * @throws UnsupportedTemporalTypeException All other units throw an exception. + */ + numeric function get( unit = "days" ){ + return variables.jPeriod.get( this.CHRONO_UNIT[ arguments.unit ] ); + } + + /** + * Gets the chronology of this period, which is the ISO calendar system. + * + * @return java.time.chrono.IsoChronology + */ + function getChronology(){ + return variables.jPeriod.getChronology(); + } + + /** + * Gets the number of Days in this period. + */ + numeric function getDays(){ + return this.get(); + } + + /** + * Gets the number of Months in this period. + */ + numeric function getMonths(){ + return this.get( "months" ); + } + + /** + * Gets the number of Years in this period. + */ + numeric function getYears(){ + return this.get( "years" ); + } + + /** + * Gets the set (array) of units supported by this period. + */ + array function getUnits(){ + return arrayMap( variables.jPeriod.getUnits(), function( thisItem ){ + return arguments.thisItem.name(); + } ); + } + + /** + * Gets the total number of months in this period. + */ + numeric function toTotalMonths(){ + return variables.jPeriod.toTotalMonths(); + } + + /** + * Outputs this period as a String, such as P6Y3M1D. + */ + string function toString(){ + return variables.jPeriod.toString(); + } + + /** + * -------------------------------------------------------------------------- + * Operations Methods + * -------------------------------------------------------------------------- + */ + + /** + * Adds this period to the specified temporal object and return back to you a date/time object + * + * @target The date/time object or string to incorporate the period into + * @asInstant Return the result either as a date/time string or a java.time.Instant object + * + * @return Return the result either as a date/time string or a java.time.Instant object + */ + function addTo( required target, boolean asInstant = false ){ + var results = variables.jPeriod.addTo( this.CHRONO_UNIT.toInstant( arguments.target ) ); + return ( arguments.asInstant ? results : results.toString() ); + } + + /** + * Subtracts this period to the specified temporal object and return back to you a date/time object + * + * @target The date/time object or string to subtract the period from + * @asInstant Return the result either as a date/time string or a java.time.Instant object + * + * @return Return the result either as a date/time string or a java.time.Instant object + */ + function subtractFrom( required target, boolean asInstant = false ){ + var results = variables.jPeriod.subtractFrom( this.CHRONO_UNIT.toInstant( arguments.target ) ); + return ( arguments.asInstant ? results : results.toString() ); + } + + /** + * Obtains a Duration representing the duration between two temporal date objects. + * + * This calculates the duration between two temporal objects. If the objects are of different types, + * then the duration is calculated based on the type of the first object. For example, if the first argument is a + * LocalTime then the second argument is converted to a LocalTime. + * + * @start The start date/time object + * @end The end date/time object + */ + Period function between( required start, required end ){ + // Do it! + variables.jPeriod = variables.jPeriod.between( + this.CHRONO_UNIT.toLocalDate( arguments.start ), + this.CHRONO_UNIT.toLocalDate( arguments.end ) + ); + return this; + } + + /** + * Returns a copy of this period with the specified period subtracted. + * + * @amountToSubtract The period to subtract + */ + Period function minus( required Period amountToSubtract ){ + variables.jPeriod = variables.jPeriod.minus( arguments.amountToSubtract.getNative() ); + return this; + } + + Period function minusDays( required daysToSubtract ){ + variables.jPeriod = variables.jPeriod.minusDays( javacast( "long", arguments.daysToSubtract ) ); + return this; + } + + Period function minusMonths( required monthsToSubtract ){ + variables.jPeriod = variables.jPeriod.minusMonths( javacast( "long", arguments.monthsToSubtract ) ); + return this; + } + + Period function minusYears( required yearsToSubtract ){ + variables.jPeriod = variables.jPeriod.minusYears( javacast( "long", arguments.yearsToSubtract ) ); + return this; + } + + Period function multipliedBy( required scalar ){ + variables.jPeriod = variables.jPeriod.multipliedBy( javacast( "int", arguments.scalar ) ); + return this; + } + + /** + * Returns a copy of this period with the specified period added. + * + * @amountToAdd The period to Add + */ + Period function plus( required Period amountToAdd ){ + variables.jPeriod = variables.jPeriod.plus( arguments.amountToAdd.getNative() ); + return this; + } + + Period function plusDays( required daysToAdd ){ + variables.jPeriod = variables.jPeriod.plusDays( javacast( "long", arguments.daysToAdd ) ); + return this; + } + + Period function plusMonths( required monthsToAdd ){ + variables.jPeriod = variables.jPeriod.plusMonths( javacast( "long", arguments.monthsToAdd ) ); + return this; + } + + Period function plusYears( required yearsToAdd ){ + variables.jPeriod = variables.jPeriod.plusYears( javacast( "long", arguments.yearsToAdd ) ); + return this; + } + + /** + * -------------------------------------------------------------------------- + * Creation Methods + * -------------------------------------------------------------------------- + */ + + /** + * Returns a copy of this period with the specified amount of days. + * + * @days The days + */ + function withDays( required days ){ + variables.jPeriod = variables.jPeriod.withDays( javacast( "long", arguments.days ) ); + return this; + } + + /** + * Returns a copy of this period with the specified amount of months. + * + * @months The months + */ + function withMonths( required months ){ + variables.jPeriod = variables.jPeriod.withMonths( javacast( "long", arguments.months ) ); + return this; + } + + /** + * Returns a copy of this period with the specified amount of years. + * + * @years The years + */ + function withYears( required years ){ + variables.jPeriod = variables.jPeriod.withYears( javacast( "long", arguments.years ) ); + return this; + } + + /** + * Obtains a Period from a text string such as PnYnMnD. + * + * This will parse the string produced by toString() which is based on the ISO-8601 period formats PnYnMnD and PnW. + * + * Examples: + * + * "P2Y" -- Period.ofYears(2) + * "P3M" -- Period.ofMonths(3) + * "P4W" -- Period.ofWeeks(4) + * "P5D" -- Period.ofDays(5) + * "P1Y2M3D" -- Period.of(1, 2, 3) + * "P1Y2M3W4D" -- Period.of(1, 2, 25) + * "P-1Y2M" -- Period.of(-1, 2, 0) + * "-P1Y2M" -- Period.of(-1, -2, 0) + * + * @see https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/Period.html#parse(java.lang.CharSequence) + * + * @text The string to parse and build up to a period + */ + Period function parse( required text ){ + variables.jPeriod = variables.jPeriod.parse( arguments.text ); + return this; + } + + /** + * Obtains an instance of Period from another period + * + * @amount The period + */ + function from( required Period amount ){ + variables.jPeriod = variables.jPeriod.from( arguments.amount.getNative() ); + return this; + } + + /** + * Obtains a Period representing an amount in the specified arguments + * + * @years The years + * @months The months + * @days The days + */ + Period function of( years = 0, months = 0, days = 0 ){ + variables.jPeriod = variables.jPeriod.of( + javacast( "long", arguments.years ), + javacast( "long", arguments.months ), + javacast( "long", arguments.days ) + ); + return this; + } + + /** + * Obtains a Period representing a number of days. + * + * @days The number of days, positive or negative + */ + Period function ofDays( required days ){ + variables.jPeriod = variables.jPeriod.ofDays( javacast( "long", arguments.days ) ); + return this; + } + + /** + * Obtains a Period representing a number of months. + * + * @months The number of months, positive or negative + */ + Period function ofMonths( required months ){ + variables.jPeriod = variables.jPeriod.ofMonths( javacast( "long", arguments.days ) ); + return this; + } + + /** + * Obtains a Period representing a number of years. + * + * @years The number of years, positive or negative + */ + Period function ofYears( required years ){ + variables.jPeriod = variables.jPeriod.ofYears( javacast( "long", arguments.days ) ); + return this; + } + +} diff --git a/tests/specs/async/AsyncManagerSpec.cfc b/tests/specs/async/AsyncManagerSpec.cfc index 55351e672..15405b30e 100644 --- a/tests/specs/async/AsyncManagerSpec.cfc +++ b/tests/specs/async/AsyncManagerSpec.cfc @@ -14,6 +14,15 @@ component extends="BaseAsyncSpec" { asyncManager = new coldbox.system.async.AsyncManager( debug = true ); } ); + it( "can produce durations", function(){ + expect( + asyncManager + .duration() + .of( 10 ) + .get() + ).toBe( 10 ); + } ); + it( "can run a cf closure with a then/get pipeline and custom executors", function(){ var singlePool = asyncManager.$executors.newFixedThreadPool( 1 ); var f = asyncManager @@ -173,7 +182,7 @@ component extends="BaseAsyncSpec" { debug( "calculating BMI" ); var combinedFuture = weightFuture.thenCombine( heightFuture, function( weight, height ){ - writeDump( var = arguments, output="console" ); + writeDump( var = arguments, output = "console" ); var heightInMeters = arguments.height / 100; return arguments.weight / ( heightInMeters * heightInMeters ); } ); diff --git a/tests/specs/async/util/DurationSpec.cfc b/tests/specs/async/util/DurationSpec.cfc new file mode 100644 index 000000000..acadef4ce --- /dev/null +++ b/tests/specs/async/util/DurationSpec.cfc @@ -0,0 +1,107 @@ +/** + * Duration Specs + */ +component extends="tests.specs.async.BaseAsyncSpec" { + + /*********************************** BDD SUITES ***********************************/ + + function run( testResults, testBox ){ + // all your suites go here. + describe( "Duration", function(){ + beforeEach( function( currentSpec ){ + duration = new coldbox.system.async.util.Duration(); + } ); + + it( "can be created", function(){ + expect( duration ).toBeComponent(); + } ); + + it( "can do creations with of methods", function(){ + expect( duration.ofDays( 7 ).toString() ).toBe( "PT168H" ); + expect( duration.ofHours( 8 ).toString() ).toBe( "PT8H" ); + expect( duration.ofMinutes( 15 ).toString() ).toBe( "PT15M" ); + expect( duration.ofSeconds( 10 ).toString() ).toBe( "PT10S" ); + expect( duration.ofSeconds( 30, 123456789 ).toString() ).toBe( "PT30.123456789S" ); + expect( duration.between( "2017-10-03T10:15:30.00Z", "2017-10-03T10:16:30.00Z" ).getSeconds() ).toBe( + 60 + ); + expect( duration.between( "2021-01-01 00:00:00", "2021-01-01 00:00:01" ).toString() ).toBe( + "PT1S" + ); + expect( + duration + .between( createDateTime( 2019, 1, 1, 0, 0, 0 ), createDateTime( 2021, 1, 1, 0, 0, 0 ) ) + .toString() + ).toBe( "PT17544H" ); + } ); + + it( "can parse strings into durations", function(){ + var d = duration.parse( "P1DT8H15M10.345000S" ); + expect( d.getSeconds() ).toBe( 116110 ); + expect( d.getNano() ).toBe( 345000000 ); + expect( d.getUnits() ).toInclude( "seconds" ).toInclude( "nanos" ); + } ); + + it( "can check utility methods", function(){ + expect( duration.iszero() ).toBeTrue(); + expect( duration.get() ).toBe( 0 ); + expect( duration.getSeconds() ).toBe( 0 ); + expect( duration.getNano() ).toBe( 0 ); + expect( duration.isNegative() ).toBeFalse(); + } ); + + it( "can do minus", function(){ + var d = duration.of( 10 ); + expect( duration.minus( 1 ).get() ).toBe( 9 ); + } ); + + it( "can do plus", function(){ + var d = duration.of( 10 ); + expect( duration.plus( 1 ).get() ).toBe( 11 ); + } ); + + it( "can do multiplication", function(){ + var d = duration.of( 10 ); + expect( duration.multipliedBy( 10 ).get() ).toBe( 100 ); + } ); + + it( "can do division", function(){ + var d = duration.of( 10 ); + expect( duration.dividedBy( 10 ).get() ).toBe( 1 ); + } ); + + it( "can do negations", function(){ + var d = duration.of( -1 ); + expect( d.negated().isNegative() ).toBeFalse(); + + var d = duration.of( -1 ); + expect( d.isNegative() ).toBeTrue(); + expect( d.abs().isNegative() ).toBeFalse(); + } ); + + it( "can create it from another duration", function(){ + var test = duration.ofSeconds( 5 ); + expect( duration.from( test ).get() ).toBe( 5 ); + } ); + + it( "can convert to other time units", function(){ + expect( duration.ofMinutes( 60 ).toHours() ).toBe( 1 ); + expect( duration.ofMinutes( 60 ).toSeconds() ).toBe( 60 * 60 ); + expect( duration.ofMinutes( 60 ).toMillis() ).toBe( 60 * 60 * 1000 ); + } ); + + it( "can add durations to date/time objects", function(){ + var now = now(); + var t = duration.ofDays( 10 ).addTo( now ); + expect( dateDiff( "d", now, t ) ).toBe( 10 ); + } ); + + it( "can subtract durations to date/time objects", function(){ + var now = now(); + var t = duration.ofDays( 10 ).subtractFrom( now ); + expect( dateDiff( "d", now, t ) ).toBe( -10 ); + } ); + } ); + } + +} diff --git a/tests/specs/async/util/PeriodSpec.cfc b/tests/specs/async/util/PeriodSpec.cfc new file mode 100644 index 000000000..acadef4ce --- /dev/null +++ b/tests/specs/async/util/PeriodSpec.cfc @@ -0,0 +1,107 @@ +/** + * Duration Specs + */ +component extends="tests.specs.async.BaseAsyncSpec" { + + /*********************************** BDD SUITES ***********************************/ + + function run( testResults, testBox ){ + // all your suites go here. + describe( "Duration", function(){ + beforeEach( function( currentSpec ){ + duration = new coldbox.system.async.util.Duration(); + } ); + + it( "can be created", function(){ + expect( duration ).toBeComponent(); + } ); + + it( "can do creations with of methods", function(){ + expect( duration.ofDays( 7 ).toString() ).toBe( "PT168H" ); + expect( duration.ofHours( 8 ).toString() ).toBe( "PT8H" ); + expect( duration.ofMinutes( 15 ).toString() ).toBe( "PT15M" ); + expect( duration.ofSeconds( 10 ).toString() ).toBe( "PT10S" ); + expect( duration.ofSeconds( 30, 123456789 ).toString() ).toBe( "PT30.123456789S" ); + expect( duration.between( "2017-10-03T10:15:30.00Z", "2017-10-03T10:16:30.00Z" ).getSeconds() ).toBe( + 60 + ); + expect( duration.between( "2021-01-01 00:00:00", "2021-01-01 00:00:01" ).toString() ).toBe( + "PT1S" + ); + expect( + duration + .between( createDateTime( 2019, 1, 1, 0, 0, 0 ), createDateTime( 2021, 1, 1, 0, 0, 0 ) ) + .toString() + ).toBe( "PT17544H" ); + } ); + + it( "can parse strings into durations", function(){ + var d = duration.parse( "P1DT8H15M10.345000S" ); + expect( d.getSeconds() ).toBe( 116110 ); + expect( d.getNano() ).toBe( 345000000 ); + expect( d.getUnits() ).toInclude( "seconds" ).toInclude( "nanos" ); + } ); + + it( "can check utility methods", function(){ + expect( duration.iszero() ).toBeTrue(); + expect( duration.get() ).toBe( 0 ); + expect( duration.getSeconds() ).toBe( 0 ); + expect( duration.getNano() ).toBe( 0 ); + expect( duration.isNegative() ).toBeFalse(); + } ); + + it( "can do minus", function(){ + var d = duration.of( 10 ); + expect( duration.minus( 1 ).get() ).toBe( 9 ); + } ); + + it( "can do plus", function(){ + var d = duration.of( 10 ); + expect( duration.plus( 1 ).get() ).toBe( 11 ); + } ); + + it( "can do multiplication", function(){ + var d = duration.of( 10 ); + expect( duration.multipliedBy( 10 ).get() ).toBe( 100 ); + } ); + + it( "can do division", function(){ + var d = duration.of( 10 ); + expect( duration.dividedBy( 10 ).get() ).toBe( 1 ); + } ); + + it( "can do negations", function(){ + var d = duration.of( -1 ); + expect( d.negated().isNegative() ).toBeFalse(); + + var d = duration.of( -1 ); + expect( d.isNegative() ).toBeTrue(); + expect( d.abs().isNegative() ).toBeFalse(); + } ); + + it( "can create it from another duration", function(){ + var test = duration.ofSeconds( 5 ); + expect( duration.from( test ).get() ).toBe( 5 ); + } ); + + it( "can convert to other time units", function(){ + expect( duration.ofMinutes( 60 ).toHours() ).toBe( 1 ); + expect( duration.ofMinutes( 60 ).toSeconds() ).toBe( 60 * 60 ); + expect( duration.ofMinutes( 60 ).toMillis() ).toBe( 60 * 60 * 1000 ); + } ); + + it( "can add durations to date/time objects", function(){ + var now = now(); + var t = duration.ofDays( 10 ).addTo( now ); + expect( dateDiff( "d", now, t ) ).toBe( 10 ); + } ); + + it( "can subtract durations to date/time objects", function(){ + var now = now(); + var t = duration.ofDays( 10 ).subtractFrom( now ); + expect( dateDiff( "d", now, t ) ).toBe( -10 ); + } ); + } ); + } + +} From bcd701f68d13e3de99426c01184f94fac07eafd9 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Sat, 10 Apr 2021 12:43:15 -0500 Subject: [PATCH 05/40] COLDBOX-993 #resolve New async.time package to deal with periods, durations, time offsets and so much more --- system/async/AsyncManager.cfc | 2 +- system/async/Future.cfc | 119 ++++++--------- system/async/tasks/Executor.cfc | 2 +- system/async/tasks/FutureTask.cfc | 2 +- system/async/time/ChronoUnit.cfc | 90 +++++++++++ system/async/{util => time}/Duration.cfc | 0 system/async/{util => time}/Period.cfc | 52 ++++--- system/async/{util => time}/TimeUnit.cfc | 0 system/async/util/ChronoUnit.cfc | 68 --------- tests/specs/async/util/DurationSpec.cfc | 2 +- tests/specs/async/util/PeriodSpec.cfc | 181 +++++++++++++++-------- tests/specs/async/util/TimeUnitSpec.cfc | 2 +- 12 files changed, 292 insertions(+), 228 deletions(-) create mode 100644 system/async/time/ChronoUnit.cfc rename system/async/{util => time}/Duration.cfc (100%) rename system/async/{util => time}/Period.cfc (86%) rename system/async/{util => time}/TimeUnit.cfc (100%) delete mode 100644 system/async/util/ChronoUnit.cfc diff --git a/system/async/AsyncManager.cfc b/system/async/AsyncManager.cfc index a08092769..196908b05 100644 --- a/system/async/AsyncManager.cfc +++ b/system/async/AsyncManager.cfc @@ -344,7 +344,7 @@ component accessors="true" singleton { ****************************************************************/ Duration function duration(){ - return new util.Duration(); + return new time.Duration(); } /** diff --git a/system/async/Future.cfc b/system/async/Future.cfc index 65b76baef..07b0df387 100644 --- a/system/async/Future.cfc +++ b/system/async/Future.cfc @@ -39,7 +39,7 @@ component accessors="true" { property name="futureTimeout" type="struct"; // Prepare the static time unit class - this.$timeUnit = new util.TimeUnit(); + this.$timeUnit = new time.TimeUnit(); /** * Construct a new ColdBox Future backed by a Java Completable Future @@ -62,15 +62,12 @@ component accessors="true" { variables.executor = ( isNull( arguments.executor ) ? "" : arguments.executor ); // Are we using a Java or CFML executor? - if( isObject( variables.executor ) && structKeyExists( variables.executor, "getNative" ) ){ + if ( isObject( variables.executor ) && structKeyExists( variables.executor, "getNative" ) ) { variables.executor = variables.executor.getNative(); } // Prepare initial timeouts - variables.futureTimeout = { - "timeout" : 0, - "timeUnit" : "milliseconds" - }; + variables.futureTimeout = { "timeout" : 0, "timeUnit" : "milliseconds" }; // Default the future to be empty variables.isEmptyFuture = true; @@ -118,10 +115,14 @@ component accessors="true" { * @timeout how long to wait before completing normally with the given value, in units of unit * @timeUnit The time unit to use, available units are: days, hours, microseconds, milliseconds, minutes, nanoseconds, and seconds. The default is milliseconds */ - Future function completeOnTimeout( required value, required timeout, timeUnit = "milliseconds" ){ + Future function completeOnTimeout( + required value, + required timeout, + timeUnit = "milliseconds" + ){ variables.native = variables.native.completeOnTimeout( arguments.value, - javaCast( "long", arguments.timeout ), + javacast( "long", arguments.timeout ), this.$timeUnit.get( arguments.timeUnit ) ); return this; @@ -135,7 +136,7 @@ component accessors="true" { */ Future function orTimeout( required timeout, timeUnit = "milliseconds" ){ variables.native = variables.native.orTimeout( - javaCast( "long", arguments.timeout ), + javacast( "long", arguments.timeout ), this.$timeUnit.get( arguments.timeUnit ) ); return this; @@ -174,7 +175,7 @@ component accessors="true" { * @defaultValue */ Future function completeWithException(){ - return completeExceptionally( argumentCollection=arguments ); + return completeExceptionally( argumentCollection = arguments ); } /** @@ -192,11 +193,11 @@ component accessors="true" { any function join( defaultValue ){ var results = variables.native.join(); - if( !isNull( results ) ){ + if ( !isNull( results ) ) { return results; } - if( isNull( results ) && !isNull( arguments.defaultValue ) ){ + if ( isNull( results ) && !isNull( arguments.defaultValue ) ) { return arguments.defaultValue; } } @@ -219,12 +220,12 @@ component accessors="true" { ){ // Do we have a timeout? if ( arguments.timeout != 0 ) { - try{ + try { var results = variables.native.get( javacast( "long", arguments.timeout ), this.$timeUnit.get( arguments.timeUnit ) ); - } catch( "java.util.concurrent.TimeoutException" e ){ + } catch ( "java.util.concurrent.TimeoutException" e ) { // If we have a result, return it, else rethrow if ( !isNull( arguments.defaultValue ) ) { return arguments.defaultValue; @@ -456,7 +457,7 @@ component accessors="true" { [ "java.util.function.BiFunction" ] ); - if( !isNull( arguments.executor ) ){ + if ( !isNull( arguments.executor ) ) { variables.native = variables.native.handleAsync( biFunction, arguments.executor ); } else { variables.native = variables.native.handleAsync( biFunction ); @@ -530,7 +531,7 @@ component accessors="true" { [ "java.util.function.BiConsumer" ] ); - if( !isNull( arguments.executor ) ){ + if ( !isNull( arguments.executor ) ) { variables.native = variables.native.whenCompleteAsync( biConsumer, arguments.executor ); } else { variables.native = variables.native.whenCompleteAsync( biConsumer ); @@ -567,7 +568,7 @@ component accessors="true" { [ "java.util.function.Function" ] ); - if( variables.isEmptyFuture ){ + if ( variables.isEmptyFuture ) { variables.native.thenApply( apply ); } else { variables.native = variables.native.thenApply( apply ); @@ -666,7 +667,7 @@ component accessors="true" { * Alias to thenRun() */ Future function thenAccept(){ - return thenRun( argumentCollection=arguments ); + return thenRun( argumentCollection = arguments ); } /** @@ -699,7 +700,7 @@ component accessors="true" { [ "java.util.function.Consumer" ] ); - if( !isNull( arguments.executor ) ){ + if ( !isNull( arguments.executor ) ) { variables.native.thenAcceptAsync( fConsumer, arguments.executor ); } else { variables.native.thenAcceptAsync( fConsumer ); @@ -712,7 +713,7 @@ component accessors="true" { * Alias to thenRunAsync() */ Future function thenAcceptAsync(){ - return thenRunAsync( argumentCollection=arguments ); + return thenRunAsync( argumentCollection = arguments ); } /** @@ -787,11 +788,10 @@ component accessors="true" { return this.thenAsync( function(){ // return back the completed array results return jFutures.map( function( jFuture ){ - //writeDump( var=arguments.jFuture.get(), output="console" ); + // writeDump( var=arguments.jFuture.get(), output="console" ); return arguments.jFuture.get(); } ); } ); - } /** @@ -816,12 +816,12 @@ component accessors="true" { * @return An array or struct with the items processed in parallel */ any function allApply( items, fn, executor, timeout, timeUnit ){ - var incomingExecutor = arguments.executor ?: ""; + var incomingExecutor = arguments.executor ?: ""; // Boolean indicator to avoid `isObject()` calls on iterations - var usingExecutor = isObject( incomingExecutor ); + var usingExecutor = isObject( incomingExecutor ); // Cast it here, so it only happens once - var currentTimeout = javacast( "long", arguments.timeout ?: variables.futureTimeout.timeout ); - var currentTimeunit = arguments.timeUnit ?: variables.futureTimeout.timeUnit; + var currentTimeout = javacast( "long", arguments.timeout ?: variables.futureTimeout.timeout ); + var currentTimeunit = arguments.timeUnit ?: variables.futureTimeout.timeUnit; // Create the function proxy once instead of many times during iterations var jApply = createDynamicProxy( @@ -834,7 +834,7 @@ component accessors="true" { ); // Array Processing - if( isArray( arguments.items ) ){ + if ( isArray( arguments.items ) ) { // Return the array as a collection of processed values return arguments.items // Startup the tasks @@ -843,63 +843,45 @@ component accessors="true" { var f = new Future( arguments.thisItem ); // Execute it on a custom executor or core - if( usingExecutor ){ - return f.setNative( - f.getNative().thenApplyAsync( jApply, incomingExecutor ) - ); + if ( usingExecutor ) { + return f.setNative( f.getNative().thenApplyAsync( jApply, incomingExecutor ) ); } // Core Executor - return f.setNative( - f.getNative().thenApplyAsync( jApply ) - ); + return f.setNative( f.getNative().thenApplyAsync( jApply ) ); } ) // Collect the tasks .map( function( thisFuture ){ - return arguments.thisFuture.get( - currentTimeout, - currentTimeunit - ); + return arguments.thisFuture.get( currentTimeout, currentTimeunit ); } ); } // Struct Processing - else if( isStruct( arguments.items ) ){ + else if ( isStruct( arguments.items ) ) { return arguments.items // Startup the tasks .map( function( key, value ){ // Create a new completed future - var f = new Future( { - "key" : arguments.key, - "value" : arguments.value - } ); + var f = new Future( { "key" : arguments.key, "value" : arguments.value } ); // Execute it on a custom executor or core - if( usingExecutor ){ - return f.setNative( - f.getNative().thenApplyAsync( jApply, incomingExecutor ) - ); + if ( usingExecutor ) { + return f.setNative( f.getNative().thenApplyAsync( jApply, incomingExecutor ) ); } // Core Executor - return f.setNative( - f.getNative().thenApplyAsync( jApply ) - ); + return f.setNative( f.getNative().thenApplyAsync( jApply ) ); } ) // Collect the tasks .map( function( key, thisFuture ){ - return arguments.thisFuture.get( - currentTimeout, - currentTimeunit - ); + return arguments.thisFuture.get( currentTimeout, currentTimeunit ); } ); - } else{ + } else { throw( - message : "The collection type passed is not yet supported!", - type : "UnsupportedCollectionException", + message: "The collection type passed is not yet supported!", + type : "UnsupportedCollectionException", detail : getMetadata( arguments.items ) ); } - } /** @@ -948,27 +930,24 @@ component accessors="true" { var target = arguments; // Is the first element an array? Then use that as the builder for workloads - if( !isNull( arguments[ 1 ] ) && isArray( arguments[ 1 ] ) ){ + if ( !isNull( arguments[ 1 ] ) && isArray( arguments[ 1 ] ) ) { target = arguments[ 1 ]; } // I have to use arrayMap() if not lucee does not use array notation // but struct notation and order get's lost - return arrayMap( - target, - function( future ){ - // Is this a closure/lambda/udf? Then inflate to a future - if ( isClosure( arguments.future ) || isCustomFunction( arguments.future ) ) { - return new Future().run( arguments.future ); - } - // Return it - return arguments.future; + return arrayMap( target, function( future ){ + // Is this a closure/lambda/udf? Then inflate to a future + if ( isClosure( arguments.future ) || isCustomFunction( arguments.future ) ) { + return new Future().run( arguments.future ); } - ).reduce( function( results, future ){ + // Return it + return arguments.future; + } ).reduce( function( results, future ){ // Now process it results.append( arguments.future.getNative() ); return results; }, [] ); } -} \ No newline at end of file +} diff --git a/system/async/tasks/Executor.cfc b/system/async/tasks/Executor.cfc index f53422931..d6b64a60e 100644 --- a/system/async/tasks/Executor.cfc +++ b/system/async/tasks/Executor.cfc @@ -21,7 +21,7 @@ component accessors="true" singleton { property name="native"; // Prepare the static time unit class - this.$timeUnit = new coldbox.system.async.util.TimeUnit(); + this.$timeUnit = new coldbox.system.async.time.TimeUnit(); /** * Constructor diff --git a/system/async/tasks/FutureTask.cfc b/system/async/tasks/FutureTask.cfc index 58d738c41..cef5eb9c3 100644 --- a/system/async/tasks/FutureTask.cfc +++ b/system/async/tasks/FutureTask.cfc @@ -13,7 +13,7 @@ component accessors="true" { property name="native"; // Prepare the static time unit class - this.$timeUnit = new coldbox.system.async.util.TimeUnit(); + this.$timeUnit = new coldbox.system.async.time.TimeUnit(); /** * Build the ColdBox Future with the Java native class diff --git a/system/async/time/ChronoUnit.cfc b/system/async/time/ChronoUnit.cfc new file mode 100644 index 000000000..58a912c26 --- /dev/null +++ b/system/async/time/ChronoUnit.cfc @@ -0,0 +1,90 @@ +component singleton { + + variables.jChronoUnit = createObject( "java", "java.time.temporal.ChronoUnit" ); + variables.ZoneOffset = createObject( "java", "java.time.ZoneOffset" ); + variables.ZoneId = createObject( "java", "java.time.ZoneId" ); + + // Unit that represents the concept of a century. + this.CENTURIES = variables.jChronoUnit.CENTURIES; + // Unit that represents the concept of a day. + this.DAYS = variables.jChronoUnit.DAYS; + // Unit that represents the concept of a decade. + this.DECADES = variables.jChronoUnit.DECADES; + // Unit that represents the concept of an era. + this.ERAS = variables.jChronoUnit.ERAS; + // Artificial unit that represents the concept of forever. + this.FOREVER = variables.jChronoUnit.FOREVER; + // Unit that represents the concept of half a day, as used in AM/PM. + this.HALF_DAYS = variables.jChronoUnit.HALF_DAYS; + // Unit that represents the concept of an hour. + this.HOURS = variables.jChronoUnit.HOURS; + // Unit that represents the concept of a microsecond. + this.MICROS = variables.jChronoUnit.MICROS; + // Unit that represents the concept of a millennium. + this.MILLENNIA = variables.jChronoUnit.MILLENNIA; + // Unit that represents the concept of a millisecond. + this.MILLIS = variables.jChronoUnit.MILLIS; + // Unit that represents the concept of a minute. + this.MINUTES = variables.jChronoUnit.MINUTES; + // Unit that represents the concept of a month. + this.MONTHS = variables.jChronoUnit.MONTHS; + // Unit that represents the concept of a nanosecond, the smallest supported unit of time. + this.NANOS = variables.jChronoUnit.NANOS; + // Unit that represents the concept of a second. + this.SECONDS = variables.jChronoUnit.SECONDS; + // Unit that represents the concept of a week. + this.WEEKS = variables.jChronoUnit.WEEKS; + // Unit that represents the concept of a year. + this.YEARS = variables.jChronoUnit.YEARS; + + /** + * Convert any ColdFusion date/time or string date/time object to a Java instant temporal object + * + * @target The date/time or string object representing the date/time + * + * @return A Java temporal object as java.time.Instant + */ + function toInstant( required target ){ + // Is this a date/time object or a string? + if ( findNoCase( "string", arguments.target.getClass().getName() ) ) { + arguments.target = createODBCDateTime( arguments.target ); + } + return arguments.target.toInstant(); + } + + /** + * Convert any ColdFUsion date/time or string date/time object to the new Java.time.LocalDate class so we can use them as Temporal objects + * + * @target The cf date/time or string object representing the date/time + * @timezone If passed, we will use this timezone to build the temporal object. Else we default to UTC + * + * @throws DateTimeException - if the zone ID has an invalid format + * @throws ZoneRulesException - if the zone ID is a region ID that cannot be found + * + * @return A Java temporal object as java.time.LocalDate + */ + function toLocalDate( required target, timezone ){ + return this + .toInstant( arguments.target ) + .atZone( + isNull( arguments.timezone ) ? variables.ZoneOffset.UTC : variables.ZoneId.of( arguments.timezone ) + ) + .toLocalDate(); + } + + /** + * Convert any date/time or string date/time object to a Java Date/Time + * + * @target The date/time or string object representing the date/time + * + * @return A java date time object + */ + function toJavaDate( required target ){ + // Is this a date/time object or a string? + if ( findNoCase( "string", arguments.target.getClass().getName() ) ) { + arguments.target = createODBCDateTime( arguments.target ); + } + return arguments.target; + } + +} diff --git a/system/async/util/Duration.cfc b/system/async/time/Duration.cfc similarity index 100% rename from system/async/util/Duration.cfc rename to system/async/time/Duration.cfc diff --git a/system/async/util/Period.cfc b/system/async/time/Period.cfc similarity index 86% rename from system/async/util/Period.cfc rename to system/async/time/Period.cfc index 6bd6fd4ba..290d3888a 100644 --- a/system/async/util/Period.cfc +++ b/system/async/time/Period.cfc @@ -115,7 +115,7 @@ component accessors="true" { * Gets the number of Days in this period. */ numeric function getDays(){ - return this.get(); + return this.get( "days" ); } /** @@ -165,26 +165,26 @@ component accessors="true" { * Adds this period to the specified temporal object and return back to you a date/time object * * @target The date/time object or string to incorporate the period into - * @asInstant Return the result either as a date/time string or a java.time.Instant object + * @asLocalDate If true, we will give you the java.time.LocalDate object, else a ColdFusion date/time string * - * @return Return the result either as a date/time string or a java.time.Instant object + * @return The date/time object with the period added to it or a java LocalDate */ - function addTo( required target, boolean asInstant = false ){ - var results = variables.jPeriod.addTo( this.CHRONO_UNIT.toInstant( arguments.target ) ); - return ( arguments.asInstant ? results : results.toString() ); + function addTo( required target, boolean asLocalDate = false ){ + var results = variables.jPeriod.addTo( this.CHRONO_UNIT.toLocalDate( arguments.target ) ); + return ( arguments.asLocalDate ? results : results.toString() ); } /** - * Subtracts this period to the specified temporal object and return back to you a date/time object + * Subtracts this period to the specified temporal object and return back to you a date/time object or a Java LocalDate object * - * @target The date/time object or string to subtract the period from - * @asInstant Return the result either as a date/time string or a java.time.Instant object + * @target The date/time object or string to incorporate the period into + * @asLocalDate If true, we will give you the java.time.LocalDate object, else a ColdFusion date/time string * - * @return Return the result either as a date/time string or a java.time.Instant object + * @return Return the result either as a date/time string or a java.time.LocalDate object */ - function subtractFrom( required target, boolean asInstant = false ){ - var results = variables.jPeriod.subtractFrom( this.CHRONO_UNIT.toInstant( arguments.target ) ); - return ( arguments.asInstant ? results : results.toString() ); + function subtractFrom( required target, boolean asLocalDate = false ){ + var results = variables.jPeriod.subtractFrom( this.CHRONO_UNIT.toLocalDate( arguments.target ) ); + return ( arguments.asLocalDate ? results : results.toString() ); } /** @@ -200,8 +200,8 @@ component accessors="true" { Period function between( required start, required end ){ // Do it! variables.jPeriod = variables.jPeriod.between( - this.CHRONO_UNIT.toLocalDate( arguments.start ), - this.CHRONO_UNIT.toLocalDate( arguments.end ) + this.CHRONO_UNIT.toJavaDate( arguments.start ), + this.CHRONO_UNIT.toJavaDate( arguments.end ) ); return this; } @@ -341,9 +341,9 @@ component accessors="true" { */ Period function of( years = 0, months = 0, days = 0 ){ variables.jPeriod = variables.jPeriod.of( - javacast( "long", arguments.years ), - javacast( "long", arguments.months ), - javacast( "long", arguments.days ) + javacast( "int", arguments.years ), + javacast( "int", arguments.months ), + javacast( "int", arguments.days ) ); return this; } @@ -354,7 +354,7 @@ component accessors="true" { * @days The number of days, positive or negative */ Period function ofDays( required days ){ - variables.jPeriod = variables.jPeriod.ofDays( javacast( "long", arguments.days ) ); + variables.jPeriod = variables.jPeriod.ofDays( javacast( "int", arguments.days ) ); return this; } @@ -364,7 +364,17 @@ component accessors="true" { * @months The number of months, positive or negative */ Period function ofMonths( required months ){ - variables.jPeriod = variables.jPeriod.ofMonths( javacast( "long", arguments.days ) ); + variables.jPeriod = variables.jPeriod.ofMonths( javacast( "int", arguments.months ) ); + return this; + } + + /** + * Obtains a Period representing a number of weeks. + * + * @weeks The number of weeks, positive or negative + */ + Period function ofWeeks( required weeks ){ + variables.jPeriod = variables.jPeriod.ofWeeks( javacast( "int", arguments.weeks ) ); return this; } @@ -374,7 +384,7 @@ component accessors="true" { * @years The number of years, positive or negative */ Period function ofYears( required years ){ - variables.jPeriod = variables.jPeriod.ofYears( javacast( "long", arguments.days ) ); + variables.jPeriod = variables.jPeriod.ofYears( javacast( "int", arguments.years ) ); return this; } diff --git a/system/async/util/TimeUnit.cfc b/system/async/time/TimeUnit.cfc similarity index 100% rename from system/async/util/TimeUnit.cfc rename to system/async/time/TimeUnit.cfc diff --git a/system/async/util/ChronoUnit.cfc b/system/async/util/ChronoUnit.cfc deleted file mode 100644 index 1c0370556..000000000 --- a/system/async/util/ChronoUnit.cfc +++ /dev/null @@ -1,68 +0,0 @@ -component singleton { - - // A standard set of date periods units. - variables.jChronoUnit = createObject( "java", "java.time.temporal.ChronoUnit" ); - // Unit that represents the concept of a century. - this.CENTURIES = variables.jChronoUnit.CENTURIES; - // Unit that represents the concept of a day. - this.DAYS = variables.jChronoUnit.DAYS; - // Unit that represents the concept of a decade. - this.DECADES = variables.jChronoUnit.DECADES; - // Unit that represents the concept of an era. - this.ERAS = variables.jChronoUnit.ERAS; - // Artificial unit that represents the concept of forever. - this.FOREVER = variables.jChronoUnit.FOREVER; - // Unit that represents the concept of half a day, as used in AM/PM. - this.HALF_DAYS = variables.jChronoUnit.HALF_DAYS; - // Unit that represents the concept of an hour. - this.HOURS = variables.jChronoUnit.HOURS; - // Unit that represents the concept of a microsecond. - this.MICROS = variables.jChronoUnit.MICROS; - // Unit that represents the concept of a millennium. - this.MILLENNIA = variables.jChronoUnit.MILLENNIA; - // Unit that represents the concept of a millisecond. - this.MILLIS = variables.jChronoUnit.MILLIS; - // Unit that represents the concept of a minute. - this.MINUTES = variables.jChronoUnit.MINUTES; - // Unit that represents the concept of a month. - this.MONTHS = variables.jChronoUnit.MONTHS; - // Unit that represents the concept of a nanosecond, the smallest supported unit of time. - this.NANOS = variables.jChronoUnit.NANOS; - // Unit that represents the concept of a second. - this.SECONDS = variables.jChronoUnit.SECONDS; - // Unit that represents the concept of a week. - this.WEEKS = variables.jChronoUnit.WEEKS; - // Unit that represents the concept of a year. - this.YEARS = variables.jChronoUnit.YEARS; - - /** - * Convert any date/time or string date/time object to a Java instant temporal object - * - * @target The date/time or string object representing the date/time - * - * @return A Java temporal object as java.time.Instant - */ - function toInstant( required target ){ - // Is this a date/time object or a string? - if ( findNoCase( "string", arguments.target.getClass().getName() ) ) { - arguments.target = createODBCDateTime( arguments.target ); - } - return arguments.target.toInstant(); - } - - /** - * Convert any date/time or string date/time object to a Java LocalDate - * - * @target The date/time or string object representing the date/time - * - * @return A java LocalDate object - */ - function toLocalDate( required target ){ - // Is this a date/time object or a string? - if ( findNoCase( "string", arguments.target.getClass().getName() ) ) { - arguments.target = createODBCDateTime( arguments.target ); - } - return arguments.target; - } - -} diff --git a/tests/specs/async/util/DurationSpec.cfc b/tests/specs/async/util/DurationSpec.cfc index acadef4ce..e5c87a791 100644 --- a/tests/specs/async/util/DurationSpec.cfc +++ b/tests/specs/async/util/DurationSpec.cfc @@ -9,7 +9,7 @@ component extends="tests.specs.async.BaseAsyncSpec" { // all your suites go here. describe( "Duration", function(){ beforeEach( function( currentSpec ){ - duration = new coldbox.system.async.util.Duration(); + duration = new coldbox.system.async.time.Duration(); } ); it( "can be created", function(){ diff --git a/tests/specs/async/util/PeriodSpec.cfc b/tests/specs/async/util/PeriodSpec.cfc index acadef4ce..d1e1e2537 100644 --- a/tests/specs/async/util/PeriodSpec.cfc +++ b/tests/specs/async/util/PeriodSpec.cfc @@ -1,5 +1,5 @@ /** - * Duration Specs + * period Specs */ component extends="tests.specs.async.BaseAsyncSpec" { @@ -7,99 +7,152 @@ component extends="tests.specs.async.BaseAsyncSpec" { function run( testResults, testBox ){ // all your suites go here. - describe( "Duration", function(){ + describe( "Period", function(){ beforeEach( function( currentSpec ){ - duration = new coldbox.system.async.util.Duration(); + period = new coldbox.system.async.time.Period(); } ); it( "can be created", function(){ - expect( duration ).toBeComponent(); + expect( period ).toBeComponent(); } ); it( "can do creations with of methods", function(){ - expect( duration.ofDays( 7 ).toString() ).toBe( "PT168H" ); - expect( duration.ofHours( 8 ).toString() ).toBe( "PT8H" ); - expect( duration.ofMinutes( 15 ).toString() ).toBe( "PT15M" ); - expect( duration.ofSeconds( 10 ).toString() ).toBe( "PT10S" ); - expect( duration.ofSeconds( 30, 123456789 ).toString() ).toBe( "PT30.123456789S" ); - expect( duration.between( "2017-10-03T10:15:30.00Z", "2017-10-03T10:16:30.00Z" ).getSeconds() ).toBe( - 60 - ); - expect( duration.between( "2021-01-01 00:00:00", "2021-01-01 00:00:01" ).toString() ).toBe( - "PT1S" - ); - expect( - duration - .between( createDateTime( 2019, 1, 1, 0, 0, 0 ), createDateTime( 2021, 1, 1, 0, 0, 0 ) ) - .toString() - ).toBe( "PT17544H" ); + var p = period.of(); + expect( p.getyears() ).toBe( 0 ); + expect( p.getMonths() ).toBe( 0 ); + expect( p.getDays() ).toBe( 0 ); + + var p = period.of( 1, 5, 2 ); + expect( p.getyears() ).toBe( 1 ); + expect( p.getMonths() ).toBe( 5 ); + expect( p.getDays() ).toBe( 2 ); } ); - it( "can parse strings into durations", function(){ - var d = duration.parse( "P1DT8H15M10.345000S" ); - expect( d.getSeconds() ).toBe( 116110 ); - expect( d.getNano() ).toBe( 345000000 ); - expect( d.getUnits() ).toInclude( "seconds" ).toInclude( "nanos" ); + it( "can build days", function(){ + expect( period.ofDays( 10 ).getDays() ).toBe( 10 ); } ); - it( "can check utility methods", function(){ - expect( duration.iszero() ).toBeTrue(); - expect( duration.get() ).toBe( 0 ); - expect( duration.getSeconds() ).toBe( 0 ); - expect( duration.getNano() ).toBe( 0 ); - expect( duration.isNegative() ).toBeFalse(); + it( "can build months", function(){ + expect( period.ofMonths( 10 ).getMonths() ).toBe( 10 ); } ); - it( "can do minus", function(){ - var d = duration.of( 10 ); - expect( duration.minus( 1 ).get() ).toBe( 9 ); + it( "can build weeks", function(){ + var p = period.ofWeeks( 2 ); + expect( p.getDays() ).toBe( 14 ); } ); - it( "can do plus", function(){ - var d = duration.of( 10 ); - expect( duration.plus( 1 ).get() ).toBe( 11 ); + it( "can build years", function(){ + expect( period.ofyears( 10 ).getyears() ).toBe( 10 ); } ); - it( "can do multiplication", function(){ - var d = duration.of( 10 ); - expect( duration.multipliedBy( 10 ).get() ).toBe( 100 ); + it( "can parse strings into periods", function(){ + var p = period.parse( "P2Y" ); + expect( p.getYears() ).toBe( 2 ); + + var p = period.parse( "P2Y5M" ); + expect( p.getYears() ).toBe( 2 ); + expect( p.getMonths() ).toBe( 5 ); + + var p = period.parse( "P2Y5M10D" ); + expect( p.getYears() ).toBe( 2 ); + expect( p.getMonths() ).toBe( 5 ); + expect( p.getDays() ).toBe( 10 ); } ); - it( "can do division", function(){ - var d = duration.of( 10 ); - expect( duration.dividedBy( 10 ).get() ).toBe( 1 ); + it( "can check utility methods", function(){ + expect( period.iszero() ).toBeTrue(); + expect( period.get() ).toBe( 0 ); + expect( period.isNegative() ).toBeFalse(); } ); - it( "can do negations", function(){ - var d = duration.of( -1 ); - expect( d.negated().isNegative() ).toBeFalse(); + it( "can get total months", function(){ + var p = period.parse( "P10Y5M20D" ) + expect( p.toTotalMonths() ).toBe( 125 ); + } ); - var d = duration.of( -1 ); - expect( d.isNegative() ).toBeTrue(); - expect( d.abs().isNegative() ).toBeFalse(); + it( "can do minus", function(){ + expect( + period + .of( 5 ) + .minus( period.ofDays( 5 ) ) + .getDays() + ).toBe( 0 ); + + expect( + period + .init() + .ofDays( 5 ) + .minusDays( 1 ) + .getDays() + ).toBe( 4 ); + + expect( + period + .init() + .ofMonths( 5 ) + .minusMonths( 1 ) + .getMonths() + ).toBe( 4 ); + + expect( + period + .init() + .ofYears( 5 ) + .minusYears( 1 ) + .getYears() + ).toBe( 4 ); } ); - it( "can create it from another duration", function(){ - var test = duration.ofSeconds( 5 ); - expect( duration.from( test ).get() ).toBe( 5 ); + it( "can do plus", function(){ + expect( + period + .of( 5 ) + .plus( period.ofDays( 5 ) ) + .getDays() + ).toBe( 10 ); + + expect( + period + .ofDays( 5 ) + .plusDays( 1 ) + .getDays() + ).toBe( 6 ); + + expect( + period + .init() + .ofMonths( 5 ) + .plusMonths( 1 ) + .getMonths() + ).toBe( 6 ); + + expect( + period + .init() + .ofYears( 5 ) + .plusYears( 1 ) + .getYears() + ).toBe( 6 ); } ); - it( "can convert to other time units", function(){ - expect( duration.ofMinutes( 60 ).toHours() ).toBe( 1 ); - expect( duration.ofMinutes( 60 ).toSeconds() ).toBe( 60 * 60 ); - expect( duration.ofMinutes( 60 ).toMillis() ).toBe( 60 * 60 * 1000 ); + it( "can do multiplication", function(){ + var d = period.ofDays( 10 ); + expect( period.multipliedBy( 10 ).getDays() ).toBe( 100 ); } ); - it( "can add durations to date/time objects", function(){ - var now = now(); - var t = duration.ofDays( 10 ).addTo( now ); - expect( dateDiff( "d", now, t ) ).toBe( 10 ); + it( "can do negations", function(){ + var d = period.ofDays( -1 ); + expect( d.negated().isNegative() ).toBeFalse(); + + var d = period.ofDays( -1 ); + expect( d.isNegative() ).toBeTrue(); } ); - it( "can subtract durations to date/time objects", function(){ - var now = now(); - var t = duration.ofDays( 10 ).subtractFrom( now ); - expect( dateDiff( "d", now, t ) ).toBe( -10 ); + it( "can addTo", function(){ + var p = period.of( 2, 5, 10 ); + var target = "2021-01-01"; + + expect( p.addTo( target ) ).tobe( "2023-06-11" ) } ); } ); } diff --git a/tests/specs/async/util/TimeUnitSpec.cfc b/tests/specs/async/util/TimeUnitSpec.cfc index 8ad95e70e..d9a1950f2 100644 --- a/tests/specs/async/util/TimeUnitSpec.cfc +++ b/tests/specs/async/util/TimeUnitSpec.cfc @@ -9,7 +9,7 @@ component extends="tests.specs.async.BaseAsyncSpec" { // all your suites go here. describe( "TimeUnit", function(){ beforeEach( function( currentSpec ){ - timeUnit = new coldbox.system.async.util.TimeUnit(); + timeUnit = new coldbox.system.async.time.TimeUnit(); } ); it( "can be created", function(){ From 1a780d3b42da793b8d70403c487d6d60da08779d Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Sat, 10 Apr 2021 12:56:48 -0500 Subject: [PATCH 06/40] much more cleanup and full pathing for easier refactoring in the future for the async package --- system/async/AsyncManager.cfc | 37 +- .../async/{tasks => executors}/Executor.cfc | 2 +- .../ExecutorBuilder.cfc} | 0 .../ScheduledExecutor.cfc | 26 +- system/async/{ => tasks}/Future.cfc | 28 +- system/cache/CacheFactory.cfc | 271 ++++---- system/ioc/Injector.cfc | 579 ++++++++++-------- system/logging/LogBox.cfc | 191 +++--- tests/specs/async/util/ExecutorsSpec.cfc | 2 +- 9 files changed, 611 insertions(+), 525 deletions(-) rename system/async/{tasks => executors}/Executor.cfc (98%) rename system/async/{util/Executors.cfc => executors/ExecutorBuilder.cfc} (100%) rename system/async/{tasks => executors}/ScheduledExecutor.cfc (91%) rename system/async/{ => tasks}/Future.cfc (97%) diff --git a/system/async/AsyncManager.cfc b/system/async/AsyncManager.cfc index 196908b05..911122951 100644 --- a/system/async/AsyncManager.cfc +++ b/system/async/AsyncManager.cfc @@ -24,10 +24,7 @@ component accessors="true" singleton { property name="executors" type="struct"; // Static class to Executors: java.util.concurrent.Executors - this.$executors = new util.Executors(); - - // Helpers - variables.IntStream = createObject( "java", "java.util.stream.IntStream" ); + this.$executors = new coldbox.system.async.executors.ExecutorBuilder(); /** * Constructor @@ -66,7 +63,7 @@ component accessors="true" singleton { * @debug Add output debugging * @loadAppContext Load the CFML App contexts or not, disable if not used * - * @return The ColdBox Schedule class to work with the schedule: coldbox.system.async.tasks.Executor + * @return The ColdBox Schedule class to work with the schedule: coldbox.system.async.executors.Executor */ Executor function newExecutor( required name, @@ -106,19 +103,19 @@ component accessors="true" singleton { switch ( arguments.type ) { case "fixed": { arguments.executor = this.$executors.newFixedThreadPool( arguments.threads ); - return new tasks.Executor( argumentCollection = arguments ); + return new executors.Executor( argumentCollection = arguments ); } case "cached": { arguments.executor = this.$executors.newCachedThreadPool(); - return new tasks.Executor( argumentCollection = arguments ); + return new executors.Executor( argumentCollection = arguments ); } case "single": { arguments.executor = this.$executors.newFixedThreadPool( 1 ); - return new tasks.Executor( argumentCollection = arguments ); + return new executors.Executor( argumentCollection = arguments ); } case "scheduled": { arguments.executor = this.$executors.newScheduledThreadPool( arguments.threads ); - return new tasks.ScheduledExecutor( argumentCollection = arguments ); + return new executors.ScheduledExecutor( argumentCollection = arguments ); } default: { } @@ -174,7 +171,7 @@ component accessors="true" singleton { * @name The executor name * * @throws ExecutorNotFoundException - * @return The executor object: coldbox.system.async.tasks.Executor + * @return The executor object: coldbox.system.async.executors.Executor */ Executor function getExecutor( required name ){ if ( hasExecutor( arguments.name ) ) { @@ -294,7 +291,7 @@ component accessors="true" singleton { boolean debug = false, boolean loadAppContext = true ){ - return new Future( argumentCollection = arguments ); + return new tasks.Future( argumentCollection = arguments ); } /** @@ -311,7 +308,7 @@ component accessors="true" singleton { boolean debug = false, boolean loadAppContext = true ){ - return new Future( argumentCollection = arguments ); + return new tasks.Future( argumentCollection = arguments ); } /**************************************************************** @@ -343,8 +340,18 @@ component accessors="true" singleton { * Utilities * ****************************************************************/ + /** + * Build out a new Duration class + */ Duration function duration(){ - return new time.Duration(); + return new time.Duration( argumentCollection = arguments ); + } + + /** + * Build out a new Period class + */ + Period function period(){ + return new time.Period( argumentCollection = arguments ); } /** @@ -373,7 +380,9 @@ component accessors="true" singleton { } // build it up - return IntStream.rangeClosed( arguments.from, arguments.to ).toArray(); + return createObject( "java", "java.util.stream.IntStream" ) + .rangeClosed( arguments.from, arguments.to ) + .toArray(); } } diff --git a/system/async/tasks/Executor.cfc b/system/async/executors/Executor.cfc similarity index 98% rename from system/async/tasks/Executor.cfc rename to system/async/executors/Executor.cfc index d6b64a60e..42d302837 100644 --- a/system/async/tasks/Executor.cfc +++ b/system/async/executors/Executor.cfc @@ -71,7 +71,7 @@ component accessors="true" singleton { ); // Send for execution - return new FutureTask( variables.native.submit( jCallable ) ); + return new coldbox.system.async.tasks.FutureTask( variables.native.submit( jCallable ) ); } /**************************************************************** diff --git a/system/async/util/Executors.cfc b/system/async/executors/ExecutorBuilder.cfc similarity index 100% rename from system/async/util/Executors.cfc rename to system/async/executors/ExecutorBuilder.cfc diff --git a/system/async/tasks/ScheduledExecutor.cfc b/system/async/executors/ScheduledExecutor.cfc similarity index 91% rename from system/async/tasks/ScheduledExecutor.cfc rename to system/async/executors/ScheduledExecutor.cfc index 77af2e90f..00d43ab7e 100644 --- a/system/async/tasks/ScheduledExecutor.cfc +++ b/system/async/executors/ScheduledExecutor.cfc @@ -12,11 +12,7 @@ * use to monitor and get results from the tasks at hand, if any. * */ -component - extends ="Executor" - accessors="true" - singleton -{ +component extends="Executor" accessors="true" singleton { /**************************************************************** * Scheduling Methods * @@ -49,9 +45,9 @@ component // build out the java callable var jCallable = createDynamicProxy( new coldbox.system.async.proxies.Callable( - supplier = arguments.task, - method = arguments.method, - debug = variables.debug, + supplier = arguments.task, + method = arguments.method, + debug = variables.debug, loadAppContext = variables.loadAppContext ), [ "java.util.concurrent.Callable" ] @@ -65,7 +61,7 @@ component ); // Return the results - return new ScheduledFuture( jScheduledFuture ); + return new coldbox.system.async.tasks.ScheduledFuture( jScheduledFuture ); } /** @@ -106,7 +102,7 @@ component ); // Return the results - return new ScheduledFuture( jScheduledFuture ); + return new coldbox.system.async.tasks.ScheduledFuture( jScheduledFuture ); } /** @@ -145,7 +141,7 @@ component ); // Return the results - return new ScheduledFuture( jScheduledFuture ); + return new coldbox.system.async.tasks.ScheduledFuture( jScheduledFuture ); } /**************************************************************** @@ -154,7 +150,7 @@ component ScheduledTask function newSchedule( required task, method = "run" ){ arguments.executor = this; - return new ScheduledTask( argumentCollection = arguments ); + return new coldbox.system.async.tasks.ScheduledTask( argumentCollection = arguments ); } /**************************************************************** @@ -172,9 +168,9 @@ component function buildJavaRunnable( required task, required method ){ return createDynamicProxy( new coldbox.system.async.proxies.Runnable( - target = arguments.task, - method = arguments.method, - debug = variables.debug, + target = arguments.task, + method = arguments.method, + debug = variables.debug, loadAppContext = variables.loadAppContext ), [ "java.lang.Runnable" ] diff --git a/system/async/Future.cfc b/system/async/tasks/Future.cfc similarity index 97% rename from system/async/Future.cfc rename to system/async/tasks/Future.cfc index 07b0df387..2fbd558d5 100644 --- a/system/async/Future.cfc +++ b/system/async/tasks/Future.cfc @@ -39,7 +39,7 @@ component accessors="true" { property name="futureTimeout" type="struct"; // Prepare the static time unit class - this.$timeUnit = new time.TimeUnit(); + this.$timeUnit = new coldbox.system.async.time.TimeUnit(); /** * Construct a new ColdBox Future backed by a Java Completable Future @@ -307,7 +307,7 @@ component accessors="true" { Future function exceptionally( required target ){ variables.native = variables.native.exceptionally( createDynamicProxy( - new proxies.Function( + new coldbox.system.async.proxies.Function( arguments.target, variables.debug, variables.loadAppContext @@ -343,7 +343,7 @@ component accessors="true" { any executor = variables.executor ){ var jSupplier = createDynamicProxy( - new proxies.Supplier( + new coldbox.system.async.proxies.Supplier( arguments.supplier, arguments.method, variables.debug, @@ -411,7 +411,7 @@ component accessors="true" { */ Future function handle( required action ){ var biFunction = createDynamicProxy( - new proxies.BiFunction( + new coldbox.system.async.proxies.BiFunction( arguments.action, variables.debug, variables.loadAppContext @@ -449,7 +449,7 @@ component accessors="true" { */ Future function handleAsync( required action, executor ){ var biFunction = createDynamicProxy( - new proxies.BiFunction( + new coldbox.system.async.proxies.BiFunction( arguments.action, variables.debug, variables.loadAppContext @@ -486,7 +486,7 @@ component accessors="true" { */ Future function whenComplete( required action ){ var biConsumer = createDynamicProxy( - new proxies.BiConsumer( + new coldbox.system.async.proxies.BiConsumer( arguments.action, variables.debug, variables.loadAppContext @@ -523,7 +523,7 @@ component accessors="true" { */ Future function whenCompleteAsync( required action, executor ){ var biConsumer = createDynamicProxy( - new proxies.BiConsumer( + new coldbox.system.async.proxies.BiConsumer( arguments.action, variables.debug, variables.loadAppContext @@ -560,7 +560,7 @@ component accessors="true" { */ Future function then( required target ){ var apply = createDynamicProxy( - new proxies.Function( + new coldbox.system.async.proxies.Function( arguments.target, variables.debug, variables.loadAppContext @@ -607,7 +607,7 @@ component accessors="true" { */ Future function thenAsync( required target, executor ){ var apply = createDynamicProxy( - new proxies.Function( + new coldbox.system.async.proxies.Function( arguments.target, variables.debug, variables.loadAppContext @@ -650,7 +650,7 @@ component accessors="true" { */ Future function thenRun( required target ){ var fConsumer = createDynamicProxy( - new proxies.Consumer( + new coldbox.system.async.proxies.Consumer( arguments.target, variables.debug, variables.loadAppContext @@ -692,7 +692,7 @@ component accessors="true" { */ Future function thenRunAsync( required target, executor ){ var fConsumer = createDynamicProxy( - new proxies.Consumer( + new coldbox.system.async.proxies.Consumer( arguments.target, variables.debug, variables.loadAppContext @@ -730,7 +730,7 @@ component accessors="true" { Future function thenCompose( required fn ){ variables.native = variables.native.thenCompose( createDynamicProxy( - new proxies.FutureFunction( + new coldbox.system.async.proxies.FutureFunction( arguments.fn, variables.debug, variables.loadAppContext @@ -752,7 +752,7 @@ component accessors="true" { variables.native = variables.native.thenCombine( arguments.future.getNative(), createDynamicProxy( - new proxies.BiFunction( + new coldbox.system.async.proxies.BiFunction( arguments.fn, variables.debug, variables.loadAppContext @@ -825,7 +825,7 @@ component accessors="true" { // Create the function proxy once instead of many times during iterations var jApply = createDynamicProxy( - new proxies.Function( + new coldbox.system.async.proxies.Function( arguments.fn, variables.debug, variables.loadAppContext diff --git a/system/cache/CacheFactory.cfc b/system/cache/CacheFactory.cfc index 622fbd598..c6ecbe0f4 100644 --- a/system/cache/CacheFactory.cfc +++ b/system/cache/CacheFactory.cfc @@ -7,7 +7,7 @@ * The main CacheBox factory and configuration of caches. From this factory * is where you will get all the caches you need to work with or register more caches. **/ - component accessors=true serializable=false{ +component accessors=true serializable=false { /** * The unique factory id @@ -60,7 +60,7 @@ /** * The logBox task scheduler executor - * @see coldbox.system.async.tasks.ScheduledExecutor + * @see coldbox.system.async.executors.ScheduledExecutor */ property name="taskScheduler"; @@ -74,28 +74,23 @@ * @factoryID A unique ID or name for this factory. If not passed I will make one up for you. * @wirebox A configured wirebox instance to get logbox, asyncManager, and EventManager from. If not passed, I will create new ones. */ - function init( - config, - coldbox, - factoryId="", - wirebox - ){ + function init( config, coldbox, factoryId = "", wirebox ){ var defaultConfigPath = "coldbox.system.cache.config.DefaultConfiguration"; // CacheBox Factory UniqueID - variables.factoryId = createObject( "java", "java.lang.System" ).identityHashCode( this ); + variables.factoryId = createObject( "java", "java.lang.System" ).identityHashCode( this ); // Version - variables.version = "@build.version@+@build.number@"; + variables.version = "@build.version@+@build.number@"; // Configuration object - variables.config = ""; + variables.config = ""; // ColdBox Application Link - variables.coldbox = ""; + variables.coldbox = ""; // ColdBox Application Link - variables.wirebox = ""; + variables.wirebox = ""; // Event Manager Link variables.eventManager = ""; // Configured Event States - variables.eventStates = [ + variables.eventStates = [ "afterCacheElementInsert", "afterCacheElementRemoved", "afterCacheElementExpired", @@ -112,13 +107,13 @@ "afterCacheShutdown" ]; // LogBox Links - variables.logBox = ""; - variables.log = ""; + variables.logBox = ""; + variables.log = ""; // Caches - variables.caches = {}; + variables.caches = {}; // Did we send a factoryID in? - if( len( arguments.factoryID ) ){ + if ( len( arguments.factoryID ) ) { variables.factoryID = arguments.factoryID; } @@ -126,51 +121,53 @@ variables.lockName = "CacheFactory.#variables.factoryID#"; // Passed in configuration? - if( isNull( arguments.config ) ){ + if ( isNull( arguments.config ) ) { // Create default configuration - arguments.config = new coldbox.system.cache.config.CacheBoxConfig( CFCConfigPath=defaultConfigPath ); - } else if( isSimpleValue( arguments.config ) ){ - arguments.config = new coldbox.system.cache.config.CacheBoxConfig( CFCConfigPath=arguments.config ); + arguments.config = new coldbox.system.cache.config.CacheBoxConfig( CFCConfigPath = defaultConfigPath ); + } else if ( isSimpleValue( arguments.config ) ) { + arguments.config = new coldbox.system.cache.config.CacheBoxConfig( CFCConfigPath = arguments.config ); } // Check if linking ColdBox - if( !isNull( arguments.coldbox ) ){ + if ( !isNull( arguments.coldbox ) ) { // Link ColdBox - variables.coldbox = arguments.coldbox; + variables.coldbox = arguments.coldbox; // Link to WireBox - variables.wirebox = variables.coldbox.getWireBox(); + variables.wirebox = variables.coldbox.getWireBox(); // link LogBox - variables.logBox = variables.coldbox.getLogBox(); + variables.logBox = variables.coldbox.getLogBox(); // Link Event Manager variables.eventManager = variables.coldbox.getInterceptorService(); // Link Interception States variables.coldbox.getInterceptorService().appendInterceptionPoints( variables.eventStates ); // Link async manager and scheduler - variables.asyncManager = variables.coldbox.getAsyncManager(); + variables.asyncManager = variables.coldbox.getAsyncManager(); variables.taskScheduler = variables.asyncManager.getExecutor( "coldbox-tasks" ); } else { - if( !isNull( arguments.wirebox ) ) { + if ( !isNull( arguments.wirebox ) ) { // Link to WireBox - variables.wirebox = arguments.wirebox; + variables.wirebox = arguments.wirebox; // If WireBox linked, get LogBox and EventManager, and asyncmanager from it - variables.asyncManager = variables.wirebox.getAsyncManager(); + variables.asyncManager = variables.wirebox.getAsyncManager(); variables.taskScheduler = variables.wirebox.getTaskScheduler(); - variables.logBox = variables.wirebox.getLogBox(); + variables.logBox = variables.wirebox.getLogBox(); // link LogBox - variables.eventManager = variables.wirebox.getEventManager(); + variables.eventManager = variables.wirebox.getEventManager(); // register the points to listen to variables.eventManager.appendInterceptionPoints( variables.eventStates ); } else { - - // Register an async manager and scheduler - variables.asyncManager = new coldbox.system.async.AsyncManager(); - variables.taskScheduler = variables.asyncManager.newScheduledExecutor( name : "cachebox-tasks", threads : 20 ); - - // Running standalone, so create our own logging first - configureLogBox( arguments.config.getLogBoxConfig() ); - // Running standalone, so create our own event manager - configureEventManager(); - } + // Register an async manager and scheduler + variables.asyncManager = new coldbox.system.async.AsyncManager(); + variables.taskScheduler = variables.asyncManager.newScheduledExecutor( + name : "cachebox-tasks", + threads: 20 + ); + + // Running standalone, so create our own logging first + configureLogBox( arguments.config.getLogBoxConfig() ); + // Running standalone, so create our own event manager + configureEventManager(); + } } // Configure Logging for the Cache Factory @@ -188,15 +185,16 @@ * @throws CacheBox.ListenerCreationException */ CacheFactory function registerListeners(){ - variables.config.getListeners() + variables.config + .getListeners() .each( function( item ){ // try to create it - try{ + try { // create it var thisListener = createObject( "component", item.class ); // configure it thisListener.configure( this, item.properties ); - } catch( Any e ){ + } catch ( Any e ) { throw( message = "Error creating listener: #item.toString()#", detail = "#e.message# #e.detail# #e.stackTrace#", @@ -217,12 +215,7 @@ * @config.doc_generic coldbox.system.cache.config.CacheBoxConfig */ function configure( required config ){ - lock - name="#variables.lockName#" - type="exclusive" - timeout="30" - throwontimeout="true" - { + lock name="#variables.lockName#" type="exclusive" timeout="30" throwontimeout="true" { // Store config object variables.config = arguments.config; // Validate configuration @@ -231,7 +224,7 @@ variables.caches = {}; // Register Listeners if not using ColdBox - if( not isObject( variables.coldbox ) ){ + if ( not isObject( variables.coldbox ) ) { registerListeners(); } @@ -244,22 +237,23 @@ ); // Register named caches - variables.config.getCaches() + variables.config + .getCaches() .each( function( key, def ){ createCache( name = key, provider = def.provider, properties = def.properties ); - }); + } ); // Scope registrations - if( variables.config.getScopeRegistration().enabled ){ + if ( variables.config.getScopeRegistration().enabled ) { doScopeRegistration(); } // Announce To Listeners - variables.eventManager.announce( "afterCacheFactoryConfiguration", { cacheFactory = this } ); + variables.eventManager.announce( "afterCacheFactoryConfiguration", { cacheFactory : this } ); } } @@ -274,8 +268,8 @@ * @return coldbox.system.cache.providers.ICacheProvider */ function getCache( required name ){ - lock name="#variables.lockName#" type="readonly" timeout="20" throwontimeout="true"{ - if( variables.caches.keyExists( arguments.name ) ){ + lock name="#variables.lockName#" type="readonly" timeout="20" throwontimeout="true" { + if ( variables.caches.keyExists( arguments.name ) ) { return variables.caches[ arguments.name ]; } throw( @@ -309,7 +303,7 @@ var defaultCacheConfig = variables.config.getDefaultCache(); // Check length - if( len( arguments.name ) eq 0 ){ + if ( len( arguments.name ) eq 0 ) { throw( message = "Invalid Cache Name", detail = "The name you sent in is invalid as it was blank, please send in a name", @@ -318,7 +312,7 @@ } // Check it does not exist already - if( cacheExists( arguments.name ) ){ + if ( cacheExists( arguments.name ) ) { throw( message = "Cache #arguments.name# already exists", detail = "Cannot register named cache as it already exists in the registry", @@ -339,12 +333,12 @@ */ CacheFactory function shutdown(){ // Log startup - if( variables.log.canDebug() ){ + if ( variables.log.canDebug() ) { variables.log.debug( "Shutdown of cache factory: #getFactoryID()# requested and started." ); } // Notify Listeners - variables.eventManager.announce( "beforeCacheFactoryShutdown", { cacheFactory = this } ); + variables.eventManager.announce( "beforeCacheFactoryShutdown", { cacheFactory : this } ); // safely iterate and shutdown caches getCacheNames().each( function( item ){ @@ -352,21 +346,21 @@ var cache = getCache( item ); // Log it - if( variables.log.canDebug() ){ + if ( variables.log.canDebug() ) { variables.log.debug( "Shutting down cache: #item# on factoryID: #getFactoryID()#." ); } - //process listeners - variables.eventManager.announce( "beforeCacheShutdown", { cache = cache } ); + // process listeners + variables.eventManager.announce( "beforeCacheShutdown", { cache : cache } ); - //Shutdown each cache + // Shutdown each cache cache.shutdown(); - //process listeners - variables.eventManager.announce( "afterCacheShutdown", { cache = cache } ); + // process listeners + variables.eventManager.announce( "afterCacheShutdown", { cache : cache } ); // log - if( variables.log.canDebug() ){ + if ( variables.log.canDebug() ) { variables.log.debug( "Cache: #item# was shut down on factoryID: #getFactoryID()#." ); } } ); @@ -378,18 +372,18 @@ removeFromScope(); // Shutdown LogBox and Executors if not in ColdBox Mode or WireBox mode - if( !isObject( variables.coldbox ) && !isObject( variables.wirebox ) ){ - if( isObject( variables.logBox ) ) { + if ( !isObject( variables.coldbox ) && !isObject( variables.wirebox ) ) { + if ( isObject( variables.logBox ) ) { variables.logBox.shutdown(); } variables.asyncManager.shutdownAllExecutors( force = true ); } // Notify Listeners - variables.eventManager.announce( "afterCacheFactoryShutdown", { cacheFactory = this } ); + variables.eventManager.announce( "afterCacheFactoryShutdown", { cacheFactory : this } ); // Log shutdown complete - if( variables.log.canDebug() ){ + if ( variables.log.canDebug() ) { variables.log.debug( "Shutdown of cache factory: #getFactoryID()# completed." ); } @@ -402,14 +396,16 @@ * @name The name of the cache to shutdown */ CacheFactory function shutdownCache( required name ){ - var iData = {}; - var cache = ""; - var i = 1; + var iData = {}; + var cache = ""; + var i = 1; // Check if cache exists, else exit out - if( NOT cacheExists( arguments.name ) ){ - if( variables.log.canWarn() ){ - variables.log.warn( "Trying to shutdown #arguments.name#, but that cache does not exist, skipping." ); + if ( NOT cacheExists( arguments.name ) ) { + if ( variables.log.canWarn() ) { + variables.log.warn( + "Trying to shutdown #arguments.name#, but that cache does not exist, skipping." + ); } return this; } @@ -418,25 +414,29 @@ var cache = getCache( arguments.name ); // log it - if( variables.log.canDebug() ){ - variables.log.debug( "Shutdown of cache: #arguments.name# requested and started on factoryID: #getFactoryID()#" ); + if ( variables.log.canDebug() ) { + variables.log.debug( + "Shutdown of cache: #arguments.name# requested and started on factoryID: #getFactoryID()#" + ); } // Notify Listeners - variables.eventManager.announce( "beforeCacheShutdown", { cache = cache } ); + variables.eventManager.announce( "beforeCacheShutdown", { cache : cache } ); - //Shutdown the cache + // Shutdown the cache cache.shutdown(); - //process listeners - variables.eventManager.announce( "afterCacheShutdown", { cache = cache } ); + // process listeners + variables.eventManager.announce( "afterCacheShutdown", { cache : cache } ); // remove cache removeCache( arguments.name ); // Log it - if( variables.log.canDebug() ){ - variables.log.debug( "Cache: #arguments.name# was shut down and removed on factoryID: #getFactoryID()#." ); + if ( variables.log.canDebug() ) { + variables.log.debug( + "Cache: #arguments.name# was shut down and removed on factoryID: #getFactoryID()#." + ); } return this; @@ -447,9 +447,8 @@ */ CacheFactory function removeFromScope(){ var scopeInfo = variables.config.getScopeRegistration(); - if( scopeInfo.enabled ){ - new coldbox.system.core.collections.ScopeStorage() - .delete( scopeInfo.key, scopeInfo.scope ); + if ( scopeInfo.enabled ) { + new coldbox.system.core.collections.ScopeStorage().delete( scopeInfo.key, scopeInfo.scope ); } return this; } @@ -460,19 +459,21 @@ * @name The name of the cache to remove */ boolean function removeCache( required name ){ - if( cacheExists( arguments.name ) ){ - lock name="#variables.lockName#" type="exclusive" timeout="20" throwontimeout="true"{ - if( cacheExists( arguments.name ) ){ - //Log - if( variables.log.canDebug() ){ - variables.log.debug( "Cache: #arguments.name# asked to be removed from factory: #getFactoryID()#" ); + if ( cacheExists( arguments.name ) ) { + lock name="#variables.lockName#" type="exclusive" timeout="20" throwontimeout="true" { + if ( cacheExists( arguments.name ) ) { + // Log + if ( variables.log.canDebug() ) { + variables.log.debug( + "Cache: #arguments.name# asked to be removed from factory: #getFactoryID()#" + ); } // Retrieve it var cache = variables.caches[ arguments.name ]; // Notify listeners here - variables.eventManager.announce( "beforeCacheRemoval", { cache = cache } ); + variables.eventManager.announce( "beforeCacheRemoval", { cache : cache } ); // process shutdown cache.shutdown(); @@ -481,10 +482,10 @@ structDelete( variables.caches, arguments.name ); // Announce it - variables.eventManager.announce( "afterCacheRemoval", { cache = arguments.name } ); + variables.eventManager.announce( "afterCacheRemoval", { cache : arguments.name } ); // Log it - if( variables.log.canDebug() ){ + if ( variables.log.canDebug() ) { variables.log.debug( "Cache: #arguments.name# removed from factory: #getFactoryID()#" ); } @@ -493,8 +494,10 @@ } } - if( variables.log.canDebug() ){ - variables.log.debug( "Cache: #arguments.name# not removed because it does not exist in registered caches: #arrayToList( getCacheNames() )#. FactoryID: #getFactoryID()#" ); + if ( variables.log.canDebug() ) { + variables.log.debug( + "Cache: #arguments.name# not removed because it does not exist in registered caches: #arrayToList( getCacheNames() )#. FactoryID: #getFactoryID()#" + ); } return false; @@ -504,7 +507,7 @@ * Remove all the registered caches in this factory, this triggers individual cache shutdowns */ CacheFactory function removeAll(){ - if( variables.log.canDebug() ){ + if ( variables.log.canDebug() ) { variables.log.debug( "Removal of all caches requested on factoryID: #getFactoryID()#" ); } @@ -513,7 +516,7 @@ } ); - if( variables.log.canDebug() ){ + if ( variables.log.canDebug() ) { variables.log.debug( "All caches removed." ); } @@ -524,7 +527,7 @@ * A nice way to call reap on all registered caches */ CacheFactory function reapAll(){ - if( variables.log.canDebug() ){ + if ( variables.log.canDebug() ) { variables.log.debug( "Executing reap on factoryID: #getFactoryID()#" ); } @@ -539,7 +542,7 @@ * Check if the passed in named cache is already registered in this factory or not */ boolean function cacheExists( required name ){ - lock name="#variables.lockName#" type="readonly" timeout="20" throwontimeout="true"{ + lock name="#variables.lockName#" type="readonly" timeout="20" throwontimeout="true" { return structKeyExists( variables.caches, arguments.name ); } } @@ -554,20 +557,20 @@ */ CacheFactory function replaceCache( required cache, required decoratedCache ){ // determine cache name - if( isObject( arguments.cache ) ){ + if ( isObject( arguments.cache ) ) { var name = arguments.cache.getName(); } else { var name = arguments.cache; } - lock name="#variables.lockName#" type="exclusive" timeout="20" throwontimeout="true"{ + lock name="#variables.lockName#" type="exclusive" timeout="20" throwontimeout="true" { // Announce to listeners var iData = { - oldCache = variables.caches[ name ], - newCache = arguments.decoratedCache + oldCache : variables.caches[ name ], + newCache : arguments.decoratedCache }; - variables.eventManager.announce( "beforeCacheReplacement", iData ); + variables.eventManager.announce( "beforeCacheReplacement", iData ); // remove old Cache structDelete( variables.caches, name ); @@ -576,8 +579,10 @@ variables.caches[ name ] = arguments.decoratedCache; // debugging - if( variables.log.canDebug() ){ - variables.log.debug( "Cache #name# replaced with decorated cache: #getMetadata( arguments.decoratedCache ).name# on factoryID: #getFactoryID()#" ); + if ( variables.log.canDebug() ) { + variables.log.debug( + "Cache #name# replaced with decorated cache: #getMetadata( arguments.decoratedCache ).name# on factoryID: #getFactoryID()#" + ); } } @@ -588,7 +593,7 @@ * Clears all the elements in all the registered caches without de-registrations */ CacheFactory function clearAll(){ - if( variables.log.canDebug() ){ + if ( variables.log.canDebug() ) { variables.log.debug( "Clearing all registered caches of their content on factoryID: #getFactoryID()#" ); } @@ -603,7 +608,7 @@ * Expires all the elements in all the registered caches without de-registrations */ CacheFactory function expireAll(){ - if( variables.log.canDebug() ){ + if ( variables.log.canDebug() ) { variables.log.debug( "Expiring all registered caches of their content on factoryID: #getFactoryID()#" ); } @@ -618,7 +623,7 @@ * Get the array of caches registered with this factory */ array function getCacheNames(){ - lock name="#variables.lockName#" type="readonly" timeout="20" throwontimeout="true"{ + lock name="#variables.lockName#" type="readonly" timeout="20" throwontimeout="true" { return structKeyArray( variables.caches ); } } @@ -655,7 +660,11 @@ * * @return coldbox.system.cache.providers.ICacheProvider */ - function createCache( required name, required provider, struct properties={} ){ + function createCache( + required name, + required provider, + struct properties = {} + ){ // Create Cache var oCache = createObject( "component", arguments.provider ).init(); // Register Name @@ -675,8 +684,7 @@ */ private CacheFactory function doScopeRegistration(){ var scopeInfo = variables.config.getScopeRegistration(); - new coldbox.system.core.collections.ScopeStorage() - .put( scopeInfo.key, this, scopeInfo.scope ); + new coldbox.system.core.collections.ScopeStorage().put( scopeInfo.key, this, scopeInfo.scope ); return this; } @@ -689,28 +697,25 @@ * @throws CacheFactory.CacheExistsException */ private CacheFactory function registerCache( required cache ){ - var name = arguments.cache.getName(); - var oCache = arguments.cache; + var name = arguments.cache.getName(); + var oCache = arguments.cache; - if( variables.caches.keyExists( name ) ){ - throw( - message = "Cache #name# already exists!", - type = "CacheFactory.CacheExistsException" - ); + if ( variables.caches.keyExists( name ) ) { + throw( message = "Cache #name# already exists!", type = "CacheFactory.CacheExistsException" ); } // Verify Registration - if( !variables.caches.keyExists( name ) ){ - lock name="#variables.lockName#" type="exclusive" timeout="20" throwontimeout="true"{ - if( !variables.caches.keyExists( name ) ){ + if ( !variables.caches.keyExists( name ) ) { + lock name="#variables.lockName#" type="exclusive" timeout="20" throwontimeout="true" { + if ( !variables.caches.keyExists( name ) ) { // Link to this CacheFactory oCache.setCacheFactory( this ); // Link ColdBox if using it - if( isObject( variables.coldbox ) AND structKeyExists( oCache, "setColdBox" ) ){ + if ( isObject( variables.coldbox ) AND structKeyExists( oCache, "setColdBox" ) ) { oCache.setColdBox( variables.coldbox ); } // Link WireBox if using it - if( isObject( variables.wirebox ) AND structKeyExists( oCache, "setWireBox" ) ){ + if ( isObject( variables.wirebox ) AND structKeyExists( oCache, "setWireBox" ) ) { oCache.setWireBox( variables.wirebox ); } // Link Event Manager @@ -720,7 +725,7 @@ // Store it variables.caches[ name ] = oCache; // Announce new cache registration now - variables.eventManager.announce( "afterCacheRegistration", { cache = oCache } ); + variables.eventManager.announce( "afterCacheRegistration", { cache : oCache } ); } } } @@ -735,7 +740,7 @@ */ private CacheFactory function configureLogBox( required configPath ){ // Create config object - var oConfig = new coldbox.system.logging.config.LogBoxConfig( CFCConfigPath = arguments.configPath ); + var oConfig = new coldbox.system.logging.config.LogBoxConfig( CFCConfigPath = arguments.configPath ); // Create LogBox standalone and store it variables.logBox = new coldbox.system.logging.LogBox( oConfig ); return this; @@ -759,4 +764,4 @@ return new coldbox.system.core.util.Util(); } - } \ No newline at end of file +} diff --git a/system/ioc/Injector.cfc b/system/ioc/Injector.cfc index 28716be14..b3cab9321 100644 --- a/system/ioc/Injector.cfc +++ b/system/ioc/Injector.cfc @@ -23,7 +23,11 @@ * injector = new coldbox.system.ioc.Injector( "config.MyBinder" ); * */ -component serializable="false" accessors="true" implements="coldbox.system.ioc.IInjector"{ +component + serializable="false" + accessors ="true" + implements ="coldbox.system.ioc.IInjector" +{ /** * Java System @@ -103,7 +107,7 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I /** * The logBox task scheduler executor - * @see coldbox.system.async.tasks.ScheduledExecutor + * @see coldbox.system.async.executors.ScheduledExecutor */ property name="taskScheduler"; @@ -117,22 +121,22 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I * @coldbox.doc_generic coldbox.system.web.Controller **/ Injector function init( - binder="coldbox.system.ioc.config.DefaultBinder", - struct properties=structNew(), - coldbox="" + binder = "coldbox.system.ioc.config.DefaultBinder", + struct properties = structNew(), + coldbox = "" ){ // Setup Available public scopes this.SCOPES = new coldbox.system.ioc.Scopes(); // Setup Available public types - this.TYPES = new coldbox.system.ioc.Types(); + this.TYPES = new coldbox.system.ioc.Types(); // Do we have a binder? - if( isSimpleValue( arguments.binder ) AND NOT len( trim( arguments.binder ) ) ){ + if ( isSimpleValue( arguments.binder ) AND NOT len( trim( arguments.binder ) ) ) { arguments.binder = "coldbox.system.ioc.config.DefaultBinder"; } // Java System - variables.javaSystem = createObject( 'java', 'java.lang.System' ); + variables.javaSystem = createObject( "java", "java.lang.System" ); // Utility class variables.utility = new coldbox.system.core.util.Util(); // Scope Storages @@ -151,38 +155,41 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I variables.eventManager = ""; // Configured Event States variables.eventStates = [ - "afterInjectorConfiguration", // X once injector is created and configured - "beforeInstanceCreation", // X Before an injector creates or is requested an instance of an object, the mapping is passed. - "afterInstanceInitialized", // X once the constructor is called and before DI is performed - "afterInstanceCreation", // X once an object is created, initialized and done with DI - "beforeInstanceInspection", // X before an object is inspected for injection metadata - "afterInstanceInspection", // X after an object has been inspected and metadata is ready to be saved - "beforeInjectorShutdown", // X right before the shutdown procedures start - "afterInjectorShutdown", // X right after the injector is shutdown - "beforeInstanceAutowire", // X right before an instance is autowired - "afterInstanceAutowire" // X right after an instance is autowired + "afterInjectorConfiguration", // X once injector is created and configured + "beforeInstanceCreation", // X Before an injector creates or is requested an instance of an object, the mapping is passed. + "afterInstanceInitialized", // X once the constructor is called and before DI is performed + "afterInstanceCreation", // X once an object is created, initialized and done with DI + "beforeInstanceInspection", // X before an object is inspected for injection metadata + "afterInstanceInspection", // X after an object has been inspected and metadata is ready to be saved + "beforeInjectorShutdown", // X right before the shutdown procedures start + "afterInjectorShutdown", // X right after the injector is shutdown + "beforeInstanceAutowire", // X right before an instance is autowired + "afterInstanceAutowire" // X right after an instance is autowired ]; // LogBox and Class Logger - variables.logBox = ""; - variables.log = ""; + variables.logBox = ""; + variables.log = ""; // Parent Injector - variables.parent = ""; + variables.parent = ""; // LifeCycle Scopes - variables.scopes = {}; + variables.scopes = {}; // Prepare instance ID variables.injectorID = variables.javaSystem.identityHashCode( this ); // Prepare Lock Info - variables.lockName = "WireBox.Injector.#variables.injectorID#"; + variables.lockName = "WireBox.Injector.#variables.injectorID#"; // Link ColdBox Context if passed - variables.coldbox = arguments.coldbox; + variables.coldbox = arguments.coldbox; // Register the task scheduler according to operating mode - if( !isObject( variables.coldbox ) ){ - variables.asyncManager = new coldbox.system.async.AsyncManager(); - variables.taskScheduler = variables.asyncManager.newScheduledExecutor( name : "wirebox-tasks", threads : 20 ); + if ( !isObject( variables.coldbox ) ) { + variables.asyncManager = new coldbox.system.async.AsyncManager(); + variables.taskScheduler = variables.asyncManager.newScheduledExecutor( + name : "wirebox-tasks", + threads: 20 + ); } else { - variables.asyncManager = variables.coldbox.getAsyncManager(); + variables.asyncManager = variables.coldbox.getAsyncManager(); variables.taskScheduler = variables.asyncManager.getExecutor( "coldbox-tasks" ); } @@ -201,18 +208,18 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I * @properties.doc_generic struct **/ Injector function configure( required binder, required struct properties ){ - var iData = {}; - var withColdbox = isColdBoxLinked(); + var iData = {}; + var withColdbox = isColdBoxLinked(); // Lock For Configuration - lock name=variables.lockName type="exclusive" timeout="30" throwontimeout="true"{ - if( withColdBox ){ + lock name=variables.lockName type="exclusive" timeout="30" throwontimeout="true" { + if ( withColdBox ) { // link LogBox - variables.logBox = variables.coldbox.getLogBox(); + variables.logBox = variables.coldbox.getLogBox(); // Configure Logging for this injector - variables.log = variables.logBox.getLogger( this ); + variables.log = variables.logBox.getLogger( this ); // Link CacheBox - variables.cacheBox = variables.coldbox.getCacheBox(); + variables.cacheBox = variables.coldbox.getCacheBox(); // Link Event Manager variables.eventManager = variables.coldbox.getInterceptorService(); } @@ -224,7 +231,7 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I variables.binder = buildBinder( arguments.binder, arguments.properties ); // Create local cache, logging and event management if not coldbox context linked. - if( NOT withColdbox ){ + if ( NOT withColdbox ) { // Running standalone, so create our own logging first configureLogBox( variables.binder.getLogBoxConfig() ); // Create local CacheBox reference @@ -240,24 +247,27 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I // Register Life Cycle Scopes registerScopes(); // Parent Injector declared - if( isObject( variables.binder.getParentInjector() ) ){ + if ( isObject( variables.binder.getParentInjector() ) ) { setParent( variables.binder.getParentInjector() ); } // Scope registration if enabled? - if( variables.binder.getScopeRegistration().enabled ){ + if ( variables.binder.getScopeRegistration().enabled ) { doScopeRegistration(); } // Register binder as an interceptor - if( NOT isColdBoxLinked() ){ + if ( NOT isColdBoxLinked() ) { variables.eventManager.register( variables.binder, "wirebox-binder" ); } else { - variables.eventManager.registerInterceptor( interceptorObject=variables.binder, interceptorName="wirebox-binder" ); + variables.eventManager.registerInterceptor( + interceptorObject = variables.binder, + interceptorName = "wirebox-binder" + ); } // process mappings for metadata and initialization. - if( variables.binder.getAutoProcessMappings() ){ + if ( variables.binder.getAutoProcessMappings() ) { variables.binder.processMappings(); } @@ -265,7 +275,7 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I variables.binder.processEagerInits(); // Check if binder has onLoad convention and execute callback - if( structKeyExists( variables.binder, "onLoad" ) ){ + if ( structKeyExists( variables.binder, "onLoad" ) ) { variables.binder.onLoad( this ); } @@ -281,12 +291,10 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I * Shutdown the injector gracefully by calling the shutdown events internally. **/ function shutdown(){ - var iData = { - injector = this - }; + var iData = { injector : this }; // Log - if( variables.log.canInfo() ){ + if ( variables.log.canInfo() ) { variables.log.info( "Shutdown of Injector: #getInjectorID()# requested and started." ); } @@ -294,17 +302,17 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I variables.eventManager.announce( "beforeInjectorShutdown", iData ); // Check if binder has onShutdown convention - if( structKeyExists( variables.binder, "onShutdown" ) ){ + if ( structKeyExists( variables.binder, "onShutdown" ) ) { variables.binder.onShutdown( this ); } // Is parent linked - if( isObject( variables.parent ) ){ + if ( isObject( variables.parent ) ) { variables.parent.shutdown( this ); } // standalone cachebox? Yes, then shut it down baby! - if( isCacheBoxLinked() ){ + if ( isCacheBoxLinked() ) { variables.cacheBox.shutdown( this ); } @@ -315,18 +323,18 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I variables.eventManager.announce( "afterInjectorShutdown", iData ); // Log shutdown complete - if( variables.log.canInfo() ){ + if ( variables.log.canInfo() ) { variables.log.info( "Shutdown of injector: #getInjectorID()# completed." ); } // Shutdown LogBox last if not in ColdBox Mode - if( !isColdBoxLinked() ){ + if ( !isColdBoxLinked() ) { variables.logBox.shutdown(); } // Shutdown Executors if not in ColdBox Mode // This needs to happen AFTER logbox is shutdown since they share the taskScheduler - if( !isColdBoxLinked() ){ + if ( !isColdBoxLinked() ) { variables.asyncManager.shutdownAllExecutors( force = true ); } @@ -345,15 +353,19 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I * * @return The requested instance **/ - function getInstance( name, struct initArguments = {}, dsl, targetObject="" ){ - + function getInstance( + name, + struct initArguments = {}, + dsl, + targetObject = "" + ){ // Is the name a DSL? - if( !isNull( arguments.name ) && variables.builder.isDSLString( arguments.name ) ){ + if ( !isNull( arguments.name ) && variables.builder.isDSLString( arguments.name ) ) { arguments.dsl = arguments.name; } // Get by DSL? - if( !isNull( arguments.dsl ) ){ + if ( !isNull( arguments.dsl ) ) { return variables.builder.buildSimpleDSL( dsl = arguments.dsl, targetID = "ExplicitCall", @@ -362,22 +374,24 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I } // Check if Mapping Exists? - if( NOT variables.binder.mappingExists( arguments.name ) ){ + if ( NOT variables.binder.mappingExists( arguments.name ) ) { // No Mapping exists, let's try to locate it first. We are now dealing with request by conventions var instancePath = locateInstance( arguments.name ); // check if not found and if we have a parent factory - if( NOT len( instancePath ) AND isObject( variables.parent ) ){ + if ( NOT len( instancePath ) AND isObject( variables.parent ) ) { // we do have a parent factory so just request it from there, let the hierarchy deal with it - return variables.parent.getInstance( argumentCollection=arguments ); + return variables.parent.getInstance( argumentCollection = arguments ); } // If Empty Throw Exception - if( NOT len( instancePath ) ){ - variables.log.error( "Requested instance:#arguments.name# was not located in any declared scan location(s): #structKeyList(variables.binder.getScanLocations())# or full CFC path" ); + if ( NOT len( instancePath ) ) { + variables.log.error( + "Requested instance:#arguments.name# was not located in any declared scan location(s): #structKeyList( variables.binder.getScanLocations() )# or full CFC path" + ); throw( message = "Requested instance not found: '#arguments.name#'", - detail = "The instance could not be located in any declared scan location(s) (#structKeyList(variables.binder.getScanLocations())#) or full path location", + detail = "The instance could not be located in any declared scan location(s) (#structKeyList( variables.binder.getScanLocations() )#) or full path location", type = "Injector.InstanceNotFoundException" ); } @@ -389,37 +403,38 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I var mapping = variables.binder.getMapping( arguments.name ); // Check if the mapping has been discovered yet, and if it hasn't it must be autowired enabled in order to process. - if( NOT mapping.isDiscovered() ){ + if ( NOT mapping.isDiscovered() ) { try { // process inspection of instance - mapping.process( binder=variables.binder, injector=this ); - } catch( any e ) { + mapping.process( binder = variables.binder, injector = this ); + } catch ( any e ) { // Remove bad mapping var mappings = variables.binder.getMappings(); mappings.delete( name ); // rethrow - throw( object=e ); + throw( object = e ); } } // scope persistence check - if( NOT structKeyExists( variables.scopes, mapping.getScope() ) ){ - variables.log.error( "The mapping scope: #mapping.getScope()# is invalid and not registered in the valid scopes: #structKeyList( variables.scopes )#" ); + if ( NOT structKeyExists( variables.scopes, mapping.getScope() ) ) { + variables.log.error( + "The mapping scope: #mapping.getScope()# is invalid and not registered in the valid scopes: #structKeyList( variables.scopes )#" + ); throw( message = "Requested mapping scope: #mapping.getScope()# is invalid for #mapping.getName()#", - detail = "The registered valid object scopes are #structKeyList(variables.scopes)#", - type = "Injector.InvalidScopeException" ); + detail = "The registered valid object scopes are #structKeyList( variables.scopes )#", + type = "Injector.InvalidScopeException" + ); } // Request object from scope now, we now have it from the scope created, initialized and wired - var target = variables - .scopes[ mapping.getScope() ] - .getFromScope( mapping, arguments.initArguments ); + var target = variables.scopes[ mapping.getScope() ].getFromScope( mapping, arguments.initArguments ); // Announce creation, initialization and DI magicfinicitation! variables.eventManager.announce( "afterInstanceCreation", - { mapping=mapping, target=target, injector=this } + { mapping : mapping, target : target, injector : this } ); return target; @@ -438,51 +453,51 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I // before construction event variables.eventManager.announce( "beforeInstanceCreation", - { mapping=arguments.mapping, injector=this } + { mapping : arguments.mapping, injector : this } ); - var oModel = ""; + var oModel = ""; // determine construction type - switch( thisMap.getType() ){ - case "cfc" : { + switch ( thisMap.getType() ) { + case "cfc": { oModel = variables.builder.buildCFC( thisMap, arguments.initArguments ); break; } - case "java" : { + case "java": { oModel = variables.builder.buildJavaClass( thisMap ); break; } - case "webservice" : { + case "webservice": { oModel = variables.builder.buildWebservice( thisMap, arguments.initArguments ); break; } - case "constant" : { + case "constant": { oModel = thisMap.getValue(); break; } - case "rss" : { + case "rss": { oModel = variables.builder.buildFeed( thisMap ); break; } - case "dsl" : { - oModel = variables.builder.buildSimpleDSL( dsl=thisMap.getDSL(), targetID=thisMap.getName() ); + case "dsl": { + oModel = variables.builder.buildSimpleDSL( dsl = thisMap.getDSL(), targetID = thisMap.getName() ); break; } - case "factory" : { + case "factory": { oModel = variables.builder.buildFactoryMethod( thisMap, arguments.initArguments ); break; } - case "provider" : { + case "provider": { // verify if it is a simple value or closure/UDF - if( isSimpleValue( thisMap.getPath() ) ){ + if ( isSimpleValue( thisMap.getPath() ) ) { oModel = getInstance( thisMap.getPath() ).$get(); } else { var closure = thisMap.getPath(); - oModel = closure( injector = this ); + oModel = closure( injector = this ); } break; } - default : { + default: { throw( message = "Invalid Construction Type: #thisMap.getType()#", type = "Injector.InvalidConstructionType" @@ -492,24 +507,30 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I // Check and see if this mapping as an influence closure var influenceClosure = thisMap.getInfluenceClosure(); - if( !isSimpleValue( influenceClosure ) ) { + if ( !isSimpleValue( influenceClosure ) ) { // Influence the creation of the instance - var result = influenceClosure( instance=oModel, injector=this ); + var result = influenceClosure( instance = oModel, injector = this ); // Allow the closure to override the entire instance if it wishes - if( !isNull( local.result ) ){ + if ( !isNull( local.result ) ) { oModel = result; } } // log data - if( variables.log.canDebug() ){ - variables.log.debug( "Instance object built: #arguments.mapping.getName()#:#arguments.mapping.getPath().toString()#" ); + if ( variables.log.canDebug() ) { + variables.log.debug( + "Instance object built: #arguments.mapping.getName()#:#arguments.mapping.getPath().toString()#" + ); } // announce afterInstanceInitialized variables.eventManager.announce( "afterInstanceInitialized", - { mapping=arguments.mapping, target=oModel, injector=this } + { + mapping : arguments.mapping, + target : oModel, + injector : this + } ); return oModel; @@ -522,34 +543,35 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I * @instancePath The path of the mapping to register **/ function registerNewInstance( required name, required instancePath ){ - // Register new instance mapping - lock name="Injector.#getInjectorID()#.RegisterNewInstance.#hash( arguments.instancePath )#" - type="exclusive" - timeout="20" - throwontimeout="true"{ - if( NOT variables.binder.mappingExists( arguments.name ) ){ - // create a new mapping to be registered within the binder - var mapping = new coldbox.system.ioc.config.Mapping( arguments.name ) - .setType( variables.binder.TYPES.CFC ) - .setPath( arguments.instancePath ); - // Now register it - variables.binder.setMapping( arguments.name, mapping ); - // return it - return mapping; - } + // Register new instance mapping + lock + name ="Injector.#getInjectorID()#.RegisterNewInstance.#hash( arguments.instancePath )#" + type ="exclusive" + timeout ="20" + throwontimeout="true" { + if ( NOT variables.binder.mappingExists( arguments.name ) ) { + // create a new mapping to be registered within the binder + var mapping = new coldbox.system.ioc.config.Mapping( arguments.name ) + .setType( variables.binder.TYPES.CFC ) + .setPath( arguments.instancePath ); + // Now register it + variables.binder.setMapping( arguments.name, mapping ); + // return it + return mapping; + } } return variables.binder.getMapping( arguments.name ); - } - - /** - * A direct way of registering custom DSL namespaces - * - * @namespace The namespace you would like to register - * @path The instantiation path to the CFC that implements this scope, it must have an init() method and implement: coldbox.system.ioc.dsl.IDSLBuilder - */ - Injector function registerDSL( required namespace, required path ){ - variables.builder.registerDSL( argumentCollection=arguments ); + } + + /** + * A direct way of registering custom DSL namespaces + * + * @namespace The namespace you would like to register + * @path The instantiation path to the CFC that implements this scope, it must have an init() method and implement: coldbox.system.ioc.dsl.IDSLBuilder + */ + Injector function registerDSL( required namespace, required path ){ + variables.builder.registerDSL( argumentCollection = arguments ); return this; } @@ -560,15 +582,15 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I */ boolean function containsInstance( required name ){ // check if we have a mapping first - if( variables.binder.mappingExists( arguments.name ) ){ + if ( variables.binder.mappingExists( arguments.name ) ) { return true; } // check if we can locate it? - if( locateInstance( arguments.name ).len() ){ + if ( locateInstance( arguments.name ).len() ) { return true; } // Ask parent hierarchy if set - if( isObject( variables.parent ) ){ + if ( isObject( variables.parent ) ) { return variables.parent.containsInstance( arguments.name ); } @@ -582,31 +604,37 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I * @name The model instance name to locate */ function locateInstance( required name ){ - var scanLocations = variables.binder.getScanLocations(); - var CFCName = replace( arguments.name, ".", "/", "all" ) & ".cfc"; + var scanLocations = variables.binder.getScanLocations(); + var CFCName = replace( arguments.name, ".", "/", "all" ) & ".cfc"; // If we find a :, then avoid doing lookups on the i/o system. - if( find( ":", CFCName ) ){ + if ( find( ":", CFCName ) ) { return ""; } // Check Scan Locations In Order - for( var thisScanPath in scanLocations ){ + for ( var thisScanPath in scanLocations ) { // Check if located? If so, return instantiation path - if( fileExists( scanLocations[ thisScanPath ] & CFCName ) ){ - if( variables.log.canDebug() ){ variables.log.debug( "Instance: #arguments.name# located in #thisScanPath#" ); } + if ( fileExists( scanLocations[ thisScanPath ] & CFCName ) ) { + if ( variables.log.canDebug() ) { + variables.log.debug( "Instance: #arguments.name# located in #thisScanPath#" ); + } return thisScanPath & "." & arguments.name; } } // Not found, so let's do full namespace location - if( fileExists( expandPath( "/" & CFCName ) ) ){ - if( variables.log.canDebug() ){ variables.log.debug( "Instance: #arguments.name# located as is." ); } + if ( fileExists( expandPath( "/" & CFCName ) ) ) { + if ( variables.log.canDebug() ) { + variables.log.debug( "Instance: #arguments.name# located as is." ); + } return arguments.name; } // debug info, NADA found! - if( variables.log.canDebug() ){ variables.log.debug( "Instance: #arguments.name# was not located anywhere" ); } + if ( variables.log.canDebug() ) { + variables.log.debug( "Instance: #arguments.name# was not located anywhere" ); + } return ""; } @@ -624,60 +652,68 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I function autowire( required target, mapping, - targetID="", - boolean annotationCheck=false + targetID = "", + boolean annotationCheck = false ){ - var targetObject = arguments.target; - var md = ""; + var targetObject = arguments.target; + var md = ""; // Do we have a mapping? Or is this a-la-carte wiring - if( NOT structKeyExists( arguments, "mapping" ) ){ + if ( NOT structKeyExists( arguments, "mapping" ) ) { // Ok, a-la-carte wiring, let's get our id first // Do we have an incoming target id? - if( NOT len( arguments.targetID ) ){ + if ( NOT len( arguments.targetID ) ) { // need to get metadata to verify identity - md = variables.utility.getInheritedMetaData( arguments.target, getBinder().getStopRecursions() ); + md = variables.utility.getInheritedMetaData( arguments.target, getBinder().getStopRecursions() ); // We have identity now, use the full location path arguments.targetID = md.path; } // Now that we know we have an identity, let's verify if we have a mapping already - if( NOT variables.binder.mappingExists( arguments.targetID ) ){ + if ( NOT variables.binder.mappingExists( arguments.targetID ) ) { // No mapping found, means we need to map this object for the first time. // Is md retrieved? If not, retrieve it as we need to register it for the first time. - if( isSimpleValue( md ) ){ - md = variables.utility.getInheritedMetaData( arguments.target, getBinder().getStopRecursions() ); + if ( isSimpleValue( md ) ) { + md = variables.utility.getInheritedMetaData( + arguments.target, + getBinder().getStopRecursions() + ); } // register new mapping instance registerNewInstance( arguments.targetID, md.path ); // get Mapping created arguments.mapping = variables.binder.getMapping( arguments.targetID ); // process it with current metadata - arguments.mapping.process( binder=variables.binder, injector=this, metadata=md ); + arguments.mapping.process( + binder = variables.binder, + injector = this, + metadata = md + ); } else { // get the mapping as it exists already arguments.mapping = variables.binder.getMapping( arguments.targetID ); } - }// end if mapping not found + } + // end if mapping not found // Set local variable for easy reference use mapping to wire object up. var thisMap = arguments.mapping; - if( NOT len( arguments.targetID ) ){ + if ( NOT len( arguments.targetID ) ) { arguments.targetID = thisMap.getName(); } // Only autowire if no annotation check or if there is one, make sure the mapping is set for autowire, and this is a CFC - if ( thisMap.getType() eq this.TYPES.CFC - AND - ( ( arguments.annotationCheck eq false ) OR ( arguments.annotationCheck AND thisMap.isAutowire() ) ) - ){ - + if ( + thisMap.getType() eq this.TYPES.CFC + AND + ( ( arguments.annotationCheck eq false ) OR ( arguments.annotationCheck AND thisMap.isAutowire() ) ) + ) { // announce beforeInstanceAutowire var iData = { - mapping = thisMap, - target = arguments.target, - targetID = arguments.targetID, - injector = this + mapping : thisMap, + target : arguments.target, + targetID : arguments.targetID, + injector : this }; variables.eventManager.announce( "beforeInstanceAutowire", iData ); @@ -685,20 +721,28 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I variables.utility.getMixerUtil().start( arguments.target ); // Bean Factory Awareness - if( structKeyExists( targetObject, "setBeanFactory" ) ){ + if ( structKeyExists( targetObject, "setBeanFactory" ) ) { targetObject.setBeanFactory( this ); } - if( structKeyExists( targetObject, "setInjector" ) ){ + if ( structKeyExists( targetObject, "setInjector" ) ) { targetObject.setInjector( this ); } // ColdBox Context Awareness - if( structKeyExists( targetObject, "setColdBox" ) ){ + if ( structKeyExists( targetObject, "setColdBox" ) ) { targetObject.setColdBox( getColdBox() ); } // DIProperty injection - processInjection( targetObject, thisMap.getDIProperties(), arguments.targetID ); + processInjection( + targetObject, + thisMap.getDIProperties(), + arguments.targetID + ); // DISetter injection - processInjection( targetObject, thisMap.getDISetters(), arguments.targetID ); + processInjection( + targetObject, + thisMap.getDISetters(), + arguments.targetID + ); // Process Provider Methods processProviderMethods( targetObject, thisMap ); // Process Mixins @@ -710,8 +754,11 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I variables.eventManager.announce( "afterInstanceAutowire", iData ); // Debug Data - if( variables.log.canDebug() ){ - variables.log.debug( "Finalized Autowire for: #arguments.targetID#", thisMap.getMemento().toString() ); + if ( variables.log.canDebug() ) { + variables.log.debug( + "Finalized Autowire for: #arguments.targetID#", + thisMap.getMemento().toString() + ); } } } @@ -734,7 +781,7 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I * * @doc_generic coldbox.system.ioc.Injector */ - function getParent() { + function getParent(){ return variables.parent; } @@ -743,7 +790,7 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I * * @doc_generic coldbox.system.core.dynamic.BeanPopulator */ - function getObjectPopulator() { + function getObjectPopulator(){ return new coldbox.system.core.dynamic.BeanPopulator(); } @@ -752,7 +799,7 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I * * @doc_generic boolean */ - boolean function isColdBoxLinked() { + boolean function isColdBoxLinked(){ return isObject( variables.coldbox ); } @@ -761,21 +808,21 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I * * @doc_generic boolean */ - boolean function isCacheBoxLinked() { + boolean function isCacheBoxLinked(){ return isObject( variables.cacheBox ); } /** * Remove the Injector from scope registration if enabled, else does nothing */ - Injector function removeFromScope() { + Injector function removeFromScope(){ var scopeInfo = variables.binder.getScopeRegistration(); // if enabled remove. - if( scopeInfo.enabled ){ + if ( scopeInfo.enabled ) { variables.scopeStorage.delete( scopeInfo.key, scopeInfo.scope ); // Log info - if( variables.log.canDebug() ){ + if ( variables.log.canDebug() ) { variables.log.debug( "Injector removed from scope: #scopeInfo.toString()#" ); } } @@ -793,7 +840,7 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I /** * Clear the singleton cache */ - Injector function clearSingletons() { + Injector function clearSingletons(){ variables.scopes[ "SINGLETON" ].clear(); return this; } @@ -803,11 +850,11 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I * * @doc_generic coldbox.system.ioc.Injector */ - function locateScopedSelf() { + function locateScopedSelf(){ var scopeInfo = variables.binder.getScopeRegistration(); // Return if it exists, else throw exception - if( variables.scopeStorage.exists( scopeInfo.key, scopeInfo.scope ) ){ + if ( variables.scopeStorage.exists( scopeInfo.key, scopeInfo.scope ) ) { return variables.scopeStorage.get( scopeInfo.key, scopeInfo.scope ); } @@ -823,7 +870,7 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I * * @return coldbox.system.core.util.Util */ - function getUtil() { + function getUtil(){ return variables.utility; } @@ -844,7 +891,7 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I */ private Injector function processMixins( required targetObject, required mapping ){ // If no length, kick out - if( !arguments.mapping.getMixins().len() ){ + if ( !arguments.mapping.getMixins().len() ) { return this; } @@ -852,10 +899,10 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I var mixin = new coldbox.system.ioc.config.Mixin().$init( arguments.mapping.getMixins() ); // iterate and mixin baby! - for( var key in mixin ){ - if( key NEQ "$init" ){ + for ( var key in mixin ) { + if ( key NEQ "$init" ) { // add the provided method to the providers structure. - arguments.targetObject.injectMixin( name=key, UDF=mixin[ key ] ); + arguments.targetObject.injectMixin( name = key, UDF = mixin[ key ] ); } } @@ -870,18 +917,18 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I */ private Injector function processProviderMethods( required targetObject, required mapping ){ var providerMethods = arguments.mapping.getProviderMethods(); - var providerLen = arrayLen( providerMethods ); - var x = 1; + var providerLen = arrayLen( providerMethods ); + var x = 1; // Decorate the target if provider methods found, in preparation for replacements - if( providerLen ){ - arguments.targetObject.$wbScopeInfo = getScopeRegistration(); - arguments.targetObject.$wbScopeStorage = variables.scopeStorage; - arguments.targetObject.$wbProviders = {}; + if ( providerLen ) { + arguments.targetObject.$wbScopeInfo = getScopeRegistration(); + arguments.targetObject.$wbScopeStorage = variables.scopeStorage; + arguments.targetObject.$wbProviders = {}; } // iterate and provide baby! - for( var x=1; x lte providerLen; x++ ){ + for ( var x = 1; x lte providerLen; x++ ) { // add the provided method to the providers structure. arguments.targetObject.$wbProviders[ providerMethods[ x ].method ] = providerMethods[ x ].mapping; // Override the function by injecting it, this does private/public functions @@ -896,20 +943,20 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I * @targetObject The target object to do some goodness on * @DICompleteMethods The array of DI completion methods to call */ - private Injector function processAfterCompleteDI(required targetObject, required DICompleteMethods) { + private Injector function processAfterCompleteDI( required targetObject, required DICompleteMethods ){ var DILen = arrayLen( arguments.DICompleteMethods ); // Check for convention first - if ( StructKeyExists( arguments.targetObject, "onDIComplete" ) ) { + if ( structKeyExists( arguments.targetObject, "onDIComplete" ) ) { // Call our mixin invoker - arguments.targetObject.invokerMixin( method="onDIComplete" ); + arguments.targetObject.invokerMixin( method = "onDIComplete" ); } // Iterate on DICompleteMethods - for( var thisMethod in arguments.DICompleteMethods ) { - if ( StructKeyExists( arguments.targetObject, thisMethod ) ) { + for ( var thisMethod in arguments.DICompleteMethods ) { + if ( structKeyExists( arguments.targetObject, thisMethod ) ) { // Call our mixin invoker - arguments.targetObject.invokerMixin( method=thisMethod ); + arguments.targetObject.invokerMixin( method = thisMethod ); } } @@ -923,19 +970,23 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I * @DIData The DI data to use * @targetID The target ID to process injections */ - private Injector function processInjection( required targetObject, required DIData, required targetID ){ + private Injector function processInjection( + required targetObject, + required DIData, + required targetID + ){ var DILen = arrayLen( arguments.DIData ); - for( var x=1; x lte DILen; x++ ){ + for ( var x = 1; x lte DILen; x++ ) { var thisDIData = arguments.DIData[ x ]; // Init the lookup structure - var refLocal = {}; + var refLocal = {}; // Check if direct value has been placed. - if( !isNull( thisDIData.value ) ){ + if ( !isNull( thisDIData.value ) ) { refLocal.dependency = thisDIData.value; } // else check if dsl is used? - else if( !isNull( thisDIData.dsl ) ){ + else if ( !isNull( thisDIData.dsl ) ) { // Get DSL dependency by sending entire DI structure to retrieve refLocal.dependency = variables.builder.buildDSLDependency( definition = thisDIData, @@ -944,15 +995,15 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I ); } // else we have to have a reference ID or a nasty bug has occurred - else{ + else { refLocal.dependency = getInstance( arguments.DIData[ x ].ref ); } // Check if dependency located, else log it and skip - if( structKeyExists( refLocal, "dependency" ) ){ + if ( structKeyExists( refLocal, "dependency" ) ) { // scope or setter determination refLocal.scope = ""; - if( structKeyExists( arguments.DIData[ x ], "scope" ) ){ + if ( structKeyExists( arguments.DIData[ x ], "scope" ) ) { refLocal.scope = arguments.DIData[ x ].scope; } // Inject dependency @@ -965,14 +1016,18 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I ); // some debugging goodness - if( variables.log.canDebug() ){ - variables.log.debug( "Dependency: #arguments.DIData[ x ].toString()# --> injected into #arguments.targetID#" ); + if ( variables.log.canDebug() ) { + variables.log.debug( + "Dependency: #arguments.DIData[ x ].toString()# --> injected into #arguments.targetID#" + ); } + } else if ( variables.log.canDebug() ) { + variables.log.debug( + "Dependency: #arguments.DIData[ x ].toString()# Not Found when wiring #arguments.targetID#. Registered mappings are: #structKeyList( variables.binder.getMappings() )#" + ); } - else if( variables.log.canDebug() ){ - variables.log.debug( "Dependency: #arguments.DIData[ x ].toString()# Not Found when wiring #arguments.targetID#. Registered mappings are: #structKeyList(variables.binder.getMappings())#" ); - } - } // end iteration + } + // end iteration return this; } @@ -993,12 +1048,12 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I required scope, required argName ){ - var argCollection = {}; + var argCollection = {}; argCollection[ arguments.argName ] = arguments.propertyObject; // Property or Setter - if ( len( arguments.scope ) == 0 ){ + if ( len( arguments.scope ) == 0 ) { // Call our mixin invoker: setterMethod - arguments.target.invokerMixin( method="set#arguments.propertyName#", argCollection=argCollection ); + arguments.target.invokerMixin( method = "set#arguments.propertyName#", argCollection = argCollection ); } else { // Call our property injector mixin arguments.target.injectPropertyMixin( @@ -1014,33 +1069,35 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I /** * Register all internal and configured WireBox Scopes */ - private Injector function registerScopes() { - var customScopes = variables.binder.getCustomScopes(); + private Injector function registerScopes(){ + var customScopes = variables.binder.getCustomScopes(); // register no_scope - variables.scopes[ "NOSCOPE" ] = new coldbox.system.ioc.scopes.NoScope( this ); + variables.scopes[ "NOSCOPE" ] = new coldbox.system.ioc.scopes.NoScope( this ); // register singleton - variables.scopes[ "SINGLETON" ] = new coldbox.system.ioc.scopes.Singleton( this ); + variables.scopes[ "SINGLETON" ] = new coldbox.system.ioc.scopes.Singleton( this ); // is cachebox linked? - if( isCacheBoxLinked() ){ + if ( isCacheBoxLinked() ) { variables.scopes[ "CACHEBOX" ] = new coldbox.system.ioc.scopes.CacheBox( this ); } // CF Scopes and references - variables.scopes[ "REQUEST" ] = new coldbox.system.ioc.scopes.RequestScope( this ); - variables.scopes[ "SESSION" ] = new coldbox.system.ioc.scopes.CFScopes( this ); - variables.scopes[ "SERVER" ] = variables.scopes[ "SESSION" ]; - variables.scopes[ "APPLICATION" ] = variables.scopes[ "SESSION" ]; + variables.scopes[ "REQUEST" ] = new coldbox.system.ioc.scopes.RequestScope( this ); + variables.scopes[ "SESSION" ] = new coldbox.system.ioc.scopes.CFScopes( this ); + variables.scopes[ "SERVER" ] = variables.scopes[ "SESSION" ]; + variables.scopes[ "APPLICATION" ] = variables.scopes[ "SESSION" ]; // Debugging - if( variables.log.canDebug() ){ - variables.log.debug( "Registered all internal lifecycle scopes successfully: #structKeyList( variables.scopes )#" ); + if ( variables.log.canDebug() ) { + variables.log.debug( + "Registered all internal lifecycle scopes successfully: #structKeyList( variables.scopes )#" + ); } // Register Custom Scopes - for( var key in customScopes ){ + for ( var key in customScopes ) { variables.scopes[ key ] = createObject( "component", customScopes[ key ] ).init( this ); // Debugging - if( variables.log.canDebug() ){ + if ( variables.log.canDebug() ) { variables.log.debug( "Registered custom scope: #key# (#customScopes[ key ]#)" ); } } @@ -1051,12 +1108,12 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I /** * Register all the configured listeners in the configuration file */ - private Injector function registerListeners() { - var listeners = variables.binder.getListeners(); - var regLen = arrayLen( listeners ); + private Injector function registerListeners(){ + var listeners = variables.binder.getListeners(); + var regLen = arrayLen( listeners ); // iterate and register listeners - for( var x = 1; x lte regLen; x++ ){ + for ( var x = 1; x lte regLen; x++ ) { registerListener( listeners[ x ] ); } return this; @@ -1068,12 +1125,12 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I * @listener The listener to register */ public Injector function registerListener( required listener ){ - try{ + try { // create it var thisListener = createObject( "component", listener.class ); // configure it thisListener.configure( this, listener.properties ); - } catch( Any e ) { + } catch ( Any e ) { variables.log.error( "Error creating listener: #listener.toString()#", e ); throw( message = "Error creating listener: #listener.toString()#", @@ -1083,14 +1140,17 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I } // Now register listener - if( NOT isColdBoxLinked() ){ + if ( NOT isColdBoxLinked() ) { variables.eventManager.register( thisListener, listener.name ); } else { - variables.eventManager.registerInterceptor( interceptorObject=thisListener, interceptorName=listener.name ); + variables.eventManager.registerInterceptor( + interceptorObject = thisListener, + interceptorName = listener.name + ); } // debugging - if( variables.log.canDebug() ){ + if ( variables.log.canDebug() ) { variables.log.debug( "Injector has just registered a new listener: #listener.toString()#" ); } @@ -1100,14 +1160,14 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I /** * Register this injector on a user specified scope */ - private Injector function doScopeRegistration() { + private Injector function doScopeRegistration(){ var scopeInfo = variables.binder.getScopeRegistration(); // register injector with scope variables.scopeStorage.put( scopeInfo.key, this, scopeInfo.scope ); // Log info - if( variables.log.canDebug() ){ + if ( variables.log.canDebug() ) { variables.log.debug( "Scope Registration enabled and Injector scoped to: #scopeInfo.toString()#" ); } @@ -1121,27 +1181,29 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I * @config.doc_generic struct */ private Injector function configureCacheBox( required struct config ){ - var args = {}; + var args = {}; // is cachebox enabled? - if( NOT arguments.config.enabled ){ + if ( NOT arguments.config.enabled ) { return this; } // Do we have a cacheBox reference? - if( isObject( arguments.config.cacheFactory ) ){ + if ( isObject( arguments.config.cacheFactory ) ) { variables.cacheBox = arguments.config.cacheFactory; // debugging - if( variables.log.canDebug() ){ - variables.log.debug( "Configured Injector #getInjectorID()# with direct CacheBox instance: #variables.cacheBox.getFactoryID()#" ); + if ( variables.log.canDebug() ) { + variables.log.debug( + "Configured Injector #getInjectorID()# with direct CacheBox instance: #variables.cacheBox.getFactoryID()#" + ); } return this; } // Do we have a configuration file? - if( len( arguments.config.configFile ) ){ + if ( len( arguments.config.configFile ) ) { // xml? - if( listFindNoCase( "xml,cfm", listLast( arguments.config.configFile, "." ) ) ){ + if ( listFindNoCase( "xml,cfm", listLast( arguments.config.configFile, "." ) ) ) { args[ "XMLConfig" ] = arguments.config.configFile; } else { // cfc @@ -1149,11 +1211,18 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I } // Create CacheBox - var oConfig = createObject( "component", "#arguments.config.classNamespace#.config.CacheBoxConfig" ).init( argumentCollection=args ); - variables.cacheBox = createObject( "component", "#arguments.config.classNamespace#.CacheFactory" ).init( config=oConfig, wirebox=this ); + var oConfig = createObject( "component", "#arguments.config.classNamespace#.config.CacheBoxConfig" ).init( + argumentCollection = args + ); + variables.cacheBox = createObject( "component", "#arguments.config.classNamespace#.CacheFactory" ).init( + config = oConfig, + wirebox = this + ); // debugging - if( variables.log.canDebug() ){ - variables.log.debug( "Configured Injector #getInjectorID()# with CacheBox instance: #variables.cacheBox.getFactoryID()# and configuration file: #arguments.config.configFile#" ); + if ( variables.log.canDebug() ) { + variables.log.debug( + "Configured Injector #getInjectorID()# with CacheBox instance: #variables.cacheBox.getFactoryID()# and configuration file: #arguments.config.configFile#" + ); } return this; } @@ -1161,8 +1230,10 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I // No config file, plain vanilla cachebox variables.cacheBox = createObject( "component", "#arguments.config.classNamespace#.CacheFactory" ).init(); // debugging - if( variables.log.canDebug() ){ - variables.log.debug( "Configured Injector #getInjectorID()# with vanilla CacheBox instance: #variables.cacheBox.getFactoryID()#" ); + if ( variables.log.canDebug() ) { + variables.log.debug( + "Configured Injector #getInjectorID()# with vanilla CacheBox instance: #variables.cacheBox.getFactoryID()#" + ); } return this; @@ -1172,22 +1243,22 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I * Configure a standalone version of logBox for logging */ private Injector function configureLogBox( required configPath ){ - var args = structnew(); + var args = structNew(); // xml? - if( listFindNoCase( "xml,cfm", listLast( arguments.configPath, "." ) ) ){ + if ( listFindNoCase( "xml,cfm", listLast( arguments.configPath, "." ) ) ) { args[ "XMLConfig" ] = arguments.configPath; } else { // cfc args[ "CFCConfigPath" ] = arguments.configPath; } - var config = new coldbox.system.logging.config.LogBoxConfig( argumentCollection=args ); + var config = new coldbox.system.logging.config.LogBoxConfig( argumentCollection = args ); // Create LogBox - variables.logBox = new coldbox.system.logging.LogBox( config=config, wirebox=this ); + variables.logBox = new coldbox.system.logging.LogBox( config = config, wirebox = this ); // Configure Logging for this injector - variables.log = variables.logBox.getLogger( this ); + variables.log = variables.logBox.getLogger( this ); return this; } @@ -1195,9 +1266,9 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I /** * Configure a standalone version of a WireBox Event Manager */ - private Injector function configureEventManager() { + private Injector function configureEventManager(){ // Use or create event manager - if( isColdBoxLinked() && isObject( variables.eventManager ) ){ + if ( isColdBoxLinked() && isObject( variables.eventManager ) ) { // Link Interception States variables.eventManager.appendInterceptionPoints( variables.eventStates ); return this; @@ -1217,7 +1288,7 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I */ private any function buildBinder( required binder, required properties ){ // Check if just a plain CFC path and build it - if( isSimpleValue( arguments.binder ) ){ + if ( isSimpleValue( arguments.binder ) ) { arguments.binder = createObject( "component", arguments.binder ); } @@ -1227,7 +1298,7 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I arguments.binder[ "getEnv" ] = variables.utility.getEnv; // Check if data CFC or binder family - if( NOT isInstanceOf( arguments.binder, "coldbox.system.ioc.config.Binder" ) ){ + if ( NOT isInstanceOf( arguments.binder, "coldbox.system.ioc.config.Binder" ) ) { // simple data cfc, create native binder and decorate data CFC var nativeBinder = new coldbox.system.ioc.config.Binder( injector = this, @@ -1236,7 +1307,7 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I ); } else { // else init the binder and configure it - var nativeBinder = arguments.binder.init( injector=this, properties=arguments.properties ); + var nativeBinder = arguments.binder.init( injector = this, properties = arguments.properties ); // Configure it nativeBinder.configure(); // Load it @@ -1246,4 +1317,4 @@ component serializable="false" accessors="true" implements="coldbox.system.ioc.I return nativeBinder; } -} \ No newline at end of file +} diff --git a/system/logging/LogBox.cfc b/system/logging/LogBox.cfc index af39acfc0..06e71e23e 100644 --- a/system/logging/LogBox.cfc +++ b/system/logging/LogBox.cfc @@ -8,48 +8,47 @@ * * By default, LogBox will log any warnings pertaining to itself in the CF logs * according to its name. -*/ -component accessors="true"{ - + */ +component accessors="true" { /** - * The LogBox unique ID - */ + * The LogBox unique ID + */ property name="logBoxID"; /** - * The LogBox operating version - */ + * The LogBox operating version + */ property name="version"; /** - * The appender registration map - */ + * The appender registration map + */ property name="appenderRegistry" type="struct"; /** - * The Logger registration map - */ + * The Logger registration map + */ property name="loggerRegistry" type="struct"; /** - * Category based appenders - */ + * Category based appenders + */ property name="categoryAppenders"; /** - * Configuration object - */ + * Configuration object + */ property name="config"; /** - * ColdBox linkage class - */ + * ColdBox linkage class + */ property name="coldbox"; /** - * WireBox linkage class - */ + * WireBox linkage class + */ property name="wirebox"; /** @@ -60,7 +59,7 @@ component accessors="true"{ /** * The logBox task scheduler executor - * @see coldbox.system.async.tasks.ScheduledExecutor + * @see coldbox.system.async.executors.ScheduledExecutor */ property name="taskScheduler"; @@ -77,16 +76,16 @@ component accessors="true"{ * @return A configured and loaded LogBox instance */ function init( - config="coldbox.system.logging.config.DefaultConfig", - coldbox="", - wirebox="" + config = "coldbox.system.logging.config.DefaultConfig", + coldbox = "", + wirebox = "" ){ // LogBox Unique ID - variables.logboxID = createObject( 'java', 'java.lang.System' ).identityHashCode( this ); + variables.logboxID = createObject( "java", "java.lang.System" ).identityHashCode( this ); // Appenders - variables.appenderRegistry = structnew(); + variables.appenderRegistry = structNew(); // Loggers - variables.loggerRegistry = structnew(); + variables.loggerRegistry = structNew(); // Category Appenders variables.categoryAppenders = ""; // Version @@ -109,16 +108,19 @@ component accessors="true"{ } ); // Register the task scheduler according to operating mode - if( isObject( variables.coldbox ) ){ - variables.wirebox = variables.coldbox.getWireBox(); - variables.asyncManager = variables.coldbox.getAsyncManager(); + if ( isObject( variables.coldbox ) ) { + variables.wirebox = variables.coldbox.getWireBox(); + variables.asyncManager = variables.coldbox.getAsyncManager(); variables.taskScheduler = variables.asyncManager.getExecutor( "coldbox-tasks" ); - } else if( isObject( arguments.wirebox ) ){ - variables.asyncManager = variables.wirebox.getAsyncManager(); + } else if ( isObject( arguments.wirebox ) ) { + variables.asyncManager = variables.wirebox.getAsyncManager(); variables.taskScheduler = variables.wirebox.getTaskScheduler(); } else { - variables.asyncManager = new coldbox.system.async.AsyncManager(); - variables.taskScheduler = variables.asyncManager.newScheduledExecutor( name : "logbox-tasks", threads : 20 ); + variables.asyncManager = new coldbox.system.async.AsyncManager(); + variables.taskScheduler = variables.asyncManager.newScheduledExecutor( + name : "logbox-tasks", + threads: 20 + ); } // Configure LogBox @@ -134,12 +136,11 @@ component accessors="true"{ * @config.doc_generic coldbox.system.logging.config.LogBoxConfig */ function configure( required config ){ - lock name="#variables.logBoxID#.logbox.config" type="exclusive" timeout="30" throwOnTimeout=true{ - + lock name="#variables.logBoxID#.logbox.config" type="exclusive" timeout="30" throwOnTimeout=true { // Do we need to build the config object? - if( isSimpleValue( arguments.config ) ){ + if ( isSimpleValue( arguments.config ) ) { arguments.config = new coldbox.system.logging.config.LogBoxConfig( - CFCConfigPath : arguments.config + CFCConfigPath: arguments.config ); } @@ -147,31 +148,29 @@ component accessors="true"{ variables.config = arguments.config.validate(); // Reset Registries - variables.appenderRegistry = structnew(); - variables.loggerRegistry = structnew(); + variables.appenderRegistry = structNew(); + variables.loggerRegistry = structNew(); - //Get appender definitions + // Get appender definitions var appenders = variables.config.getAllAppenders(); // Register All Appenders configured - for( var key in appenders ){ - registerAppender( argumentCollection=appenders[ key ] ); + for ( var key in appenders ) { + registerAppender( argumentCollection = appenders[ key ] ); } // Get Root def var rootConfig = variables.config.getRoot(); // Create Root Logger - var args = { - category = "ROOT", - levelMin = rootConfig.levelMin, - levelMax = rootConfig.levelMax, - appenders = getAppendersMap( rootConfig.appenders ) + var args = { + category : "ROOT", + levelMin : rootConfig.levelMin, + levelMax : rootConfig.levelMax, + appenders : getAppendersMap( rootConfig.appenders ) }; - //Save in Registry - variables.loggerRegistry = { - "ROOT" = new coldbox.system.logging.Logger( argumentCollection=args ) - }; + // Save in Registry + variables.loggerRegistry = { "ROOT" : new coldbox.system.logging.Logger( argumentCollection = args ) }; } } @@ -179,14 +178,13 @@ component accessors="true"{ * Shutdown the injector gracefully by calling the shutdown events internally. **/ function shutdown(){ - // Check if config has onShutdown convention - if( structKeyExists( variables.config, "onShutdown" ) ){ + if ( structKeyExists( variables.config, "onShutdown" ) ) { variables.config.onShutdown( this ); } // Shutdown Executors if not in ColdBox Mode or WireBox mode - if( !isObject( variables.coldbox ) && !isObject( variables.wirebox ) ){ + if ( !isObject( variables.coldbox ) && !isObject( variables.wirebox ) ) { variables.asyncManager.shutdownAllExecutors( force = true ); } @@ -216,7 +214,7 @@ component accessors="true"{ var root = getRootLogger(); // is category object? - if( isObject( arguments.category ) ){ + if ( isObject( arguments.category ) ) { arguments.category = getMetadata( arguments.category ).name; } @@ -224,37 +222,41 @@ component accessors="true"{ arguments.category = trim( arguments.category ); // Is logger by category name created already? - if( structKeyExists( variables.loggerRegistry, arguments.category ) ){ + if ( structKeyExists( variables.loggerRegistry, arguments.category ) ) { return variables.loggerRegistry[ arguments.category ]; } // Do we have a category definition, so we can build it? var args = {}; - if( variables.config.categoryExists( arguments.category ) ){ + if ( variables.config.categoryExists( arguments.category ) ) { var categoryConfig = variables.config.getCategory( arguments.category ); // Setup creation arguments - args = { - category = categoryConfig.name, - levelMin = categoryConfig.levelMin, - levelMax = categoryConfig.levelMax, - appenders = getAppendersMap( categoryConfig.appenders ) + args = { + category : categoryConfig.name, + levelMin : categoryConfig.levelMin, + levelMax : categoryConfig.levelMax, + appenders : getAppendersMap( categoryConfig.appenders ) }; } else { // Do Category Inheritance? or else just return the root logger. root = locateCategoryParentLogger( arguments.category ); // Build it out as per Root logger args = { - category = arguments.category, - levelMin = root.getLevelMin(), - levelMax = root.getLevelMax() + category : arguments.category, + levelMin : root.getLevelMin(), + levelMax : root.getLevelMax() }; } // Create it - lock name="#variables.logboxID#.logBox.logger.#arguments.category#" type="exclusive" throwontimeout="true" timeout="30"{ - if( NOT structKeyExists( variables.loggerRegistry, arguments.category ) ){ + lock + name ="#variables.logboxID#.logBox.logger.#arguments.category#" + type ="exclusive" + throwontimeout="true" + timeout ="30" { + if ( NOT structKeyExists( variables.loggerRegistry, arguments.category ) ) { // Create logger - var oLogger = new coldbox.system.logging.Logger( argumentCollection=args ); + var oLogger = new coldbox.system.logging.Logger( argumentCollection = args ); // Inject Root Logger oLogger.setRootLogger( root ); // Store it @@ -292,30 +294,29 @@ component accessors="true"{ LogBox function registerAppender( required name, required class, - struct properties={}, - layout="", - numeric levelMin=0, - numeric levelMax=4 + struct properties = {}, + layout = "", + numeric levelMin = 0, + numeric levelMax = 4 ){ - - if( !structKeyExists( variables.appenderRegistry, arguments.name ) ){ - - lock name="#variables.logboxID#.registerappender.#name#" type="exclusive" timeout="15" throwOnTimeout="true"{ - - if( !structKeyExists( variables.appenderRegistry, arguments.name ) ){ - + if ( !structKeyExists( variables.appenderRegistry, arguments.name ) ) { + lock + name ="#variables.logboxID#.registerappender.#name#" + type ="exclusive" + timeout ="15" + throwOnTimeout="true" { + if ( !structKeyExists( variables.appenderRegistry, arguments.name ) ) { // Create it and store it - variables.appenderRegistry[ arguments.name ] = new "#getLoggerClass( arguments.class )#"( argumentCollection=arguments ) - .setLogBox( this ) + variables.appenderRegistry[ arguments.name ] = new "#getLoggerClass( arguments.class )#"( + argumentCollection = arguments + ).setLogBox( this ) .setColdBox( variables.coldbox ) .setWireBox( variables.wirebox ) .onRegistration() .setInitialized( true ); - } - - } // end lock - + } + // end lock } return this; @@ -335,7 +336,7 @@ component accessors="true"{ */ private function getLoggerClass( required class ){ // is this a local class? - if( arrayFindNoCase( variables.systemAppenders, arguments.class ) ){ + if ( arrayFindNoCase( variables.systemAppenders, arguments.class ) ) { return "coldbox.system.logging.appenders.#arguments.class#"; } @@ -352,21 +353,25 @@ component accessors="true"{ var parentCategory = ""; // category len check - if( len( arguments.category ) ){ - parentCategory = listDeleteAt( arguments.category, listLen( arguments.category, "." ), "." ); + if ( len( arguments.category ) ) { + parentCategory = listDeleteAt( + arguments.category, + listLen( arguments.category, "." ), + "." + ); } // Check if parent Category is empty - if( len( parentCategory ) EQ 0 ){ + if ( len( parentCategory ) EQ 0 ) { // Just return the root logger, nothing found. return getRootLogger(); } // Does it exist already in the instantiated loggers? - if( structKeyExists( variables.loggerRegistry, parentCategory ) ){ + if ( structKeyExists( variables.loggerRegistry, parentCategory ) ) { return variables.loggerRegistry[ parentCategory ]; } // Do we need to create it, lazy loading? - if( variables.config.categoryExists( parentCategory ) ){ + if ( variables.config.categoryExists( parentCategory ) ) { return getLogger( parentCategory ); } // Else, it was not located, recurse @@ -383,7 +388,7 @@ component accessors="true"{ .listToArray() .reduce( function( result, item, index ){ var target = {}; - if( !isNull( arguments.result ) ){ + if ( !isNull( arguments.result ) ) { target = result; } target[ item ] = variables.appenderRegistry[ item ]; @@ -400,4 +405,4 @@ component accessors="true"{ return new coldbox.system.core.util.Util(); } -} \ No newline at end of file +} diff --git a/tests/specs/async/util/ExecutorsSpec.cfc b/tests/specs/async/util/ExecutorsSpec.cfc index b9e780b01..af671ff84 100644 --- a/tests/specs/async/util/ExecutorsSpec.cfc +++ b/tests/specs/async/util/ExecutorsSpec.cfc @@ -9,7 +9,7 @@ component extends="tests.specs.async.BaseAsyncSpec" { // all your suites go here. describe( "Executors", function(){ beforeEach( function( currentSpec ){ - executors = new coldbox.system.async.util.Executors(); + executors = new coldbox.system.async.executors.ExecutorBuilder(); } ); it( "can be created", function(){ From ae303fe6ccf843a50ff510f72f633b5a5f60d725 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Mon, 12 Apr 2021 10:44:03 -0500 Subject: [PATCH 07/40] -more documentation -more tests --- system/async/AsyncManager.cfc | 6 ++ system/async/time/ChronoUnit.cfc | 97 ++++++++++++++----- system/async/time/Period.cfc | 4 +- system/async/time/TimeUnit.cfc | 4 +- .../{util => executors}/ExecutorsSpec.cfc | 0 tests/specs/async/time/ChronoUnitSpec.cfc | 76 +++++++++++++++ .../async/{util => time}/DurationSpec.cfc | 0 .../specs/async/{util => time}/PeriodSpec.cfc | 0 .../async/{util => time}/TimeUnitSpec.cfc | 0 9 files changed, 162 insertions(+), 25 deletions(-) rename tests/specs/async/{util => executors}/ExecutorsSpec.cfc (100%) create mode 100644 tests/specs/async/time/ChronoUnitSpec.cfc rename tests/specs/async/{util => time}/DurationSpec.cfc (100%) rename tests/specs/async/{util => time}/PeriodSpec.cfc (100%) rename tests/specs/async/{util => time}/TimeUnitSpec.cfc (100%) diff --git a/system/async/AsyncManager.cfc b/system/async/AsyncManager.cfc index 911122951..c5815ef62 100644 --- a/system/async/AsyncManager.cfc +++ b/system/async/AsyncManager.cfc @@ -17,6 +17,12 @@ */ component accessors="true" singleton { + /** + * -------------------------------------------------------------------------- + * Properties + * -------------------------------------------------------------------------- + */ + /** * A collection of executors you can register in the async manager * so you can run queues, tasks or even scheduled tasks diff --git a/system/async/time/ChronoUnit.cfc b/system/async/time/ChronoUnit.cfc index 58a912c26..b2ae81216 100644 --- a/system/async/time/ChronoUnit.cfc +++ b/system/async/time/ChronoUnit.cfc @@ -1,41 +1,51 @@ +/** + * We represent a chrono unit class that assists with time units on date/time conversions + * It doesn't hold any date/time information. + * + * @see https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/temporal/ChronoUnit.html + * @see https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/ZoneId.html + * @see https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/ZoneOffset.html + */ component singleton { - variables.jChronoUnit = createObject( "java", "java.time.temporal.ChronoUnit" ); - variables.ZoneOffset = createObject( "java", "java.time.ZoneOffset" ); - variables.ZoneId = createObject( "java", "java.time.ZoneId" ); + // TimeZone Helpers + this.ZoneOffset = createObject( "java", "java.time.ZoneOffset" ); + this.ZoneId = createObject( "java", "java.time.ZoneId" ); + // The java chrono unit we model + variables.jChronoUnit = createObject( "java", "java.time.temporal.ChronoUnit" ); // Unit that represents the concept of a century. - this.CENTURIES = variables.jChronoUnit.CENTURIES; + this.CENTURIES = variables.jChronoUnit.CENTURIES; // Unit that represents the concept of a day. - this.DAYS = variables.jChronoUnit.DAYS; + this.DAYS = variables.jChronoUnit.DAYS; // Unit that represents the concept of a decade. - this.DECADES = variables.jChronoUnit.DECADES; + this.DECADES = variables.jChronoUnit.DECADES; // Unit that represents the concept of an era. - this.ERAS = variables.jChronoUnit.ERAS; + this.ERAS = variables.jChronoUnit.ERAS; // Artificial unit that represents the concept of forever. - this.FOREVER = variables.jChronoUnit.FOREVER; + this.FOREVER = variables.jChronoUnit.FOREVER; // Unit that represents the concept of half a day, as used in AM/PM. - this.HALF_DAYS = variables.jChronoUnit.HALF_DAYS; + this.HALF_DAYS = variables.jChronoUnit.HALF_DAYS; // Unit that represents the concept of an hour. - this.HOURS = variables.jChronoUnit.HOURS; + this.HOURS = variables.jChronoUnit.HOURS; // Unit that represents the concept of a microsecond. - this.MICROS = variables.jChronoUnit.MICROS; + this.MICROS = variables.jChronoUnit.MICROS; // Unit that represents the concept of a millennium. - this.MILLENNIA = variables.jChronoUnit.MILLENNIA; + this.MILLENNIA = variables.jChronoUnit.MILLENNIA; // Unit that represents the concept of a millisecond. - this.MILLIS = variables.jChronoUnit.MILLIS; + this.MILLIS = variables.jChronoUnit.MILLIS; // Unit that represents the concept of a minute. - this.MINUTES = variables.jChronoUnit.MINUTES; + this.MINUTES = variables.jChronoUnit.MINUTES; // Unit that represents the concept of a month. - this.MONTHS = variables.jChronoUnit.MONTHS; + this.MONTHS = variables.jChronoUnit.MONTHS; // Unit that represents the concept of a nanosecond, the smallest supported unit of time. - this.NANOS = variables.jChronoUnit.NANOS; + this.NANOS = variables.jChronoUnit.NANOS; // Unit that represents the concept of a second. - this.SECONDS = variables.jChronoUnit.SECONDS; + this.SECONDS = variables.jChronoUnit.SECONDS; // Unit that represents the concept of a week. - this.WEEKS = variables.jChronoUnit.WEEKS; + this.WEEKS = variables.jChronoUnit.WEEKS; // Unit that represents the concept of a year. - this.YEARS = variables.jChronoUnit.YEARS; + this.YEARS = variables.jChronoUnit.YEARS; /** * Convert any ColdFusion date/time or string date/time object to a Java instant temporal object @@ -52,6 +62,24 @@ component singleton { return arguments.target.toInstant(); } + /** + * Convert any ColdFUsion date/time or string date/time object to the new Java.time.LocalDateTime class so we can use them as Temporal objects + * + * @target The cf date/time or string object representing the date/time + * @timezone If passed, we will use this timezone to build the temporal object. Else we default to UTC + * + * @throws DateTimeException - if the zone ID has an invalid format + * @throws ZoneRulesException - if the zone ID is a region ID that cannot be found + * + * @return A Java temporal object as java.time.LocalDateTime + */ + function toLocalDateTime( required target, timezone ){ + return this + .toInstant( arguments.target ) + .atZone( isNull( arguments.timezone ) ? this.ZoneOffset.UTC : this.ZoneId.of( arguments.timezone ) ) + .toLocalDateTime(); + } + /** * Convert any ColdFUsion date/time or string date/time object to the new Java.time.LocalDate class so we can use them as Temporal objects * @@ -66,12 +94,37 @@ component singleton { function toLocalDate( required target, timezone ){ return this .toInstant( arguments.target ) - .atZone( - isNull( arguments.timezone ) ? variables.ZoneOffset.UTC : variables.ZoneId.of( arguments.timezone ) - ) + .atZone( isNull( arguments.timezone ) ? this.ZoneOffset.UTC : this.ZoneId.of( arguments.timezone ) ) .toLocalDate(); } + /** + * Get the Java Zone ID of the passed in timezone identifier string + * + * @see https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/ZoneId.html + * + * @timezone The String timezone identifier + * + * @throws DateTimeException - if the zone ID has an invalid format + * @throws ZoneRulesException - if the zone ID is a region ID that cannot be found + * + * @return Java Timezone java.time.ZoneId + */ + function getTimezone( required timezone ){ + return this.ZoneId.of( arguments.timezone ); + } + + /** + * This queries TimeZone.getDefault() to find the default time-zone and converts it to a ZoneId. If the system default time-zone is changed, then the result of this method will also change. + * + * @see https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/ZoneId.html + * + * @return Java Timezone java.time.ZoneId + */ + function getSystemTimezone(){ + return this.ZoneId.systemDefault(); + } + /** * Convert any date/time or string date/time object to a Java Date/Time * diff --git a/system/async/time/Period.cfc b/system/async/time/Period.cfc index 290d3888a..d4a6d2cce 100644 --- a/system/async/time/Period.cfc +++ b/system/async/time/Period.cfc @@ -170,7 +170,7 @@ component accessors="true" { * @return The date/time object with the period added to it or a java LocalDate */ function addTo( required target, boolean asLocalDate = false ){ - var results = variables.jPeriod.addTo( this.CHRONO_UNIT.toLocalDate( arguments.target ) ); + var results = variables.jPeriod.addTo( this.CHRONO_UNIT.toLocalDateTime( arguments.target ) ); return ( arguments.asLocalDate ? results : results.toString() ); } @@ -183,7 +183,7 @@ component accessors="true" { * @return Return the result either as a date/time string or a java.time.LocalDate object */ function subtractFrom( required target, boolean asLocalDate = false ){ - var results = variables.jPeriod.subtractFrom( this.CHRONO_UNIT.toLocalDate( arguments.target ) ); + var results = variables.jPeriod.subtractFrom( this.CHRONO_UNIT.toLocalDateTime( arguments.target ) ); return ( arguments.asLocalDate ? results : results.toString() ); } diff --git a/system/async/time/TimeUnit.cfc b/system/async/time/TimeUnit.cfc index 7b5fd9432..1a9ff1ff7 100644 --- a/system/async/time/TimeUnit.cfc +++ b/system/async/time/TimeUnit.cfc @@ -1,5 +1,7 @@ /** - * Static class to map ColdFusion string timeouts to Java Timeouts + * Static class to map ColdFusion strings units to Java units + * A TimeUnit does not maintain time information, + * but only helps organize and use time representations that may be maintained separately across various contexts */ component singleton { diff --git a/tests/specs/async/util/ExecutorsSpec.cfc b/tests/specs/async/executors/ExecutorsSpec.cfc similarity index 100% rename from tests/specs/async/util/ExecutorsSpec.cfc rename to tests/specs/async/executors/ExecutorsSpec.cfc diff --git a/tests/specs/async/time/ChronoUnitSpec.cfc b/tests/specs/async/time/ChronoUnitSpec.cfc new file mode 100644 index 000000000..0aba2dd78 --- /dev/null +++ b/tests/specs/async/time/ChronoUnitSpec.cfc @@ -0,0 +1,76 @@ +/** + * Duration Specs + */ +component extends="tests.specs.async.BaseAsyncSpec" { + + /*********************************** BDD SUITES ***********************************/ + + function run( testResults, testBox ){ + // all your suites go here. + describe( "Chrono Units", function(){ + beforeEach( function( currentSpec ){ + chronounit = new coldbox.system.async.time.ChronoUnit(); + } ); + + it( "can be created", function(){ + expect( chronounit ).toBeComponent(); + } ); + + it( "can convert cf dates to java instants", function(){ + var instant = chronounit.toInstant( now() ); + expect( instant.getEpochSecond() ).toBeGT( 0 ); + + var instant = chronounit.toInstant( "2021-01-01 12:00:00 pm" ); + expect( instant.getEpochSecond() ).toBeGT( 0 ); + } ); + + it( "can convert cf dates to Java LocalDates", function(){ + var jdate = chronounit.toLocalDate( now() ); + expect( jDate.getYear() ).toBe( year( now() ) ); + var jdate = chronounit.toLocalDate( now(), "America/New_York" ); + expect( jDate.getYear() ).toBe( year( now() ) ); + } ); + + it( "can get timezones", function(){ + var t = chronounit.getTimezone( "America/Chicago" ); + expect( t.getId() ).toInclude( "America/Chicago" ); + } ); + + it( "can get the system timezone id", function(){ + var t = chronounit.getSystemTimezone(); + expect( t.getId() ).notToBeEmpty(); + } ); + + var units = [ + "CENTURIES", + "DAYS", + "DECADES", + "ERAS", + "FOREVER", + "HALF_DAYS", + "HOURS", + "MICROS", + "MILLENNIA", + "MILLIS", + "MINUTES", + "MONTHS", + "NANOS", + "SECONDS", + "WEEKS", + "YEARS" + ]; + + units.each( function( thisUnit ){ + it( + data = { unit : thisUnit }, + title = "can produce the #thisUnit# java chrono unit", + body = function( data ){ + var unit = chronoUnit[ data.unit ]; + expect( unit.toString() ).toInclude( data.unit.replace( "_", "" ) ); + } + ); + } ); + } ); + } + +} diff --git a/tests/specs/async/util/DurationSpec.cfc b/tests/specs/async/time/DurationSpec.cfc similarity index 100% rename from tests/specs/async/util/DurationSpec.cfc rename to tests/specs/async/time/DurationSpec.cfc diff --git a/tests/specs/async/util/PeriodSpec.cfc b/tests/specs/async/time/PeriodSpec.cfc similarity index 100% rename from tests/specs/async/util/PeriodSpec.cfc rename to tests/specs/async/time/PeriodSpec.cfc diff --git a/tests/specs/async/util/TimeUnitSpec.cfc b/tests/specs/async/time/TimeUnitSpec.cfc similarity index 100% rename from tests/specs/async/util/TimeUnitSpec.cfc rename to tests/specs/async/time/TimeUnitSpec.cfc From b771ccd3ae595eae8f2ecaaa330cbceaf86f1705 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Mon, 12 Apr 2021 19:01:01 -0500 Subject: [PATCH 08/40] COLDBOX-995 New scheduler object to keep track and metrics of registered tasks COLDBOX-994 New Scheduled Task with life-cycles and metrics COLDBOX-996 newTask() method on scheduled executor to replace nameless newSchedule --- system/async/AsyncManager.cfc | 38 ++- system/async/executors/ScheduledExecutor.cfc | 23 ++ system/async/tasks/ScheduledTask.cfc | 315 ++++++++++++++++-- system/async/tasks/Scheduler.cfc | 288 ++++++++++++++++ system/async/time/Period.cfc | 16 +- system/web/services/SchedulerService.cfc | 91 +++++ tests/Application.cfc | 12 +- .../async/tasks/ScheduledExecutorSpec.cfc | 4 +- tests/specs/async/tasks/SchedulerSpec.cfc | 133 ++++++++ 9 files changed, 879 insertions(+), 41 deletions(-) create mode 100644 system/async/tasks/Scheduler.cfc create mode 100644 system/web/services/SchedulerService.cfc create mode 100644 tests/specs/async/tasks/SchedulerSpec.cfc diff --git a/system/async/AsyncManager.cfc b/system/async/AsyncManager.cfc index c5815ef62..6485c641d 100644 --- a/system/async/AsyncManager.cfc +++ b/system/async/AsyncManager.cfc @@ -29,6 +29,11 @@ component accessors="true" singleton { */ property name="executors" type="struct"; + /** + * This scheduler can be linked to a ColdBox context + */ + property name="coldbox"; + // Static class to Executors: java.util.concurrent.Executors this.$executors = new coldbox.system.async.executors.ExecutorBuilder(); @@ -38,7 +43,10 @@ component accessors="true" singleton { * @debug Add debugging logs to System out, disabled by default */ AsyncManager function init( boolean debug = false ){ - variables.debug = arguments.debug; + variables.System = createObject( "java", "java.lang.System" ); + variables.debug = arguments.debug; + + // Build out our executors map variables.executors = {}; return this; @@ -346,6 +354,16 @@ component accessors="true" singleton { * Utilities * ****************************************************************/ + /** + * Build out a scheduler for usage within this async manager context and return it to you. + * You must manage it's persistence, we only wire it and create it for you. + * + * @name The unique name for the scheduler + */ + Scheduler function newScheduler( required name ){ + return new coldbox.system.async.tasks.Scheduler( arguments.name, this ); + } + /** * Build out a new Duration class */ @@ -391,4 +409,22 @@ component accessors="true" singleton { .toArray(); } + /** + * Utility to send to output to the output stream + * + * @var Variable/Message to send + */ + function out( required var ){ + variables.System.out.println( arguments.var.toString() ); + } + + /** + * Utility to send to output to the error stream + * + * @var Variable/Message to send + */ + function err( required var ){ + variables.System.err.println( arguments.var.toString() ); + } + } diff --git a/system/async/executors/ScheduledExecutor.cfc b/system/async/executors/ScheduledExecutor.cfc index 00d43ab7e..ba82a12b5 100644 --- a/system/async/executors/ScheduledExecutor.cfc +++ b/system/async/executors/ScheduledExecutor.cfc @@ -148,7 +148,30 @@ component extends="Executor" accessors="true" singleton { * Builder Methods * ****************************************************************/ + /** + * Build out a new scheduled task + * + * @deprecated DO NOT USE, use newTask() instead + * + * @task The closure or cfc that represents the task + * @method The method on the cfc to call, defaults to "run" (optional) + */ ScheduledTask function newSchedule( required task, method = "run" ){ + return this.newTask( argumentCollection = arguments ); + } + + /** + * Build out a new scheduled task representation. Calling this method does not mean that the task is executed. + * + * @name The name of the task + * @task The closure or cfc that represents the task (optional) + * @method The method on the cfc to call, defaults to "run" (optional) + */ + ScheduledTask function newTask( + name = "task-#getName()#-#createUUID()#", + task, + method = "run" + ){ arguments.executor = this; return new coldbox.system.async.tasks.ScheduledTask( argumentCollection = arguments ); } diff --git a/system/async/tasks/ScheduledTask.cfc b/system/async/tasks/ScheduledTask.cfc index b16b32c32..59b24212f 100644 --- a/system/async/tasks/ScheduledTask.cfc +++ b/system/async/tasks/ScheduledTask.cfc @@ -1,7 +1,13 @@ +/** + * This object represents a scheduled task that will be sent in to an executor for scheduling. + * It has a fluent and human dsl for setting it up and restricting is scheduling. + * + * A task can be represented as either a closure or a cfc with a `run()` or custom runnable method. + */ component accessors="true" { /** - * The delay to use in the schedule execution + * The delay or time to wait before we execute the task in the scheduler */ property name="delay" type="numeric"; @@ -16,66 +22,329 @@ component accessors="true" { property name="spacedDelay" type="numeric"; /** - * The task to execute + * The task closure or CFC to execute */ property name="task"; /** - * The method to execute if any + * The method to execute if the task is a CFC */ property name="method"; + /** + * The human name of this task + */ + property name="name"; + + /** + * A handy boolean that disables the scheduling of this task + */ + property name="disabled" type="boolean"; + + /** + * A closure, that if registered, determines if this task will be sent for scheduling or not + */ + property name="when" type="any"; + + /** + * The timezone this task runs under, by default we use the timezone defined in the schedulers + */ + property name="timezone"; + + /** + * This task can be assigned to a task scheduler or be executed on its own at runtime + */ + property name="scheduler"; + + /** + * The collection of stats for the task: { created, lastRun, nextRun, totalRuns, totalFailures, totalSuccess } + */ + property name="stats" type="struct"; + /** * Constructor * - * @task - * @method + * @name The name of this task + * @executor The executor this task will run under and be linked to + * @task The closure or cfc that represents the task (optional) + * @method The method on the cfc to call, defaults to "run" (optional) */ ScheduledTask function init( - required task, + required name, required executor, - method = "run" + any task = "", + method = "run" ){ - variables.task = arguments.task; - variables.executor = arguments.executor; - variables.method = arguments.method; - + // Link up the executor and name + variables.executor = arguments.executor; + variables.name = arguments.name; + // time unit helper + variables.chronoUnit = new coldbox.system.async.time.ChronoUnit(); // Init Properties + variables.task = arguments.task; + variables.method = arguments.method; + // Default Frequencies variables.period = 0; variables.delay = 0; variables.spacedDelay = 0; variables.timeUnit = "milliseconds"; + // Constraints + variables.disabled = false; + variables.when = ""; + // Probable Scheduler or not + variables.scheduler = ""; + // Prepare execution tracking stats + variables.stats = { + "created" : now(), + "lastRun" : "", + "nextRun" : "", + "totalRuns" : 0, + "totalFailures" : 0, + "totalSuccess" : 0, + "lastExecutionTime" : 0, + "lastResults" : "", + "neverRun" : true + }; + // Life cycle methods + variables.beforeTask = ""; + variables.afterTask = ""; + variables.onTaskSuccess = ""; + variables.onTaskFailure = ""; + + return this; + } + + /** + * -------------------------------------------------------------------------- + * Utility and Operational + * -------------------------------------------------------------------------- + */ + + /** + * Set the timezone for this task using the task identifier else we default to our scheduler + * + * @see https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/ZoneId.html + * + * @timezone The timezone string identifier + */ + ScheduledTask function setTimezone( required timezone ){ + variables.timezone = variables.chronoUnit.ZoneId.of( arguments.timezone ); + return this; + } + + /** + * Has this task been assigned to a scheduler or not? + */ + boolean function hasScheduler(){ + return isObject( variables.scheduler ); + } + /** + * This method is used to register the callable closure or cfc on this scheduled task. + * + * @task The closure or cfc that represents the task + * @method The method on the cfc to call, defaults to "run" (optional) + * + * @return The schedule with the task/method registered on it + */ + ScheduledTask function call( required task, method = "run" ){ + variables.task = arguments.task; + variables.method = arguments.method; return this; } + /** + * -------------------------------------------------------------------------- + * Restrictions + * -------------------------------------------------------------------------- + */ + + /** + * Register a when closure that will be executed before the task is set to be registered. + * If the closure returns true we schedule, else we disable it. + */ + ScheduledTask function when( target ){ + variables.when = arguments.target; + return this; + } + + /** + * Disable the task when scheduled, meaning, don't run this sucker! + */ + ScheduledTask function disable(){ + variables.disabled = true; + return this; + } + + /** + * Verifies if we can schedule this task or not by looking at the following constraints: + * + * - disabled + * - when closure + */ + boolean function isDisabled(){ + // Disabled bit + if ( variables.disabled ) { + return true; + } + + // When Closure that dictates if the task can be scheduled: true => yes, false => no + if ( isClosure( variables.when ) ) { + return !variables.when( this ); + } + + // Not disabled + return false; + } + + /** + * -------------------------------------------------------------------------- + * Startup and Runnable Proxy + * -------------------------------------------------------------------------- + */ + + /** + * This is the runnable proxy method that executes your code by the executors + */ + function run(){ + var sTime = getTickCount(); + + // If disabled, skip run + if ( isDisabled() ) { + return; + } + + // Init now as it is running + variables.stats.neverRun = false; + + try { + // Life-Cycle methods + if ( isClosure( variables.beforeTask ) ) { + variables.beforeTask( this ); + } + + // Target task call proxy + if ( isClosure( variables.task ) || isCustomFunction( variables.task ) ) { + variables.stats.lastResults = variables.task() ?: ""; + } else { + variables.stats.lastResults = invoke( variables.task, variables.method ) ?: ""; + } + + // Life-Cycle methods + if ( isClosure( variables.afterTask ) ) { + variables.afterTask( this, variables.stats.lastResults ); + } + + // store successes + variables.stats.totalSuccess++; + if ( isClosure( variables.onTaskSuccess ) ) { + variables.onTaskSuccess( this, variables.stats.lastResults ); + } + } catch ( any e ) { + // store failures + variables.stats.totalFailures++; + // Life Cycle + if ( isClosure( variables.onTaskFailure ) ) { + variables.onTaskFailure( this, e ); + } + } finally { + // Store finalization stats + variables.stats.lastRun = now(); + variables.stats.totalRuns = variables.stats.totalRuns + 1; + variables.stats.lastExecutionTime = getTickCount() - sTime; + } + } + + /** + * This method registers the task into the executor and sends it for execution and scheduling. + * This will not register the task for execution if the disabled flag or the constraints allow it. + * + * @return A ScheduledFuture from where you can monitor the task, an empty ScheduledFuture if the task was not registered + */ ScheduledFuture function start(){ + // Startup a spaced frequency task if ( variables.spacedDelay > 0 ) { return variables.executor.scheduleWithFixedDelay( - task : variables.task, + task : this, spacedDelay: variables.spacedDelay, delay : variables.delay, timeUnit : variables.timeUnit, - method : variables.method + method : "run" ); - } else if ( variables.period > 0 ) { + } + + // Startup a task with a frequency period + if ( variables.period > 0 ) { return variables.executor.scheduleAtFixedRate( - task : variables.task, + task : this, every : variables.period, delay : variables.delay, timeUnit: variables.timeUnit, - method : variables.method - ); - } else { - return variables.executor.schedule( - task : variables.task, - delay : variables.delay, - timeUnit: variables.timeUnit, - method : variables.method + method : "run" ); } + + // Start off the one-off task + return variables.executor.schedule( + task : this, + delay : variables.delay, + timeUnit: variables.timeUnit, + method : "run" + ); + } + + /** + * -------------------------------------------------------------------------- + * Life - Cycle Methods + * -------------------------------------------------------------------------- + */ + + /** + * Store the closure to execute before the task is executed + * + * @target The closure to execute + */ + ScheduledTask function before( required target ){ + variables.beforeTask = arguments.target; + return this; } + /** + * Store the closure to execute after the task is executed + * + * @target The closure to execute + */ + ScheduledTask function after( required target ){ + variables.afterTask = arguments.target; + return this; + } + + /** + * Store the closure to execute after the task is executed successfully + * + * @target The closure to execute + */ + ScheduledTask function onSuccess( required target ){ + variables.onTaskSuccess = arguments.target; + return this; + } + + /** + * Store the closure to execute after the task is executed successfully + * + * @target The closure to execute + */ + ScheduledTask function onFailure( required target ){ + variables.onTaskFailure = arguments.target; + return this; + } + + /** + * -------------------------------------------------------------------------- + * Frequency Methods + * -------------------------------------------------------------------------- + */ + /** * Set a delay in the running of the task that will be registered with this schedule * diff --git a/system/async/tasks/Scheduler.cfc b/system/async/tasks/Scheduler.cfc new file mode 100644 index 000000000..06244bb40 --- /dev/null +++ b/system/async/tasks/Scheduler.cfc @@ -0,0 +1,288 @@ +/** + * The ColdBox Scheduler is in charge of registering scheduled tasks, starting them, monitoring them and shutting them down if needed. + * + * In a ColdBox context, you might have the global scheduler in charge of the global tasks and also 1 per module as well in HMVC fashion. + * In a ColdBox context, this object will inherit from the ColdBox super type as well dynamically at runtime. + * + */ +component accessors="true" singleton { + + /** + * -------------------------------------------------------------------------- + * Properties + * -------------------------------------------------------------------------- + */ + + /** + * The name of this scheduler + */ + property name="name"; + + /** + * An ordered struct of all the tasks this scheduler manages + */ + property name="tasks" type="struct"; + + /** + * The Scheduled Executor we are bound to + */ + property name="executor"; + + /** + * The default timezone to use for task executions + */ + property name="timezone"; + + /** + * Constructor + * + * @name The name of this scheduler + * @asyncManager The async manager we are linked to + */ + function init( required name, required asyncManager ){ + // Name + variables.name = arguments.name; + // The async manager + variables.asyncManager = arguments.asyncManager; + // The collection of tasks we will run + variables.tasks = structNew( "ordered" ); + // time unit helper + variables.chronoUnit = new coldbox.system.async.time.ChronoUnit(); + // Default TimeZone to UTC for all tasks + variables.timezone = variables.chronoUnit.ZoneOffset.UTC; + // Build out the executor for this scheduler + variables.executor = arguments.asyncManager.newExecutor( + name: arguments.name & "-scheduler", + type: "scheduled" + ); + // Bit that denotes if this scheduler has been started or not + variables.started = false; + + // Send notice + arguments.asyncManager.out( "Scheduler (#arguments.name#) has been registered" ); + + return this; + } + + /** + * Set the timezone for all tasks to use using the timezone string identifier + * + * @see https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/ZoneId.html + * + * @timezone The timezone string identifier + */ + Scheduler function setTimezone( required timezone ){ + variables.timezone = variables.chronoUnit.ZoneId.of( arguments.timezone ); + return this; + } + + /** + * Register a new task in this scheduler that will be executed once the `startup()` is fired or manually + * via the run() method of the task. + * + * @return a ScheduledTask object so you can work on the registration of the task + */ + ScheduledTask function task( required name ){ + // Create task with custom name + var oTask = variables.executor + // Give me the task broda! + .newTask( arguments.name ) + // Set default timezone into the task + .setTimezone( getTimezone().getId() ); + + // Register the task by name + variables.tasks[ arguments.name ] = { + // task name + "name" : arguments.name, + // task object + "task" : oTask, + // task scheduled future object + "future" : "", + // when it registers + "registeredAt" : now(), + // when it's scheduled + "scheduledAt" : "", + // Tracks if the task has been disabled for startup purposes + "disabled" : false, + // If there is an error scheduling the task + "error" : false, + // Any error messages when scheduling + "errorMessage" : "", + // The exception stacktrace if something went wrong scheduling the task + "stacktrace" : "" + }; + + return oTask; + } + + /** + * -------------------------------------------------------------------------- + * Life - Cycle Methods + * -------------------------------------------------------------------------- + */ + + /** + * Startup this scheduler and all of it's scheduled tasks + */ + Scheduler function startup(){ + if ( !variables.started ) { + lock name="scheduler-#getName()#-startup" type="exclusive" timeout="45" throwOnTimeout="true" { + if ( !variables.started ) { + // Iterate over tasks and send them off for execution + variables.tasks.each( function( taskName, taskRecord ){ + // Verify we can start it up the task or not + if ( arguments.taskRecord.task.isDisabled() ) { + arguments.taskRecord.disabled = true; + variables.asyncManager.out( + "Scheduler (#getName()#) skipping task (#arguments.taskRecord.task.getName()#) as it is disabled." + ); + // Continue iteration + return; + } else { + // Log scheduling startup + variables.asyncManager.out( + "Scheduler (#getName()#) scheduling task (#arguments.taskRecord.task.getName()#)..." + ); + } + + // Send it off for scheduling + try { + arguments.taskRecord.future = arguments.taskRecord.task.start(); + arguments.taskRecord.scheduledAt = now(); + variables.asyncManager.out( + "Task (#arguments.taskRecord.task.getName()#) scheduled successfully" + ); + } catch ( any e ) { + variables.asyncManager.err( + "Error scheduling task (#arguments.taskRecord.task.getName()#) => #e.message# #e.detail#" + ); + arguments.taskRecord.error = true; + arguments.taskRecord.errorMessage = e.message & e.detail; + arguments.taskRecord.stackTrace = e.stacktrace; + } + } ); + + // Mark scheduler as started + variables.started = true; + + // callback + this.onStartup(); + + // Log it + variables.asyncManager.out( "Scheduler (#getname()#) has started!" ); + } + // end double if not started + } + // end lock + } + // end if not started + + return this; + } + + /** + * Check if this scheduler has started or not + */ + boolean function hasStarted(){ + return variables.started; + } + + /** + * Shutdown this scheduler by calling the executor to shutdown. + */ + Scheduler function shutdown(){ + // callback + this.onShutdown(); + // shutdown executor + variables.executor.shutdownNow(); + // Mark it + variables.started = false; + // Log it + variables.asyncManager.out( "Scheduler (#getname()#) has been shutdown!" ); + return this; + } + + /** + * Called before the scheduler is going to be shutdown + */ + function onShutdown(){ + } + + /** + * Called after the scheduler has registered all schedules + */ + function onStartup(){ + } + + /** + * -------------------------------------------------------------------------- + * Utility Methods + * -------------------------------------------------------------------------- + */ + + /** + * Builds out a report for all the registered tasks in this scheduler + */ + struct function getTaskStats(){ + // return back a struct of stats for each registered task + return variables.tasks.map( function( key, record ){ + return arguments.record.task.getStats(); + } ); + } + + /** + * Get an array of all the tasks managed by this scheduler + */ + array function getRegisteredTasks(){ + var taskKeys = variables.tasks.keyArray(); + taskKeys.sort( "textnocase" ); + return taskKeys; + } + + /** + * Checks if this scheduler manages a task by name + * + * @name The name of the task to search + */ + boolean function hasTask( required name ){ + return variables.tasks.keyExists( arguments.name ); + } + + /** + * Get's a task record from the collection by name + * + * @name The name of the task + * + * @throws UnregisteredTaskException if no task is found under that name + * + * @return The task record struct: { name, task, future, scheduledAt, registeredAt, error, errorMessage, stacktrace } + */ + struct function getTaskRecord( required name ){ + if ( hasTask( arguments.name ) ) { + return variables.tasks[ arguments.name ]; + } + throw( type: "UnregisteredTaskException", message: "No task found with the name: #arguments.name#" ); + } + + /** + * Unregister a task from this scheduler + * + * @name The name of the task to remove + * + * @throws UnregisteredTaskException if no task is found under that name + */ + Scheduler function removeTask( required name ){ + // Remove from executor if registered + var taskRecord = getTaskRecord( arguments.name ); + + // Check if the task has been registered so we can cancel it + if ( isObject( taskRecord.future ) ) { + taskRecord.future.cancel( mayInterruptIfRunning = true ); + } + + // Delete it + variables.tasks.delete( arguments.name ); + return this; + } + +} diff --git a/system/async/time/Period.cfc b/system/async/time/Period.cfc index d4a6d2cce..8dd7510e4 100644 --- a/system/async/time/Period.cfc +++ b/system/async/time/Period.cfc @@ -165,26 +165,26 @@ component accessors="true" { * Adds this period to the specified temporal object and return back to you a date/time object * * @target The date/time object or string to incorporate the period into - * @asLocalDate If true, we will give you the java.time.LocalDate object, else a ColdFusion date/time string + * @asNative If true, we will give you the java.time.LocalDate object, else a ColdFusion date/time string * * @return The date/time object with the period added to it or a java LocalDate */ - function addTo( required target, boolean asLocalDate = false ){ - var results = variables.jPeriod.addTo( this.CHRONO_UNIT.toLocalDateTime( arguments.target ) ); - return ( arguments.asLocalDate ? results : results.toString() ); + function addTo( required target, boolean asNative = false ){ + var results = variables.jPeriod.addTo( this.CHRONO_UNIT.toLocalDate( arguments.target ) ); + return ( arguments.asNative ? results : results.toString() ); } /** * Subtracts this period to the specified temporal object and return back to you a date/time object or a Java LocalDate object * * @target The date/time object or string to incorporate the period into - * @asLocalDate If true, we will give you the java.time.LocalDate object, else a ColdFusion date/time string + * @asNative If true, we will give you the java.time.LocalDate object, else a ColdFusion date/time string * * @return Return the result either as a date/time string or a java.time.LocalDate object */ - function subtractFrom( required target, boolean asLocalDate = false ){ - var results = variables.jPeriod.subtractFrom( this.CHRONO_UNIT.toLocalDateTime( arguments.target ) ); - return ( arguments.asLocalDate ? results : results.toString() ); + function subtractFrom( required target, boolean asNative = false ){ + var results = variables.jPeriod.subtractFrom( this.CHRONO_UNIT.toLocalDate( arguments.target ) ); + return ( arguments.asNative ? results : results.toString() ); } /** diff --git a/system/web/services/SchedulerService.cfc b/system/web/services/SchedulerService.cfc new file mode 100644 index 000000000..11ed17f93 --- /dev/null +++ b/system/web/services/SchedulerService.cfc @@ -0,0 +1,91 @@ +/** + * This class manages Scheduler cfc's that represent collection of scheduled tasks. + * + * It can also be linked to a ColdBox instance to enhance the schedulers and tasks so they can work within a ColdBox context + */ +component accessors="true" singleton { + + /** + * -------------------------------------------------------------------------- + * Properties + * -------------------------------------------------------------------------- + */ + + /** + * A collection of schedulers this manager manages + */ + property name="schedulers" type="struct"; + + /** + * The async manager link + */ + property name="asyncManager"; + + /** + * Constructor + * + * @asyncManager The async manager we are linked to + */ + function init( required asyncManager ){ + // The async manager + variables.asyncManager = arguments.asyncManager; + // The collection of tasks we will run + variables.schedulers = structNew( "ordered" ); + + return this; + } + + /** + * Register a new scheduler in this manager by name and cfc instantiation path + * + * @name The name of the scheduler + * @path The instantiation path to the cfc that represents the scheduler or empty to use the default core scheduler class + * + * @return The created and registered scheduler Object + */ + Scheduler function registerScheduler( required name, path = "" ){ + // Build it + var oScheduler = ( + variables.asyncManager.hasColdBox() ? buildColdBoxScheduler( argumentCollection = arguments ) : buildSimpleScheduler( + argumentCollection = arguments + ) + ); + // Register it + variables.schedulers[ arguments.name ] = oScheduler; + // Return it + return oScheduler; + } + + private function buildSimpleScheduler( required name, required path ){ + } + + private function buildColdBoxScheduler( required name, required path ){ + } + + /** + * Verify if a scheduler has been registered + * + * @name The name of the scheduler + */ + boolean function hasScheduler( required name ){ + return variables.schedulers.keyExists( arguments.name ); + } + + /** + * Remove a scheduler from this manager + * + * @name The name of the scheduler + * + * @return True if removed, else if not found or not removed + */ + boolean function removeScheduler( required name ){ + if ( hasScheduler( arguments.name ) ) { + variables.schedulers[ arguments.name ].shutdown(); + structDelete( variables.schedulers, arguments.name ); + return true; + } + + return false; + } + +} diff --git a/tests/Application.cfc b/tests/Application.cfc index d65bd916c..9d4649422 100755 --- a/tests/Application.cfc +++ b/tests/Application.cfc @@ -49,15 +49,13 @@ component{ public void function onRequestEnd( required targetPage ) { - thread name="testbox-shutdown" { - if( !isNull( application.cbController ) ){ - application.cbController.getLoaderService().processShutdown(); - } - - structDelete( application, "cbController" ); - structDelete( application, "wirebox" ); + if( !isNull( application.cbController ) ){ + application.cbController.getLoaderService().processShutdown(); } + structDelete( application, "cbController" ); + structDelete( application, "wirebox" ); + } } \ No newline at end of file diff --git a/tests/specs/async/tasks/ScheduledExecutorSpec.cfc b/tests/specs/async/tasks/ScheduledExecutorSpec.cfc index f85b3da35..171374d2c 100644 --- a/tests/specs/async/tasks/ScheduledExecutorSpec.cfc +++ b/tests/specs/async/tasks/ScheduledExecutorSpec.cfc @@ -135,14 +135,14 @@ component extends="tests.specs.async.BaseAsyncSpec" { } ); } ); - story( "Ability to use a builder to schedule tasks", function(){ it( "can use the builder to schedule a one-time task", function(){ var scheduler = asyncManager.newScheduledExecutor( "myExecutor" ); var atomicLong = createObject( "java", "java.util.concurrent.atomic.AtomicLong" ).init( 0 ); var sFuture = scheduler - .newSchedule( function(){ + .newTask( "autoIncrementTask" ) + .call( function(){ var results = atomicLong.incrementAndGet(); toConsole( "running periodic task (#results#) from:#getThreadName()#" ); } ) diff --git a/tests/specs/async/tasks/SchedulerSpec.cfc b/tests/specs/async/tasks/SchedulerSpec.cfc new file mode 100644 index 000000000..be932c163 --- /dev/null +++ b/tests/specs/async/tasks/SchedulerSpec.cfc @@ -0,0 +1,133 @@ +/** + * Duration Specs + */ +component extends="tests.specs.async.BaseAsyncSpec" { + + /*********************************** BDD SUITES ***********************************/ + + function run( testResults, testBox ){ + // all your suites go here. + describe( "Scheduler Spec", function(){ + beforeEach( function( currentSpec ){ + asyncManager = new coldbox.system.async.AsyncManager(); + scheduler = asyncManager.newScheduler( "bdd-test" ); + } ); + + it( "can be created", function(){ + expect( scheduler ).toBeComponent(); + expect( scheduler.getName() ).toBe( "bdd-test" ); + expect( scheduler.getExecutor() ).toBeComponent(); + expect( scheduler.getExecutor().getName() ).toBe( "bdd-test-scheduler" ); + } ); + + it( "can set a new timezone", function(){ + scheduler.setTimezone( "America/New_York" ); + expect( scheduler.getTimezone().toString() ).toBe( "America/New_York" ); + } ); + + it( "can register a new task and get it's record", function(){ + var task = scheduler.task( "bddTest" ) + expect( scheduler.hasTask( "bddTest" ) ).toBeTrue(); + expect( scheduler.getRegisteredTasks() ).toInclude( "bddTest" ); + expect( scheduler.getTaskRecord( "bddTest" ).task.getName() ).toBe( "bddTest" ); + } ); + + it( "can throw an exception on getting a bogus task record", function(){ + expect( function(){ + scheduler.getTaskRecord( "bogus" ); + } ).toThrow(); + } ); + + it( "can remove a task", function(){ + var task = scheduler.task( "bddTest" ) + expect( scheduler.hasTask( "bddTest" ) ).toBeTrue(); + scheduler.removeTask( "bddTest" ); + expect( scheduler.hasTask( "bddTest" ) ).toBeFalse(); + } ); + + it( "can throw an exception on removing a non-existent task", function(){ + expect( function(){ + scheduler.removeTask( "bogus" ); + } ).toThrow(); + } ); + + + it( "can register and run the tasks with life cycle methods", function(){ + var atomicLong = createObject( "java", "java.util.concurrent.atomic.AtomicLong" ).init( 0 ); + + // Register two one-time tasks + scheduler + .task( "test1" ) + .call( function(){ + var results = atomicLong.incrementAndGet(); + toConsole( "Running test1 (#results#) from:#getThreadName()#" ); + return results; + } ) + .before( function( task ){ + toConsole( "I am about to run baby!" ); + } ) + .after( function( task, results ){ + toConsole( "this task is done baby!" ); + } ) + .onFailure( function( task, exception ){ + toConsole( "we blew up: #exception.message#" ); + } ) + .onSuccess( function( task, results ){ + toConsole( "Task has completed with success baby! #results#" ); + } ); + + scheduler + .task( "test2" ) + .call( function(){ + var results = atomicLong.incrementAndGet(); + toConsole( "Running test2 (#results#) from:#getThreadName()#" ); + } ); + scheduler + .task( "test3" ) + .call( function(){ + var results = atomicLong.incrementAndGet(); + toConsole( "Running test3 (#results#) from:#getThreadName()#" ); + } ) + .disable(); + + // Startup the scheduler + try { + scheduler.startup(); + expect( scheduler.hasStarted() ).toBeTrue(); + + var record = scheduler.getTaskRecord( "test1" ); + expect( record.future ).toBeComponent(); + expect( record.scheduledAt ).notToBeEmpty(); + + var record = scheduler.getTaskRecord( "test2" ); + expect( record.future ).toBeComponent(); + expect( record.scheduledAt ).notToBeEmpty(); + + var record = scheduler.getTaskRecord( "test3" ); + expect( record.disabled ).toBeTrue(); + expect( record.future ).toBeEmpty(); + expect( record.scheduledAt ).toBeEmpty(); + + // Wait for them to execute + sleep( 500 ); + var stats = scheduler.getTaskStats(); + + debug( stats ); + + expect( stats.test1.neverRun ).toBeFalse(); + expect( stats.test2.neverRun ).toBeFalse(); + expect( stats.test3.neverRun ).toBeTrue(); + + expect( stats.test1.totalRuns ).toBe( 1 ); + expect( stats.test2.totalRuns ).toBe( 1 ); + expect( stats.test3.totalRuns ).toBe( 0 ); + } finally { + sleep( 1000 ); + scheduler.shutdown(); + expect( scheduler.hasStarted() ).toBeFalse(); + } + } ); + } ); + } + +} From fcd42ff9162f2b6ec88c19230a7441ad281a19e2 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Tue, 13 Apr 2021 08:44:31 -0500 Subject: [PATCH 09/40] COLDBOX-997 #resolve Added out and error stream helpers to Scheduled Tasks for better debugging --- system/async/AsyncManager.cfc | 4 ++-- system/async/tasks/ScheduledTask.cfc | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/system/async/AsyncManager.cfc b/system/async/AsyncManager.cfc index 6485c641d..9f7046d0e 100644 --- a/system/async/AsyncManager.cfc +++ b/system/async/AsyncManager.cfc @@ -414,7 +414,7 @@ component accessors="true" singleton { * * @var Variable/Message to send */ - function out( required var ){ + AsyncManager function out( required var ){ variables.System.out.println( arguments.var.toString() ); } @@ -423,7 +423,7 @@ component accessors="true" singleton { * * @var Variable/Message to send */ - function err( required var ){ + AsyncManager function err( required var ){ variables.System.err.println( arguments.var.toString() ); } diff --git a/system/async/tasks/ScheduledTask.cfc b/system/async/tasks/ScheduledTask.cfc index 59b24212f..8df9b9744 100644 --- a/system/async/tasks/ScheduledTask.cfc +++ b/system/async/tasks/ScheduledTask.cfc @@ -80,6 +80,8 @@ component accessors="true" { variables.name = arguments.name; // time unit helper variables.chronoUnit = new coldbox.system.async.time.ChronoUnit(); + // System Helper + variables.System = createObject( "java", "java.lang.System" ); // Init Properties variables.task = arguments.task; variables.method = arguments.method; @@ -120,6 +122,26 @@ component accessors="true" { * -------------------------------------------------------------------------- */ + /** + * Utility to send to output to the output stream + * + * @var Variable/Message to send + */ + ScheduledTask function out( required var ){ + variables.System.out.println( arguments.var.toString() ); + return this; + } + + /** + * Utility to send to output to the error stream + * + * @var Variable/Message to send + */ + ScheduledTask function err( required var ){ + variables.System.err.println( arguments.var.toString() ); + return this; + } + /** * Set the timezone for this task using the task identifier else we default to our scheduler * From 63bed093f0b1dcaa8eb174943b3600af33f4c42f Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Tue, 13 Apr 2021 08:50:07 -0500 Subject: [PATCH 10/40] streamlining usage of timezone classes for the scheduler --- system/async/AsyncManager.cfc | 5 +++-- system/async/tasks/Scheduler.cfc | 8 +++----- tests/specs/async/tasks/SchedulerSpec.cfc | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/system/async/AsyncManager.cfc b/system/async/AsyncManager.cfc index 9f7046d0e..0c13c9f12 100644 --- a/system/async/AsyncManager.cfc +++ b/system/async/AsyncManager.cfc @@ -355,8 +355,9 @@ component accessors="true" singleton { ****************************************************************/ /** - * Build out a scheduler for usage within this async manager context and return it to you. - * You must manage it's persistence, we only wire it and create it for you. + * Build out a scheduler object for usage within this async manager context and return it to you. + * You must manage it's persistence, we only wire it and create it for you so you can use it + * to schedule tasks. * * @name The unique name for the scheduler */ diff --git a/system/async/tasks/Scheduler.cfc b/system/async/tasks/Scheduler.cfc index 06244bb40..b5679463a 100644 --- a/system/async/tasks/Scheduler.cfc +++ b/system/async/tasks/Scheduler.cfc @@ -1,5 +1,5 @@ /** - * The ColdBox Scheduler is in charge of registering scheduled tasks, starting them, monitoring them and shutting them down if needed. + * The Async Scheduler is in charge of registering scheduled tasks, starting them, monitoring them and shutting them down if needed. * * In a ColdBox context, you might have the global scheduler in charge of the global tasks and also 1 per module as well in HMVC fashion. * In a ColdBox context, this object will inherit from the ColdBox super type as well dynamically at runtime. @@ -46,10 +46,8 @@ component accessors="true" singleton { variables.asyncManager = arguments.asyncManager; // The collection of tasks we will run variables.tasks = structNew( "ordered" ); - // time unit helper - variables.chronoUnit = new coldbox.system.async.time.ChronoUnit(); // Default TimeZone to UTC for all tasks - variables.timezone = variables.chronoUnit.ZoneOffset.UTC; + variables.timezone = createObject( "java", "java.time.ZoneId" ).systemDefault(); // Build out the executor for this scheduler variables.executor = arguments.asyncManager.newExecutor( name: arguments.name & "-scheduler", @@ -72,7 +70,7 @@ component accessors="true" singleton { * @timezone The timezone string identifier */ Scheduler function setTimezone( required timezone ){ - variables.timezone = variables.chronoUnit.ZoneId.of( arguments.timezone ); + variables.timezone = createObject( "java", "java.time.ZoneId" ).of( arguments.timezone ); return this; } diff --git a/tests/specs/async/tasks/SchedulerSpec.cfc b/tests/specs/async/tasks/SchedulerSpec.cfc index be932c163..2e33c27c5 100644 --- a/tests/specs/async/tasks/SchedulerSpec.cfc +++ b/tests/specs/async/tasks/SchedulerSpec.cfc @@ -51,7 +51,6 @@ component extends="tests.specs.async.BaseAsyncSpec" { } ).toThrow(); } ); - it( "can register and run the tasks with life cycle methods", function(){ var atomicLong = createObject( "java", "java.util.concurrent.atomic.AtomicLong" ).init( 0 ); @@ -82,6 +81,7 @@ component extends="tests.specs.async.BaseAsyncSpec" { var results = atomicLong.incrementAndGet(); toConsole( "Running test2 (#results#) from:#getThreadName()#" ); } ); + scheduler .task( "test3" ) .call( function(){ From 21c9e36eaf501f3326a7bb1e94cc8723abe3b1e9 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Tue, 13 Apr 2021 08:57:50 -0500 Subject: [PATCH 11/40] scheduler now finalized and revised with all test specs COLDBOX-995 --- system/async/tasks/Scheduler.cfc | 24 +++++++++++++---------- tests/specs/async/tasks/SchedulerSpec.cfc | 1 + 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/system/async/tasks/Scheduler.cfc b/system/async/tasks/Scheduler.cfc index b5679463a..b713e2c66 100644 --- a/system/async/tasks/Scheduler.cfc +++ b/system/async/tasks/Scheduler.cfc @@ -1,6 +1,9 @@ /** * The Async Scheduler is in charge of registering scheduled tasks, starting them, monitoring them and shutting them down if needed. * + * Each scheduler is bound to an scheduled executor class. You can override the executor using the `setExecutor()` method if you so desire. + * The scheduled executor will be named {name}-scheduler + * * In a ColdBox context, you might have the global scheduler in charge of the global tasks and also 1 per module as well in HMVC fashion. * In a ColdBox context, this object will inherit from the ColdBox super type as well dynamically at runtime. * @@ -55,9 +58,8 @@ component accessors="true" singleton { ); // Bit that denotes if this scheduler has been started or not variables.started = false; - // Send notice - arguments.asyncManager.out( "Scheduler (#arguments.name#) has been registered" ); + arguments.asyncManager.out( "√ Scheduler (#arguments.name#) has been registered" ); return this; } @@ -126,20 +128,20 @@ component accessors="true" singleton { if ( !variables.started ) { lock name="scheduler-#getName()#-startup" type="exclusive" timeout="45" throwOnTimeout="true" { if ( !variables.started ) { - // Iterate over tasks and send them off for execution + // Iterate over tasks and send them off for scheduling variables.tasks.each( function( taskName, taskRecord ){ // Verify we can start it up the task or not if ( arguments.taskRecord.task.isDisabled() ) { arguments.taskRecord.disabled = true; variables.asyncManager.out( - "Scheduler (#getName()#) skipping task (#arguments.taskRecord.task.getName()#) as it is disabled." + "- Scheduler (#getName()#) skipping task (#arguments.taskRecord.task.getName()#) as it is disabled." ); // Continue iteration return; } else { // Log scheduling startup variables.asyncManager.out( - "Scheduler (#getName()#) scheduling task (#arguments.taskRecord.task.getName()#)..." + "- Scheduler (#getName()#) scheduling task (#arguments.taskRecord.task.getName()#)..." ); } @@ -148,11 +150,11 @@ component accessors="true" singleton { arguments.taskRecord.future = arguments.taskRecord.task.start(); arguments.taskRecord.scheduledAt = now(); variables.asyncManager.out( - "Task (#arguments.taskRecord.task.getName()#) scheduled successfully" + "√ Task (#arguments.taskRecord.task.getName()#) scheduled successfully." ); } catch ( any e ) { variables.asyncManager.err( - "Error scheduling task (#arguments.taskRecord.task.getName()#) => #e.message# #e.detail#" + "X Error scheduling task (#arguments.taskRecord.task.getName()#) => #e.message# #e.detail#" ); arguments.taskRecord.error = true; arguments.taskRecord.errorMessage = e.message & e.detail; @@ -167,7 +169,7 @@ component accessors="true" singleton { this.onStartup(); // Log it - variables.asyncManager.out( "Scheduler (#getname()#) has started!" ); + variables.asyncManager.out( "√ Scheduler (#getname()#) has started!" ); } // end double if not started } @@ -186,7 +188,7 @@ component accessors="true" singleton { } /** - * Shutdown this scheduler by calling the executor to shutdown. + * Shutdown this scheduler by calling the executor to shutdown and disabling all tasks */ Scheduler function shutdown(){ // callback @@ -196,18 +198,20 @@ component accessors="true" singleton { // Mark it variables.started = false; // Log it - variables.asyncManager.out( "Scheduler (#getname()#) has been shutdown!" ); + variables.asyncManager.out( "√ Scheduler (#getName()#) has been shutdown!" ); return this; } /** * Called before the scheduler is going to be shutdown + * @abstract */ function onShutdown(){ } /** * Called after the scheduler has registered all schedules + * @abstract */ function onStartup(){ } diff --git a/tests/specs/async/tasks/SchedulerSpec.cfc b/tests/specs/async/tasks/SchedulerSpec.cfc index 2e33c27c5..fe2e796a2 100644 --- a/tests/specs/async/tasks/SchedulerSpec.cfc +++ b/tests/specs/async/tasks/SchedulerSpec.cfc @@ -92,6 +92,7 @@ component extends="tests.specs.async.BaseAsyncSpec" { // Startup the scheduler try { + expect( scheduler.hasStarted() ).toBeFalse(); scheduler.startup(); expect( scheduler.hasStarted() ).toBeTrue(); From d0be3ae15fe57e67b0274520e13c685a34ae2ce5 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Tue, 13 Apr 2021 09:10:43 -0500 Subject: [PATCH 12/40] for some reason ++ operators where not working in threaded mode. --- system/async/AsyncManager.cfc | 2 ++ system/async/tasks/ScheduledTask.cfc | 38 ++++++++++++----------- tests/specs/async/tasks/SchedulerSpec.cfc | 16 +++++----- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/system/async/AsyncManager.cfc b/system/async/AsyncManager.cfc index 0c13c9f12..815f6fd16 100644 --- a/system/async/AsyncManager.cfc +++ b/system/async/AsyncManager.cfc @@ -417,6 +417,7 @@ component accessors="true" singleton { */ AsyncManager function out( required var ){ variables.System.out.println( arguments.var.toString() ); + return this; } /** @@ -426,6 +427,7 @@ component accessors="true" singleton { */ AsyncManager function err( required var ){ variables.System.err.println( arguments.var.toString() ); + return this; } } diff --git a/system/async/tasks/ScheduledTask.cfc b/system/async/tasks/ScheduledTask.cfc index 8df9b9744..85b8b35b4 100644 --- a/system/async/tasks/ScheduledTask.cfc +++ b/system/async/tasks/ScheduledTask.cfc @@ -1,6 +1,6 @@ /** - * This object represents a scheduled task that will be sent in to an executor for scheduling. - * It has a fluent and human dsl for setting it up and restricting is scheduling. + * This object represents a scheduled task that will be sent in to a scheduled executor for scheduling. + * It has a fluent and human dsl for setting it up and restricting is scheduling and frequency of scheduling. * * A task can be represented as either a closure or a cfc with a `run()` or custom runnable method. */ @@ -12,7 +12,8 @@ component accessors="true" { property name="delay" type="numeric"; /** - * The period of execution of the tasks in this schedule + * A fixed time period of execution of the tasks in this schedule. It does not wait for tasks to finish, + * tasks are fired exactly at that time period. */ property name="period" type="numeric"; @@ -22,7 +23,7 @@ component accessors="true" { property name="spacedDelay" type="numeric"; /** - * The task closure or CFC to execute + * The task closure or CFC to execute in the task */ property name="task"; @@ -42,7 +43,8 @@ component accessors="true" { property name="disabled" type="boolean"; /** - * A closure, that if registered, determines if this task will be sent for scheduling or not + * A closure, that if registered, determines if this task will be sent for scheduling or not. + * It is both evaluated at scheduling and at runtime. */ property name="when" type="any"; @@ -57,7 +59,7 @@ component accessors="true" { property name="scheduler"; /** - * The collection of stats for the task: { created, lastRun, nextRun, totalRuns, totalFailures, totalSuccess } + * The collection of stats for the task: { created, lastRun, nextRun, totalRuns, totalFailures, totalSuccess, lastResult, neverRun, lastExecutionTime } */ property name="stats" type="struct"; @@ -104,7 +106,7 @@ component accessors="true" { "totalFailures" : 0, "totalSuccess" : 0, "lastExecutionTime" : 0, - "lastResults" : "", + "lastResult" : "", "neverRun" : true }; // Life cycle methods @@ -150,7 +152,7 @@ component accessors="true" { * @timezone The timezone string identifier */ ScheduledTask function setTimezone( required timezone ){ - variables.timezone = variables.chronoUnit.ZoneId.of( arguments.timezone ); + variables.timezone = createObject( "java", "java.time.ZoneId" ).of( arguments.timezone ); return this; } @@ -240,31 +242,31 @@ component accessors="true" { variables.stats.neverRun = false; try { - // Life-Cycle methods + // Before Interceptor if ( isClosure( variables.beforeTask ) ) { variables.beforeTask( this ); } - // Target task call proxy + // Target task call callable if ( isClosure( variables.task ) || isCustomFunction( variables.task ) ) { - variables.stats.lastResults = variables.task() ?: ""; + variables.stats.lastResult = variables.task() ?: ""; } else { - variables.stats.lastResults = invoke( variables.task, variables.method ) ?: ""; + variables.stats.lastResult = invoke( variables.task, variables.method ) ?: ""; } - // Life-Cycle methods + // After Interceptor if ( isClosure( variables.afterTask ) ) { - variables.afterTask( this, variables.stats.lastResults ); + variables.afterTask( this, variables.stats.lastResult ); } - // store successes - variables.stats.totalSuccess++; + // store successes and call success interceptor + variables.stats.totalSuccess = variables.stats.totalSuccess + 1; if ( isClosure( variables.onTaskSuccess ) ) { - variables.onTaskSuccess( this, variables.stats.lastResults ); + variables.onTaskSuccess( this, variables.stats.lastResult ); } } catch ( any e ) { // store failures - variables.stats.totalFailures++; + variables.stats.totalFailures = variables.stats.totalFailures + 1; // Life Cycle if ( isClosure( variables.onTaskFailure ) ) { variables.onTaskFailure( this, e ); diff --git a/tests/specs/async/tasks/SchedulerSpec.cfc b/tests/specs/async/tasks/SchedulerSpec.cfc index fe2e796a2..3d825e5f0 100644 --- a/tests/specs/async/tasks/SchedulerSpec.cfc +++ b/tests/specs/async/tasks/SchedulerSpec.cfc @@ -110,22 +110,22 @@ component extends="tests.specs.async.BaseAsyncSpec" { expect( record.scheduledAt ).toBeEmpty(); // Wait for them to execute - sleep( 500 ); + sleep( 1000 ); var stats = scheduler.getTaskStats(); debug( stats ); - expect( stats.test1.neverRun ).toBeFalse(); - expect( stats.test2.neverRun ).toBeFalse(); - expect( stats.test3.neverRun ).toBeTrue(); + expect( stats.test1.neverRun ).toBeFalse( "test 1 neverRun" ); + expect( stats.test2.neverRun ).toBeFalse( "test 2 neverRun" ); + expect( stats.test3.neverRun ).toBeTrue( "test 3 neverRun" ); - expect( stats.test1.totalRuns ).toBe( 1 ); - expect( stats.test2.totalRuns ).toBe( 1 ); - expect( stats.test3.totalRuns ).toBe( 0 ); + expect( stats.test1.totalRuns ).toBe( 1, "test1 totalRuns" ); + expect( stats.test2.totalRuns ).toBe( 1, "test2 totalRuns" ); + expect( stats.test3.totalRuns ).toBe( 0, "test3 totalRuns" ); } finally { sleep( 1000 ); scheduler.shutdown(); - expect( scheduler.hasStarted() ).toBeFalse(); + expect( scheduler.hasStarted() ).toBeFalse( "Final scheduler stopped" ); } } ); } ); From 7bd10906547886ca3364c53535662b2aad0479f7 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Tue, 13 Apr 2021 14:35:56 -0500 Subject: [PATCH 13/40] creation of coldbox counter parts for schedulers and tasks --- system/async/tasks/ScheduledTask.cfc | 17 +- system/async/tasks/Scheduler.cfc | 8 +- system/core/util/Util.cfc | 36 +- system/web/tasks/ColdBoxScheduledTask.cfc | 129 +++++ system/web/tasks/ColdBoxScheduler.cfc | 459 ++++++++++++++++++ test-harness/config/Scheduler.cfc | 19 + .../resourcesTest/config/Scheduler.cfc | 19 + tests/specs/async/tasks/SchedulerSpec.cfc | 1 + 8 files changed, 671 insertions(+), 17 deletions(-) create mode 100644 system/web/tasks/ColdBoxScheduledTask.cfc create mode 100644 system/web/tasks/ColdBoxScheduler.cfc create mode 100644 test-harness/config/Scheduler.cfc create mode 100644 test-harness/modules/resourcesTest/config/Scheduler.cfc diff --git a/system/async/tasks/ScheduledTask.cfc b/system/async/tasks/ScheduledTask.cfc index 85b8b35b4..ef3dd38a5 100644 --- a/system/async/tasks/ScheduledTask.cfc +++ b/system/async/tasks/ScheduledTask.cfc @@ -77,6 +77,8 @@ component accessors="true" { any task = "", method = "run" ){ + // Utility class + variables.util = new coldbox.system.core.util.Util(); // Link up the executor and name variables.executor = arguments.executor; variables.name = arguments.name; @@ -99,15 +101,28 @@ component accessors="true" { variables.scheduler = ""; // Prepare execution tracking stats variables.stats = { + // When task got created "created" : now(), + // The last execution run timestamp "lastRun" : "", + // When's the next execution "nextRun" : "", + // Total runs "totalRuns" : 0, + // Total faiulres "totalFailures" : 0, + // Total successful task executions "totalSuccess" : 0, + // How long the last execution took "lastExecutionTime" : 0, + // The latest result if any "lastResult" : "", - "neverRun" : true + // If the task has never ran or not + "neverRun" : true, + // Server Host + "inetHost" : variables.util.discoverInetHost(), + // Server IP + "localIp" : variables.util.getServerIp() }; // Life cycle methods variables.beforeTask = ""; diff --git a/system/async/tasks/Scheduler.cfc b/system/async/tasks/Scheduler.cfc index b713e2c66..9caeeface 100644 --- a/system/async/tasks/Scheduler.cfc +++ b/system/async/tasks/Scheduler.cfc @@ -43,6 +43,8 @@ component accessors="true" singleton { * @asyncManager The async manager we are linked to */ function init( required name, required asyncManager ){ + // Utility class + variables.util = new coldbox.system.core.util.Util(); // Name variables.name = arguments.name; // The async manager @@ -109,7 +111,11 @@ component accessors="true" singleton { // Any error messages when scheduling "errorMessage" : "", // The exception stacktrace if something went wrong scheduling the task - "stacktrace" : "" + "stacktrace" : "", + // Server Host + "inetHost" : variables.util.discoverInetHost(), + // Server IP + "localIp" : variables.util.getServerIp() }; return oTask; diff --git a/system/core/util/Util.cfc b/system/core/util/Util.cfc index d8696e09a..7a94dc949 100644 --- a/system/core/util/Util.cfc +++ b/system/core/util/Util.cfc @@ -9,6 +9,24 @@ The main ColdBox utility library, it is built with tags to allow for dumb ACF10 -----------------------------------------------------------------------> + /** + * Get the hostname of the executing machine. + */ + function discoverInetHost(){ + try { + return createObject( "java", "java.net.InetAddress" ).getLocalHost().getHostName(); + } catch ( any e ) { + return cgi.SERVER_NAME; + } + } + + /** + * Get the server IP Address + */ + function getServerIp(){ + return ( isNull( cgi.local_addr ) ? "0.0.0.0" : cgi.local_addr ); + } + /** * Builds the unique Session Key of a user request and returns it to you. */ @@ -202,11 +220,7 @@ The main ColdBox utility library, it is built with tags to allow for dumb ACF10 // Found? if ( lookup.pos[ 1 ] ) { // Get Variable Name From Pattern - var varName = mid( - returnString, - lookup.pos[ 2 ], - lookup.len[ 2 ] - ); + var varName = mid( returnString, lookup.pos[ 2 ], lookup.len[ 2 ] ); var varValue = "VAR_NOT_FOUND"; // Lookup Value @@ -218,17 +232,9 @@ The main ColdBox utility library, it is built with tags to allow for dumb ACF10 varValue = structFindKey( arguments.settings, varName )[ 1 ].value; } // Remove PlaceHolder Entirely - returnString = removeChars( - returnString, - lookup.pos[ 1 ], - lookup.len[ 1 ] - ); + returnString = removeChars( returnString, lookup.pos[ 1 ], lookup.len[ 1 ] ); // Insert Var Value - returnString = insert( - varValue, - returnString, - lookup.pos[ 1 ] - 1 - ); + returnString = insert( varValue, returnString, lookup.pos[ 1 ] - 1 ); } else { break; } diff --git a/system/web/tasks/ColdBoxScheduledTask.cfc b/system/web/tasks/ColdBoxScheduledTask.cfc new file mode 100644 index 000000000..f4f439790 --- /dev/null +++ b/system/web/tasks/ColdBoxScheduledTask.cfc @@ -0,0 +1,129 @@ +/** + * This object represents a scheduled task that will be sent in to a scheduled executor for scheduling. + * It has a fluent and human dsl for setting it up and restricting is scheduling and frequency of scheduling. + * + * A task can be represented as either a closure or a cfc with a `run()` or custom runnable method. + */ +component extends="coldbox.system.async.tasks.ScheduledTask" accessors="true" { + + /** + * -------------------------------------------------------------------------- + * DI + * -------------------------------------------------------------------------- + */ + + property name="controller" inject="coldbox"; + property name="wirebox" inject="wirebox"; + property name="cachebox" inject="cachebox"; + property name="log" inject="logbox:logger:{this}"; + + /** + * -------------------------------------------------------------------------- + * Properties + * -------------------------------------------------------------------------- + */ + + /** + * Execution Environment + */ + property name="environments" type="array"; + + /** + * This indicates that the task should ONLY run on one server and not on all servers clustered for the application. + * Please note that this will ONLY work if you are using a distributed cache in your application via CacheBox. + * The default cache region we will use is the template cache, which you can connect to any distributed + * caching engine like: Redis, Couchbase, Mongo, Elastic, DB etc. + */ + property name="serverFixation" type="boolean"; + + /** + * The cache name to use for server fixation and more. By default we use the template region + */ + property name="cacheName"; + + /** + * Constructor + * + * @name The name of this task + * @executor The executor this task will run under and be linked to + * @task The closure or cfc that represents the task (optional) + * @method The method on the cfc to call, defaults to "run" (optional) + */ + ScheduledTask function init( + required name, + required executor, + any task = "", + method = "run" + ){ + // init + super.init( argumentCollection = arguments ); + // seed environments + variables.environments = []; + // Can we run on all servers, or just one + variables.serverFixation = false; + // CacheBox Region + variables.cacheName = "template"; + + return this; + } + + /** + * Set the environments that this task can run under ONLY + * + * @environment A string, a list, or an array of environments + */ + ColdBoxScheduledTask function setEnvironments( required environment ){ + if ( isSimpleValue( arguments.environment ) ) { + arguments.environment = listToArray( arguments.environment ); + } + variables.environments = arguments.environment; + return this; + } + + /** + * This indicates that the task should ONLY run on one server and not on all servers clustered for the application. + * Please note that this will ONLY work if you are using a distributed cache in your application via CacheBox. + * The default cache region we will use is the template cache, which you can connect to any distributed + * caching engine like: Redis, Couchbase, Mongo, Elastic, DB etc. + */ + ColdBoxScheduledTask function onOneServer(){ + variables.serverFixation = true; + return this; + } + + /** + * Verifies if we can schedule this task or not by looking at the following constraints: + * + * - disabled + * - environments + * - when closure + */ + boolean function isDisabled(){ + // Call super and if disabled already, then just exit out. + if ( super.isDisabled() ) { + return true; + } + + // Environments Check + if ( + variables.environments.len() && !variables.environments.containsNoCase( + variables.controller.getSetting( "environment" ) + ) + ) { + return true; + } + + // Not disabled + return false; + } + + /** + * This method retrieves the selected CacheBox provider that will be used for server fixation and much more. + * + * @return coldbox.system.cache.providers.IColdBoxProvider + */ + function getCache(){ + return variables.cachebox.getCache( variables.cacheName ); + } + +} diff --git a/system/web/tasks/ColdBoxScheduler.cfc b/system/web/tasks/ColdBoxScheduler.cfc new file mode 100644 index 000000000..94a69a431 --- /dev/null +++ b/system/web/tasks/ColdBoxScheduler.cfc @@ -0,0 +1,459 @@ +/** + * The Async Scheduler is in charge of registering scheduled tasks, starting them, monitoring them and shutting them down if needed. + * + * Each scheduler is bound to an scheduled executor class. You can override the executor using the `setExecutor()` method if you so desire. + * The scheduled executor will be named {name}-scheduler + * + * In a ColdBox context, you might have the global scheduler in charge of the global tasks and also 1 per module as well in HMVC fashion. + * In a ColdBox context, this object will inherit from the ColdBox super type as well dynamically at runtime. + * + */ +component + extends ="coldbox.system.async.tasks.Scheduler" + accessors="true" + singleton +{ + + /** + * -------------------------------------------------------------------------- + * Properties + * -------------------------------------------------------------------------- + */ + + /** + * ColdBox controller + */ + property name="controller"; + + /** + * CacheBox + */ + property name="cachebox"; + + /** + * WireBox + */ + property name="wirebox"; + + /** + * Logger + */ + property name="log"; + + /** + * Constructor + * + * @name The name of this scheduler + * @asyncManager The async manager we are linked to + * @asyncManager.inject coldbox:asyncManager + * @controller The coldBox controller + * @controller.inject coldbox + */ + function init( + required name, + required asyncManager, + required controller + ){ + // Super init + super.init( argumentCollection = arguments ); + // Controller + variables.controller = arguments.controller; + // Register Log object + variables.log = variables.controller.getLogBox().getLogger( this ); + // Register CacheBox + variables.cacheBox = arguments.controller.getCacheBox(); + // Register WireBox + variables.wireBox = arguments.controller.getWireBox(); + return this; + } + + /** + * -------------------------------------------------------------------------- + * Overidden Methods + * -------------------------------------------------------------------------- + */ + + /** + * Register a new task in this scheduler that will be executed once the `startup()` is fired or manually + * via the run() method of the task. + * + * @return a ScheduledTask object so you can work on the registration of the task + */ + ColdBoxScheduledTask function task( required name ){ + // Create task with custom name + var oColdBoxTask = variables.wirebox + .getInstance( + "coldbox.system.web.tasks.ColdBoxScheduledTask", + { name : arguments.name, executor : variables.executor } + ) + // Set default timezone into the task + .setTimezone( getTimezone().getId() ); + + // Super init + super.init( arguments.name ); + // Overwrite task object with ColdBox one + variables.tasks[ arguments.name ].task = oColdBoxTask; + + return oColdBoxTask; + } + + + /** + * -------------------------------------------------------------------------- + * ColdBox Methods + * -------------------------------------------------------------------------- + */ + + /** + * Get a instance object from WireBox + * + * @name The mapping name or CFC path or DSL to retrieve + * @initArguments The constructor structure of arguments to passthrough when initializing the instance + * @dsl The DSL string to use to retrieve an instance + * + * @return The requested instance + */ + function getInstance( name, initArguments = {}, dsl ){ + return variables.controller.getWirebox().getInstance( argumentCollection = arguments ); + } + + /** + * Retrieve the system web renderer + * + * @return coldbox.system.web.Renderer + */ + function getRenderer(){ + return variables.controller.getRenderer(); + } + + /** + * Render out a view + * + * @view The the view to render, if not passed, then we look in the request context for the current set view. + * @args A struct of arguments to pass into the view for rendering, will be available as 'args' in the view. + * @module The module to render the view from explicitly + * @cache Cached the view output or not, defaults to false + * @cacheTimeout The time in minutes to cache the view + * @cacheLastAccessTimeout The time in minutes the view will be removed from cache if idle or requested + * @cacheSuffix The suffix to add into the cache entry for this view rendering + * @cacheProvider The provider to cache this view in, defaults to 'template' + * @collection A collection to use by this Renderer to render the view as many times as the items in the collection (Array or Query) + * @collectionAs The name of the collection variable in the partial rendering. If not passed, we will use the name of the view by convention + * @collectionStartRow The start row to limit the collection rendering with + * @collectionMaxRows The max rows to iterate over the collection rendering with + * @collectionDelim A string to delimit the collection renderings by + * @prePostExempt If true, pre/post view interceptors will not be fired. By default they do fire + * @name The name of the rendering region to render out, Usually all arguments are coming from the stored region but you override them using this function's arguments. + * + * @return The rendered view + */ + function view( + view = "", + struct args = {}, + module = "", + boolean cache = false, + cacheTimeout = "", + cacheLastAccessTimeout = "", + cacheSuffix = "", + cacheProvider = "template", + collection, + collectionAs = "", + numeric collectionStartRow = "1", + numeric collectionMaxRows = 0, + collectionDelim = "", + boolean prePostExempt = false, + name + ){ + return variables.controller.getRenderer().renderView( argumentCollection = arguments ); + } + + /** + * Renders an external view anywhere that cfinclude works. + * + * @view The the view to render + * @args A struct of arguments to pass into the view for rendering, will be available as 'args' in the view. + * @cache Cached the view output or not, defaults to false + * @cacheTimeout The time in minutes to cache the view + * @cacheLastAccessTimeout The time in minutes the view will be removed from cache if idle or requested + * @cacheSuffix The suffix to add into the cache entry for this view rendering + * @cacheProvider The provider to cache this view in, defaults to 'template' + * + * @return The rendered view + */ + function externalView( + required view, + struct args = {}, + boolean cache = false, + cacheTimeout = "", + cacheLastAccessTimeout = "", + cacheSuffix = "", + cacheProvider = "template" + ){ + return variables.controller.getRenderer().renderExternalView( argumentCollection = arguments ); + } + + /** + * Render a layout or a layout + view combo + * + * @layout The layout to render out + * @module The module to explicitly render this layout from + * @view The view to render within this layout + * @args An optional set of arguments that will be available to this layouts/view rendering ONLY + * @viewModule The module to explicitly render the view from + * @prePostExempt If true, pre/post layout interceptors will not be fired. By default they do fire + * + * @return The rendered layout + */ + function layout( + layout, + module = "", + view = "", + struct args = {}, + viewModule = "", + boolean prePostExempt = false + ){ + return variables.controller.getRenderer().renderLayout( argumentCollection = arguments ); + } + + /** + * Announce an interception + * + * @state The interception state to announce + * @data A data structure used to pass intercepted information. + * @async If true, the entire interception chain will be ran in a separate thread. + * @asyncAll If true, each interceptor in the interception chain will be ran in a separate thread and then joined together at the end. + * @asyncAllJoin If true, each interceptor in the interception chain will be ran in a separate thread and joined together at the end by default. If you set this flag to false then there will be no joining and waiting for the threads to finalize. + * @asyncPriority The thread priority to be used. Either LOW, NORMAL or HIGH. The default value is NORMAL + * @asyncJoinTimeout The timeout in milliseconds for the join thread to wait for interceptor threads to finish. By default there is no timeout. + * + * @return struct of thread information or void + */ + any function announce( + required state, + struct data = {}, + boolean async = false, + boolean asyncAll = false, + boolean asyncAllJoin = true, + asyncPriority = "NORMAL", + numeric asyncJoinTimeout = 0 + ){ + // Backwards Compat: Remove by ColdBox 7 + if ( !isNull( arguments.interceptData ) ) { + arguments.data = arguments.interceptData; + } + return variables.controller.getInterceptorService().announce( argumentCollection = arguments ); + } + + /** + * Executes events with full life-cycle methods and returns the event results if any were returned. + * + * @event The event string to execute, if nothing is passed we will execute the application's default event. + * @prePostExempt If true, pre/post handlers will not be fired. Defaults to false + * @private Execute a private event if set, else defaults to public events + * @defaultEvent The flag that let's this service now if it is the default event running or not. USED BY THE FRAMEWORK ONLY + * @eventArguments A collection of arguments to passthrough to the calling event handler method + * @cache Cached the output of the runnable execution, defaults to false. A unique key will be created according to event string + arguments. + * @cacheTimeout The time in minutes to cache the results + * @cacheLastAccessTimeout The time in minutes the results will be removed from cache if idle or requested + * @cacheSuffix The suffix to add into the cache entry for this event rendering + * @cacheProvider The provider to cache this event rendering in, defaults to 'template' + * + * @return null or anything produced from the event + */ + function runEvent( + event = "", + boolean prePostExempt = false, + boolean private = false, + boolean defaultEvent = false, + struct eventArguments = {}, + boolean cache = false, + cacheTimeout = "", + cacheLastAccessTimeout = "", + cacheSuffix = "", + cacheProvider = "template" + ){ + return variables.controller.runEvent( argumentCollection = arguments ); + } + + /** + * Executes internal named routes with or without parameters. If the named route is not found or the route has no event to execute then this method will throw an `InvalidArgumentException`. + * If you need a route from a module then append the module address: `@moduleName` or prefix it like in run event calls `moduleName:routeName` in order to find the right route. + * The route params will be passed to events as action arguments much how eventArguments work. + * + * @name The name of the route + * @params The parameters of the route to replace + * @cache Cached the output of the runnable execution, defaults to false. A unique key will be created according to event string + arguments. + * @cacheTimeout The time in minutes to cache the results + * @cacheLastAccessTimeout The time in minutes the results will be removed from cache if idle or requested + * @cacheSuffix The suffix to add into the cache entry for this event rendering + * @cacheProvider The provider to cache this event rendering in, defaults to 'template' + * @prePostExempt If true, pre/post handlers will not be fired. Defaults to false + * + * @throws InvalidArgumentException + * + * @return null or anything produced from the route + */ + any function runRoute( + required name, + struct params = {}, + boolean cache = false, + cacheTimeout = "", + cacheLastAccessTimeout = "", + cacheSuffix = "", + cacheProvider = "template", + boolean prePostExempt = false + ){ + return variables.controller.runRoute( argumentCollection = arguments ); + } + + /** + * Get a named CacheBox Cache + * + * @name The name of the cache to retrieve, if not passed, it used the 'default' cache. + * + * @return coldbox.system.cache.providers.IColdBoxProvider + */ + function getCache( name = "default" ){ + return variables.controller.getCache( arguments.name ); + } + + /** + * Get a setting from the system + * + * @name The key of the setting + * @defaultValue If not found in config, default return value + * + * @throws SettingNotFoundException + * + * @return The requested setting + */ + function getSetting( required name, defaultValue ){ + return variables.controller.getSetting( argumentCollection = arguments ); + } + + /** + * Get a ColdBox setting + * + * @name The key to get + * @defaultValue The default value if it doesn't exist + * + * @throws SettingNotFoundException + * + * @return The framework setting value + */ + function getColdBoxSetting( required name, defaultValue ){ + return variables.controller.getColdBoxSetting( argumentCollection = arguments ); + } + + /** + * Check if the setting exists in the application + * + * @name The key of the setting + */ + boolean function settingExists( required name ){ + return variables.controller.settingExists( argumentCollection = arguments ); + } + + /** + * Set a new setting in the system + * + * @name The key of the setting + * @value The value of the setting + * + * @return FrameworkSuperType + */ + any function setSetting( required name, required value ){ + controller.setSetting( argumentCollection = arguments ); + return this; + } + + /** + * Get a module's settings structure or a specific setting if the setting key is passed + * + * @module The module to retrieve the configuration settings from + * @setting The setting to retrieve if passed + * @defaultValue The default value to return if setting does not exist + * + * @return struct or any + */ + any function getModuleSettings( required module, setting, defaultValue ){ + var moduleSettings = getModuleConfig( arguments.module ).settings; + // return specific setting? + if ( structKeyExists( arguments, "setting" ) ) { + return ( + structKeyExists( moduleSettings, arguments.setting ) ? moduleSettings[ arguments.setting ] : arguments.defaultValue + ); + } + return moduleSettings; + } + + /** + * Get a module's configuration structure + * + * @module The module to retrieve the configuration structure from + * + * @throws InvalidModuleException - The module passed is invalid + * + * @return The struct requested + */ + struct function getModuleConfig( required module ){ + var mConfig = variables.controller.getSetting( "modules" ); + if ( structKeyExists( mConfig, arguments.module ) ) { + return mConfig[ arguments.module ]; + } + throw( + message = "The module you passed #arguments.module# is invalid.", + detail = "The loaded modules are #structKeyList( mConfig )#", + type = "InvalidModuleException" + ); + } + + /** + * Resolve a file to be either relative or absolute in your application + * + * @pathToCheck The file path to check + */ + string function locateFilePath( required pathToCheck ){ + return variables.controller.locateFilePath( argumentCollection = arguments ); + } + + /** + * Resolve a directory to be either relative or absolute in your application + * + * @pathToCheck The file path to check + */ + string function locateDirectoryPath( required pathToCheck ){ + return variables.controller.locateDirectoryPath( argumentCollection = arguments ); + } + + /** + * Retrieve a Java System property or env value by name. It looks at properties first then environment variables + * + * @key The name of the setting to look up. + * @defaultValue The default value to use if the key does not exist in the system properties or the env + */ + function getSystemSetting( required key, defaultValue ){ + return variables.controller.getUtil().getSystemSetting( argumentCollection = arguments ); + } + + /** + * Retrieve a Java System property only! + * + * @key The name of the setting to look up. + * @defaultValue The default value to use if the key does not exist in the system properties or the env + */ + function getSystemProperty( required key, defaultValue ){ + return variables.controller.getUtil().getSystemProperty( argumentCollection = arguments ); + } + + /** + * Retrieve a environment variable only + * + * @key The name of the setting to look up. + * @defaultValue The default value to use if the key does not exist in the system properties or the env + */ + function getEnv( required key, defaultValue ){ + return variables.controller.getUtil().getEnv( argumentCollection = arguments ); + } + +} diff --git a/test-harness/config/Scheduler.cfc b/test-harness/config/Scheduler.cfc new file mode 100644 index 000000000..8e219d15d --- /dev/null +++ b/test-harness/config/Scheduler.cfc @@ -0,0 +1,19 @@ +component { + + property name="userService" inject="UserService"; + + function configure(){ + + task( "vistacaballo-notifications" ) + .call( () => runEvent( "tasks.sendNotifications" ) ) + .dailyAt( "9:00" ) + .environments( [ "staging", "production"] ) + .before( ( task ) => notifyJorge ) + .after( ( task, results ) => notifyJorge ) + .onFailure( ( task, exception ) => {} ) + .onSuccess( ( task, results ) => {} ) + .onOneServer(); + + } + +} \ No newline at end of file diff --git a/test-harness/modules/resourcesTest/config/Scheduler.cfc b/test-harness/modules/resourcesTest/config/Scheduler.cfc new file mode 100644 index 000000000..64e9d38cc --- /dev/null +++ b/test-harness/modules/resourcesTest/config/Scheduler.cfc @@ -0,0 +1,19 @@ +component { + + property name="userService" inject="UserService"; + + function configure(){ + + task( "vistacaballo-notifications" ) + .call( () => runEvent( "tasks.sendNotifications" ) ) + .dailyAt( "9:00" ) + .environments( [ "staging", "production"] ) + .before( ( task ) => notifyJorge ) + .after( ( results ) => notifyJorge ) + .onFailure( ( task, exception ) => {} ) + .onSuccess( ( task, results ) => {} ) + .onOneServer(); + + } + +} \ No newline at end of file diff --git a/tests/specs/async/tasks/SchedulerSpec.cfc b/tests/specs/async/tasks/SchedulerSpec.cfc index 3d825e5f0..fc46635fb 100644 --- a/tests/specs/async/tasks/SchedulerSpec.cfc +++ b/tests/specs/async/tasks/SchedulerSpec.cfc @@ -113,6 +113,7 @@ component extends="tests.specs.async.BaseAsyncSpec" { sleep( 1000 ); var stats = scheduler.getTaskStats(); + debug( scheduler.getTasks() ); debug( stats ); expect( stats.test1.neverRun ).toBeFalse( "test 1 neverRun" ); From 5d5664f869761a3620f4f496f98bf7e7aee71db7 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Wed, 14 Apr 2021 09:22:12 -0500 Subject: [PATCH 14/40] WIREBOX-112 #resolve virtual inheritance causes double inits on objects that do not have a constructor and their parent does. --- system/ioc/Builder.cfc | 601 ++++++++++++++++++++++++----------------- 1 file changed, 348 insertions(+), 253 deletions(-) diff --git a/system/ioc/Builder.cfc b/system/ioc/Builder.cfc index 42092a395..796cd4020 100644 --- a/system/ioc/Builder.cfc +++ b/system/ioc/Builder.cfc @@ -4,7 +4,7 @@ * --- * The WireBox builder for components, java, etc. I am in charge of building stuff and integration dsl builders. **/ - component serializable="false" accessors="true"{ +component serializable="false" accessors="true" { /** * Injector Reference @@ -55,25 +55,34 @@ * @return coldbox.system.ioc.Builder */ Builder function init( required injector ){ - - variables.injector = arguments.injector; - variables.logBox = arguments.injector.getLogBox(); - variables.log = arguments.injector.getLogBox().getlogger( this ); - variables.utility = arguments.injector.getUtil(); - variables.customDSL = {}; + variables.injector = arguments.injector; + variables.logBox = arguments.injector.getLogBox(); + variables.log = arguments.injector.getLogBox().getlogger( this ); + variables.utility = arguments.injector.getUtil(); + variables.customDSL = {}; // Internal DSL Registry variables.internalDSL = [ - "coldbox", "box", "executor", "cachebox", "logbox", "model", "id", "provider", "wirebox", "java", "byType" + "coldbox", + "box", + "executor", + "cachebox", + "logbox", + "model", + "id", + "provider", + "wirebox", + "java", + "byType" ]; // Do we need to build the coldbox DSL namespace - if( variables.injector.isColdBoxLinked() ){ + if ( variables.injector.isColdBoxLinked() ) { variables.coldboxDSL = new coldbox.system.ioc.dsl.ColdBoxDSL( arguments.injector ); } // Is CacheBox Linked? - if( variables.injector.isCacheBoxLinked() ){ + if ( variables.injector.isCacheBoxLinked() ) { variables.cacheBoxDSL = new coldbox.system.ioc.dsl.CacheBoxDSL( arguments.injector ); } @@ -87,12 +96,12 @@ * Register custom DSL builders with this main wirebox builder * */ - Builder function registerCustomBuilders(){ + Builder function registerCustomBuilders(){ var customDSL = variables.injector.getBinder().getCustomDSL(); // Register Custom DSL Builders - for( var key in customDSL ){ - registerDSL( namespace=key, path=customDSL[ key ] ); + for ( var key in customDSL ) { + registerDSL( namespace = key, path = customDSL[ key ] ); } return this; } @@ -103,11 +112,11 @@ * @namespace The namespace you would like to register * @path The instantiation path to the CFC that implements this scope, it must have an init() method and implement: coldbox.system.ioc.dsl.IDSLBuilder */ - Builder function registerDSL( required namespace, required path ){ + Builder function registerDSL( required namespace, required path ){ // register dsl variables.customDSL[ arguments.namespace ] = new "#arguments.path#"( variables.injector ); // Debugging - if( variables.log.canDebug() ){ + if ( variables.log.canDebug() ) { variables.log.debug( "Registered custom DSL Builder with namespace: #arguments.namespace#" ); } return this; @@ -121,12 +130,12 @@ var targetProvider = this.$wbProviders[ getFunctionCalledName() ]; // Verify if this is a mapping first? - if( targetInjector.containsInstance( targetProvider ) ){ - return targetInjector.getInstance( name=targetProvider, targetObject=this ); + if ( targetInjector.containsInstance( targetProvider ) ) { + return targetInjector.getInstance( name = targetProvider, targetObject = this ); } // else treat as full DSL - return targetInjector.getInstance( dsl=targetProvider, targetObject=this ); + return targetInjector.getInstance( dsl = targetProvider, targetObject = this ); } /** @@ -138,58 +147,77 @@ * @initArguments.doc_generic struct */ function buildCFC( required mapping, initArguments = structNew() ){ - var thisMap = arguments.mapping; - var oModel = createObject( "component", thisMap.getPath() ); - - // Do we have virtual inheritance? - var constructorArgs = thisMap.getDIConstructorArguments(); - var constructorArgNames = constructorArgs.map( function( arg ) { return arg.name; } ); - if( thisMap.isVirtualInheritance() ){ + var thisMap = arguments.mapping; + var oModel = createObject( "component", thisMap.getPath() ); + + // Do we have virtual inheritance? + var constructorArgs = thisMap.getDIConstructorArguments(); + var constructorArgNames = constructorArgs.map( function( arg ){ + return arg.name; + } ); + if ( thisMap.isVirtualInheritance() ) { // retrieve the VI mapping. var viMapping = variables.injector.getBinder().getMapping( thisMap.getVirtualInheritance() ); // Does it match the family already? - if( NOT isInstanceOf( oModel, viMapping.getPath() ) ){ + if ( NOT isInstanceOf( oModel, viMapping.getPath() ) ) { // Virtualize it. - toVirtualInheritance( viMapping, oModel, thisMap ); - - // Only add virtual inheritance constructor args if we don't already have one with that name. - arrayAppend( constructorArgs, viMapping.getDIConstructorArguments().filter( function( arg ) { - return !arrayContainsNoCase( constructorArgNames, arg.name ); - } ), true ); + toVirtualInheritance( viMapping, oModel, thisMap ); + // Only add virtual inheritance constructor args if we don't already have one with that name. + arrayAppend( + constructorArgs, + viMapping + .getDIConstructorArguments() + .filter( function( arg ){ + return !arrayContainsNoCase( constructorArgNames, arg.name ); + } ), + true + ); } } // Constructor initialization? - if( thisMap.isAutoInit() AND structKeyExists( oModel, thisMap.getConstructor() ) ){ - // Get Arguments + if ( thisMap.isAutoInit() AND structKeyExists( oModel, thisMap.getConstructor() ) ) { + // Get Arguments var constructorArgCollection = buildArgumentCollection( thisMap, constructorArgs, oModel ); // Do We have initArguments to override - if( NOT structIsEmpty( arguments.initArguments ) ){ - structAppend( constructorArgCollection, arguments.initArguments, true ); + if ( NOT structIsEmpty( arguments.initArguments ) ) { + structAppend( + constructorArgCollection, + arguments.initArguments, + true + ); } try { // Invoke constructor - invoke( oModel, thisMap.getConstructor(), constructorArgCollection ); - } catch( any e ){ - - var reducedTagContext = e.tagContext.reduce( function(result, file) { - if( !result.done ) { - if( file.template.listLast( '/\' ) == 'Builder.cfc' ) { + invoke( + oModel, + thisMap.getConstructor(), + constructorArgCollection + ); + } catch ( any e ) { + writeDump( var = e ); + abort; + var reducedTagContext = e.tagContext + .reduce( function( result, file ){ + if ( !result.done ) { + if ( file.template.listLast( "/\" ) == "Builder.cfc" ) { result.done = true; } else { - result.rows.append( '#file.template#:#file.line#' ); + result.rows.append( "#file.template#:#file.line#" ); } } return result; - }, {rows:[],done:false} ).rows.toList( chr(13)&chr(10) ); + }, { rows : [], done : false } ) + .rows + .toList( chr( 13 ) & chr( 10 ) ); throw( type = "Builder.BuildCFCDependencyException", message = "Error building: #thisMap.getName()# -> #e.message# #e.detail#.", - detail = "DSL: #thisMap.getDSL()#, Path: #thisMap.getPath()#, + detail = "DSL: #thisMap.getDSL()#, Path: #thisMap.getPath()#, Error Location: #reducedTagContext#" ); @@ -197,7 +225,7 @@ } return oModel; - } + } /** * Build an object using a factory method @@ -207,12 +235,12 @@ * @initArguments The constructor structure of arguments to passthrough when initializing the instance * @initArguments.doc_generic struct */ - function buildFactoryMethod( required mapping, initArguments=structNew() ){ - var thisMap = arguments.mapping; + function buildFactoryMethod( required mapping, initArguments = structNew() ){ + var thisMap = arguments.mapping; var factoryName = thisMap.getPath(); // check if factory exists, else throw exception - if( NOT variables.injector.containsInstance( factoryName ) ){ + if ( NOT variables.injector.containsInstance( factoryName ) ) { throw( message = "The factory mapping: #factoryName# is not registered with the injector", type = "Builder.InvalidFactoryMappingException" @@ -220,20 +248,24 @@ } // get Factory mapping - var oFactory = variables.injector.getInstance( factoryName ); + var oFactory = variables.injector.getInstance( factoryName ); // Get Method Arguments - var methodArgs = buildArgumentCollection( thisMap, thisMap.getDIMethodArguments(), oFactory ); + var methodArgs = buildArgumentCollection( + thisMap, + thisMap.getDIMethodArguments(), + oFactory + ); // Do we have overrides - if( NOT structIsEmpty( arguments.initArguments ) ){ + if ( NOT structIsEmpty( arguments.initArguments ) ) { structAppend( methodArgs, arguments.initArguments, true ); } // Get From Factory var oModel = invoke( oFactory, thisMap.getMethod(), methodArgs ); - //Return factory bean + // Return factory bean return oModel; - } + } /** * Build a Java class via mappings @@ -242,22 +274,22 @@ * @mapping.doc_generic coldbox.system.ioc.config.Mapping */ function buildJavaClass( required mapping ){ - var DIArgs = arguments.mapping.getDIConstructorArguments(); - var args = []; - var thisMap = arguments.mapping; + var DIArgs = arguments.mapping.getDIConstructorArguments(); + var args = []; + var thisMap = arguments.mapping; // Process arguments to constructor call. - for( var thisArg in DIArgs ){ - if( !isNull( thisArg.javaCast ) ){ - args.append( javaCast( thisArg.javacast, thisArg.value ) ); + for ( var thisArg in DIArgs ) { + if ( !isNull( thisArg.javaCast ) ) { + args.append( javacast( thisArg.javacast, thisArg.value ) ); } else { args.append( thisArg.value ); } } // init? - if( thisMap.isAutoInit() ){ - if( args.len() ){ + if ( thisMap.isAutoInit() ) { + if ( args.len() ) { return invoke( createObject( "java", arguments.mapping.getPath() ), "init", @@ -279,53 +311,66 @@ * @argumentArray The argument array of data * @targetObject The target object we are building the DSL dependency for */ - function buildArgumentCollection( required mapping, required argumentArray, required targetObject ){ - var thisMap = arguments.mapping; - var DIArgs = arguments.argumentArray; - var args = {}; + function buildArgumentCollection( + required mapping, + required argumentArray, + required targetObject + ){ + var thisMap = arguments.mapping; + var DIArgs = arguments.argumentArray; + var args = {}; // Process Arguments - for( var thisArg in DIArgs ){ - + for ( var thisArg in DIArgs ) { // Process if we have a value and continue - if( !isNull( thisArg.value ) ){ + if ( !isNull( thisArg.value ) ) { args[ thisArg.name ] = thisArg.value; continue; } // Is it by DSL construction? If so, add it and continue, if not found it returns null, which is ok - if( !isNull( thisArg.dsl ) ){ - args[ thisArg.name ] = buildDSLDependency( definition=thisArg, targetID=thisMap.getName(), targetObject=arguments.targetObject ); + if ( !isNull( thisArg.dsl ) ) { + args[ thisArg.name ] = buildDSLDependency( + definition = thisArg, + targetID = thisMap.getName(), + targetObject = arguments.targetObject + ); continue; } // If we get here then it is by ref id, so let's verify it exists and optional - if( variables.injector.containsInstance( thisArg.ref ) ){ - args[ thisArg.name ] = variables.injector.getInstance( name=thisArg.ref ); + if ( variables.injector.containsInstance( thisArg.ref ) ) { + args[ thisArg.name ] = variables.injector.getInstance( name = thisArg.ref ); continue; } // Not found, so check if it is required - if( thisArg.required ){ + if ( thisArg.required ) { // Log the error - variables.log.error( "Target: #thisMap.getName()# -> Argument reference not located: #thisArg.name#", thisArg ); + variables.log.error( + "Target: #thisMap.getName()# -> Argument reference not located: #thisArg.name#", + thisArg + ); // not found but required, then throw exception throw( message = "Argument reference not located: #thisArg.name#", detail = "Injecting: #thisMap.getName()#. The argument details are: #thisArg.toString()#.", type = "Injector.ArgumentNotFoundException" ); - } // else just log it via debug - else if( variables.log.canDebug() ){ - variables.log.debug( "Target: #thisMap.getName()# -> Argument reference not located: #thisArg.name#", thisArg ); } - + // else just log it via debug + else if ( variables.log.canDebug() ) { + variables.log.debug( + "Target: #thisMap.getName()# -> Argument reference not located: #thisArg.name#", + thisArg + ); + } } return args; - } + } - /** + /** * Build a webservice object * * @mapping The mapping to construct @@ -333,21 +378,25 @@ * @initArguments The constructor structure of arguments to passthrough when initializing the instance * @initArguments.doc_generic struct */ - function buildWebservice( required mapping, initArguments={} ){ - var argStruct = {}; - var DIArgs = arguments.mapping.getDIConstructorArguments(); + function buildWebservice( required mapping, initArguments = {} ){ + var argStruct = {}; + var DIArgs = arguments.mapping.getDIConstructorArguments(); // Process args - for( var thisArg in DIArgs ){ + for ( var thisArg in DIArgs ) { argStruct[ thisArg.name ] = thisArg.value; } // Do we have overrides - if( NOT structIsEmpty( arguments.initArguments ) ){ + if ( NOT structIsEmpty( arguments.initArguments ) ) { structAppend( argStruct, arguments.initArguments, true ); } - return createObject( "webservice", arguments.mapping.getPath(), argStruct ); + return createObject( + "webservice", + arguments.mapping.getPath(), + argStruct + ); } /** @@ -357,20 +406,20 @@ * @mapping.doc_generic coldbox.system.ioc.config.Mapping */ function buildFeed( required mapping ){ - var results = {}; + var results = {}; var feedAttributes = { - action = "read", - source = arguments.mapping.getPath(), - query = "results.items", - properties = "results.metadata", - timeout = "20", - userAgent = "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36" + action : "read", + source : arguments.mapping.getPath(), + query : "results.items", + properties : "results.metadata", + timeout : "20", + userAgent : "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36" }; include "/coldbox/system/core/util/cffeed.cfm"; return results; - } + } // Internal DSL Builders @@ -383,12 +432,12 @@ * * @return The requested DSL object */ - function buildSimpleDSL( required dsl, required targetID, required targetObject = "" ){ - var definition = { - required = true, - name = "", - dsl = arguments.dsl - }; + function buildSimpleDSL( + required dsl, + required targetID, + required targetObject = "" + ){ + var definition = { required : true, name : "", dsl : arguments.dsl }; return buildDSLDependency( definition = definition, targetID = arguments.targetID, @@ -434,115 +483,125 @@ * * @return The requested object or null if not found and not required */ - function buildDSLDependency( required definition, required targetID, targetObject = "" ){ - var refLocal = {}; - var DSLNamespace = listFirst( arguments.definition.dsl, ":" ); + function buildDSLDependency( + required definition, + required targetID, + targetObject = "" + ){ + var refLocal = {}; + var DSLNamespace = listFirst( arguments.definition.dsl, ":" ); // Check if Custom DSL exists, if it does, execute it - if( structKeyExists( variables.customDSL, DSLNamespace ) ){ - return variables.customDSL[ DSLNamespace ].process( argumentCollection=arguments ); + if ( structKeyExists( variables.customDSL, DSLNamespace ) ) { + return variables.customDSL[ DSLNamespace ].process( argumentCollection = arguments ); } // Determine Type of Injection according to type // Some namespaces requires the ColdBox context, if not found, an exception is thrown. - switch( DSLNamespace ){ - + switch ( DSLNamespace ) { // ColdBox Context DSL - case "coldbox" : case "box" : { - if( !variables.injector.isColdBoxLinked() ){ + case "coldbox": + case "box": { + if ( !variables.injector.isColdBoxLinked() ) { throw( - message = "The DSLNamespace: #DSLNamespace# cannot be used as it requires a ColdBox Context", - type = "Builder.IllegalDSLException" + message = "The DSLNamespace: #DSLNamespace# cannot be used as it requires a ColdBox Context", + type = "Builder.IllegalDSLException" ); } - refLocal.dependency = variables.coldboxDSL.process( argumentCollection=arguments ); + refLocal.dependency = variables.coldboxDSL.process( argumentCollection = arguments ); break; } // Executor - case "executor" : { + case "executor": { // retrieve it - refLocal.dependency = getExecutorDSl( argumentCollection=arguments ); + refLocal.dependency = getExecutorDSl( argumentCollection = arguments ); break; } // CacheBox Context DSL - case "cacheBox" : { + case "cacheBox": { // check if linked - if( !variables.injector.isCacheBoxLinked() AND !variables.injector.isColdBoxLinked() ){ + if ( !variables.injector.isCacheBoxLinked() AND !variables.injector.isColdBoxLinked() ) { throw( - message = "The DSLNamespace: #DSLNamespace# cannot be used as it requires a ColdBox/CacheBox Context", - type = "Builder.IllegalDSLException" + message = "The DSLNamespace: #DSLNamespace# cannot be used as it requires a ColdBox/CacheBox Context", + type = "Builder.IllegalDSLException" ); } // retrieve it - refLocal.dependency = variables.cacheBoxDSL.process( argumentCollection=arguments ); + refLocal.dependency = variables.cacheBoxDSL.process( argumentCollection = arguments ); break; } // logbox injection DSL always available - case "logbox" : { - refLocal.dependency = variables.logBoxDSL.process( argumentCollection=arguments ); + case "logbox": { + refLocal.dependency = variables.logBoxDSL.process( argumentCollection = arguments ); break; } // WireBox Internal DSL for models and id - case "model" : case "id" : { - refLocal.dependency = getModelDSL( argumentCollection=arguments ); + case "model": + case "id": { + refLocal.dependency = getModelDSL( argumentCollection = arguments ); break; } // provider injection DSL always available - case "provider" : { - refLocal.dependency = getProviderDSL( argumentCollection=arguments ); + case "provider": { + refLocal.dependency = getProviderDSL( argumentCollection = arguments ); break; } // wirebox injection DSL always available - case "wirebox" : { - refLocal.dependency = getWireBoxDSL( argumentCollection=arguments ); + case "wirebox": { + refLocal.dependency = getWireBoxDSL( argumentCollection = arguments ); break; } // java class - case "java" : { - refLocal.dependency = getJavaDSL( argumentCollection=arguments ); + case "java": { + refLocal.dependency = getJavaDSL( argumentCollection = arguments ); break; } // coldfusion type annotation - case "bytype" : { - refLocal.dependency = getByTypeDSL( argumentCollection=arguments ); + case "bytype": { + refLocal.dependency = getByTypeDSL( argumentCollection = arguments ); break; } // If no DSL's found, let's try to use the name as the empty namespace - default : { - if( len( DSLNamespace ) && left( DSLNamespace, 1 ) == "@" ){ - arguments.definition.dsl = arguments.definition.name & arguments.definition.dsl; - } - refLocal.dependency = getModelDSL( argumentCollection=arguments ); + default: { + if ( len( DSLNamespace ) && left( DSLNamespace, 1 ) == "@" ) { + arguments.definition.dsl = arguments.definition.name & arguments.definition.dsl; + } + refLocal.dependency = getModelDSL( argumentCollection = arguments ); } } // return only if found - if( !isNull( refLocal.dependency ) ){ + if ( !isNull( refLocal.dependency ) ) { return refLocal.dependency; } // was dependency required? If so, then throw exception - if( arguments.definition.required ){ - + if ( arguments.definition.required ) { // Build human-readable description of the mapping var depDesc = []; - if( !isNull( arguments.definition.name ) ) { depDesc.append( "Name of '#arguments.definition.name#'" ); } - if( !isNull( arguments.definition.DSL ) ) { depDesc.append( "DSL of '#arguments.definition.DSL#'" ); } - if( !isNull( arguments.definition.REF ) ) { depDesc.append( "REF of '#arguments.definition.REF#'" ); } + if ( !isNull( arguments.definition.name ) ) { + depDesc.append( "Name of '#arguments.definition.name#'" ); + } + if ( !isNull( arguments.definition.DSL ) ) { + depDesc.append( "DSL of '#arguments.definition.DSL#'" ); + } + if ( !isNull( arguments.definition.REF ) ) { + depDesc.append( "REF of '#arguments.definition.REF#'" ); + } - var injectMessage = "The target '#arguments.targetID#' requested a missing dependency with a #depDesc.toList( ' and ' )#"; + var injectMessage = "The target '#arguments.targetID#' requested a missing dependency with a #depDesc.toList( " and " )#"; // Logging - if( variables.log.canError() ){ + if ( variables.log.canError() ) { variables.log.error( injectMessage, arguments.definition ); } @@ -550,16 +609,18 @@ throw( message = injectMessage, // safe serialization that won't blow uo on complex values or do weird things with nulls (looking at you, Adobe) - detail = serializeJSON ( arguments.definition.map( function(k,v){ - if( isNull( v ) ) { - return; - } else if( !isSimpleValue( v ) ) { - return '[complex value]'; - } else { - return v; - } - } ) ), - type = "Builder.DSLDependencyNotFoundException" + detail = serializeJSON( + arguments.definition.map( function( k, v ){ + if ( isNull( v ) ) { + return; + } else if ( !isSimpleValue( v ) ) { + return "[complex value]"; + } else { + return v; + } + } ) + ), + type = "Builder.DSLDependencyNotFoundException" ); } // else return void, no dependency found that was required @@ -574,7 +635,7 @@ * @targetObject The target object we are building the DSL dependency for */ private any function getJavaDSL( required definition, targetObject ){ - var javaClass = getToken( arguments.definition.dsl, 2, ":" ); + var javaClass = getToken( arguments.definition.dsl, 2, ":" ); return createObject( "java", javaClass ); } @@ -586,41 +647,60 @@ * @targetObject The target object we are building the DSL dependency for */ private any function getWireBoxDSL( required definition, targetObject ){ - var thisType = arguments.definition.dsl; - var thisTypeLen = listLen(thisType,":" ); - var thisLocationType = ""; - var thisLocationKey = ""; + var thisType = arguments.definition.dsl; + var thisTypeLen = listLen( thisType, ":" ); + var thisLocationType = ""; + var thisLocationKey = ""; // DSL stages - switch( thisTypeLen ){ + switch ( thisTypeLen ) { // WireBox injector - case 1 : { return variables.injector; } + case 1: { + return variables.injector; + } // Level 2 DSL - case 2 : { + case 2: { thisLocationKey = getToken( thisType, 2, ":" ); - switch( thisLocationKey ){ - case "parent" : { return variables.injector.getParent(); } - case "eventManager" : { return variables.injector.getEventManager(); } - case "binder" : { return variables.injector.getBinder(); } - case "populator" : { return variables.injector.getObjectPopulator(); } - case "properties" : { return variables.injector.getBinder().getProperties(); } + switch ( thisLocationKey ) { + case "parent": { + return variables.injector.getParent(); + } + case "eventManager": { + return variables.injector.getEventManager(); + } + case "binder": { + return variables.injector.getBinder(); + } + case "populator": { + return variables.injector.getObjectPopulator(); + } + case "properties": { + return variables.injector.getBinder().getProperties(); + } } break; } // Level 3 DSL - case 3 : { - thisLocationType = getToken( thisType, 2, ":" ); - thisLocationKey = getToken( thisType, 3, ":" ); + case 3: { + thisLocationType = getToken( thisType, 2, ":" ); + thisLocationKey = getToken( thisType, 3, ":" ); // DSL Level 2 Stage Types - switch( thisLocationType ){ + switch ( thisLocationType ) { // Scope DSL - case "scope" : { return variables.injector.getScope( thisLocationKey ); break; } - case "property" : { return variables.injector.getBinder().getProperty( thisLocationKey );break; } + case "scope": { + return variables.injector.getScope( thisLocationKey ); + break; + } + case "property": { + return variables.injector.getBinder().getProperty( thisLocationKey ); + break; + } } break; - } // end level 3 main DSL + } + // end level 3 main DSL } } @@ -631,30 +711,32 @@ * @targetObject The target object we are building the DSL dependency for */ private any function getExecutorDSl( required definition, targetObject ){ - var asyncManager = variables.injector.getAsyncManager(); - var thisType = arguments.definition.dsl; - var thisTypeLen = listLen( thisType, ":" ); - var executorName = ""; + var asyncManager = variables.injector.getAsyncManager(); + var thisType = arguments.definition.dsl; + var thisTypeLen = listLen( thisType, ":" ); + var executorName = ""; // DSL stages - switch( thisTypeLen ){ + switch ( thisTypeLen ) { // property name='myExecutorService' inject="executor"; - case 1 : { + case 1: { executorName = arguments.definition.name; break; } // executor:{alias} stage - case 2 : { + case 2: { executorName = getToken( thisType, 2, ":" ); break; } } // Check if executor Exists - if( asyncManager.hasExecutor( executorName ) ){ + if ( asyncManager.hasExecutor( executorName ) ) { return asyncManager.getExecutor( executorName ); - } else if ( variables.log.canDebug() ){ - variables.log.debug( "X getExecutorDsl() cannot find executor #executorName# using definition #arguments.definition.toString()#" ); + } else if ( variables.log.canDebug() ) { + variables.log.debug( + "X getExecutorDsl() cannot find executor #executorName# using definition #arguments.definition.toString()#" + ); } } @@ -665,15 +747,15 @@ * @targetObject The target object we are building the DSL dependency for */ private any function getModelDSL( required definition, targetObject ){ - var thisType = arguments.definition.dsl; - var thisTypeLen = listLen( thisType, ":" ); - var methodCall = ""; - var modelName = ""; + var thisType = arguments.definition.dsl; + var thisTypeLen = listLen( thisType, ":" ); + var methodCall = ""; + var modelName = ""; // DSL stages - switch( thisTypeLen ){ + switch ( thisTypeLen ) { // No injection defined, use property name: property name='luis' inject; - case 0 : { + case 0: { modelName = arguments.definition.name; break; } @@ -681,9 +763,9 @@ // property name='luis' inject="id"; use property name // property name='luis' inject="model"; use property name // property name='luis' inject="alias"; - case 1 : { + case 1: { // Are we the key identifiers - if( listFindNoCase( "id,model", arguments.definition.dsl ) ){ + if ( listFindNoCase( "id,model", arguments.definition.dsl ) ) { modelName = arguments.definition.name; } // else we are a real ID @@ -694,29 +776,31 @@ break; } // model:{alias} stage - case 2 : { + case 2: { modelName = getToken( thisType, 2, ":" ); break; } // model:{alias}:{method} stage - case 3 : { - modelName = getToken( thisType, 2, ":" ); - methodCall = getToken( thisType, 3, ":" ); + case 3: { + modelName = getToken( thisType, 2, ":" ); + methodCall = getToken( thisType, 3, ":" ); break; } } // Check if model Exists - if( variables.injector.containsInstance( modelName ) ){ + if ( variables.injector.containsInstance( modelName ) ) { // Get Model object var oModel = variables.injector.getInstance( modelName ); // Factories: TODO: Add arguments with 'ref()' parsing for argument references or 'dsl()' - if( len( methodCall ) ){ + if ( len( methodCall ) ) { return invoke( oModel, methodCall ); } return oModel; - } else if ( variables.log.canDebug() ){ - variables.log.debug( "getModelDSL() cannot find model object #modelName# using definition #arguments.definition.toString()#" ); + } else if ( variables.log.canDebug() ) { + variables.log.debug( + "getModelDSL() cannot find model object #modelName# using definition #arguments.definition.toString()#" + ); } } @@ -726,41 +810,47 @@ * @definition The dependency definition structure * @targetObject The target object we are building the DSL dependency for. If empty, means we are just requesting building */ - private any function getProviderDSL( required definition, targetObject="" ){ - var thisType = arguments.definition.dsl; - var thisTypeLen = listLen( thisType,":" ); - var providerName = ""; + private any function getProviderDSL( required definition, targetObject = "" ){ + var thisType = arguments.definition.dsl; + var thisTypeLen = listLen( thisType, ":" ); + var providerName = ""; // DSL stages - switch( thisTypeLen ){ + switch ( thisTypeLen ) { // provider default, get name of the provider from property - case 1: { providerName = arguments.definition.name; break; } + case 1: { + providerName = arguments.definition.name; + break; + } // provider:{name} stage - case 2: { providerName = getToken( thisType, 2, ":" ); break; } + case 2: { + providerName = getToken( thisType, 2, ":" ); + break; + } // multiple stages then most likely it is a full DSL being used - default : { + default: { providerName = replaceNoCase( thisType, "provider:", "" ); } } // Build provider arguments var args = { - scopeRegistration = variables.injector.getScopeRegistration(), - scopeStorage = variables.injector.getScopeStorage(), - targetObject = arguments.targetObject + scopeRegistration : variables.injector.getScopeRegistration(), + scopeStorage : variables.injector.getScopeStorage(), + targetObject : arguments.targetObject }; // Check if the passed in provider is an ID directly - if( variables.injector.containsInstance( providerName ) ){ + if ( variables.injector.containsInstance( providerName ) ) { args.name = providerName; } // Else try to tag it by FULL DSL - else{ + else { args.dsl = providerName; } // Build provider and return it. - return createObject( "component","coldbox.system.ioc.Provider" ).init( argumentCollection=args ); + return createObject( "component", "coldbox.system.ioc.Provider" ).init( argumentCollection = args ); } /** @@ -770,9 +860,9 @@ * @targetObject The target object we are building the DSL dependency for. If empty, means we are just requesting building */ private any function getByTypeDSL( required definition, targetObject ){ - var injectType = arguments.definition.type; + var injectType = arguments.definition.type; - if( variables.injector.containsInstance( injectType ) ){ + if ( variables.injector.containsInstance( injectType ) ) { return variables.injector.getInstance( injectType ); } } @@ -786,83 +876,88 @@ * * @return The target object */ - function toVirtualInheritance( required mapping, required target, required targetMapping ){ - var excludedProperties = "$super,$wbaopmixed,$mixed,this,init"; + function toVirtualInheritance( + required mapping, + required target, + required targetMapping + ){ + var excludedProperties = "$super,super,$wbaopmixed,$mixed,this,init"; // Check if the base mapping has been discovered yet - if( NOT arguments.mapping.isDiscovered() ){ + if ( NOT arguments.mapping.isDiscovered() ) { // process inspection of instance - arguments.mapping.process( - binder = variables.injector.getBinder(), - injector = variables.injector - ); + arguments.mapping.process( binder = variables.injector.getBinder(), injector = variables.injector ); } - // Build it out now and wire it + // Build it out the base object and wire it var baseObject = variables.injector.buildInstance( arguments.mapping ); - variables.injector.autowire( target=baseObject, mapping=arguments.mapping ); + variables.injector.autowire( target = baseObject, mapping = arguments.mapping ); // Mix them up baby! variables.utility.getMixerUtil().start( arguments.target ); variables.utility.getMixerUtil().start( baseObject ); - // Check if init already exists in target and base? - if( structKeyExists( arguments.target, "init" ) AND structKeyExists( baseObject, "init" ) ){ + // Check if init already exists in target and base? If so, then inject it as $superInit + if ( structKeyExists( arguments.target, "init" ) AND structKeyExists( baseObject, "init" ) ) { arguments.target.$superInit = baseObject.init; } // Mix in public methods and public properties - for( var key in baseObject ){ + for ( var key in baseObject ) { // If target has overridden method, then don't override it with mixin, simulated inheritance - if( NOT structKeyExists( arguments.target, key ) AND NOT listFindNoCase( excludedProperties, key ) ){ + if ( NOT structKeyExists( arguments.target, key ) AND NOT listFindNoCase( excludedProperties, key ) ) { // inject method in both variables and this scope to simulate public access arguments.target.injectMixin( key, baseObject[ key ] ); } } - // Prepare for private Property/method Injections - var targetVariables = arguments.target.getVariablesMixin(); - var generateAccessors = false; - if( arguments.mapping.getObjectMetadata().keyExists( "accessors" ) and arguments.mapping.getObjectMetadata().accessors ){ + // Prepare for private property/method Injections + var targetVariables = arguments.target.getVariablesMixin(); + var generateAccessors = false; + if ( + arguments.mapping.getObjectMetadata().keyExists( "accessors" ) and arguments.mapping.getObjectMetadata().accessors + ) { generateAccessors = true; } - var baseProperties = {}; + var baseProperties = {}; // Process baseProperties lookup map - if( arguments.mapping.getObjectMetadata().keyExists( "properties" ) ){ - arguments.mapping.getObjectMetadata().properties + if ( arguments.mapping.getObjectMetadata().keyExists( "properties" ) ) { + arguments.mapping + .getObjectMetadata() + .properties .each( function( item ){ baseProperties[ item.name ] = true; } ); } + // Commenting out for now, as I believe this causes double initializations of objects // Copy init only if the base object has it and the child doesn't. - if( !structKeyExists( arguments.target, "init" ) AND structKeyExists( baseObject, "init" ) ){ - arguments.target.injectMixin( 'init', baseObject.init ); - } + // if ( !structKeyExists( arguments.target, "init" ) AND structKeyExists( baseObject, "init" ) ) { + // arguments.target.injectMixin( "init", baseObject.init ); + // } // local reference to arguments to use in closures below var args = arguments; - baseObject.getVariablesMixin() + baseObject + .getVariablesMixin() // filter out overrides - .filter( function( key, value ) { + .filter( function( key, value ){ return ( !targetVariables.keyExists( key ) AND NOT listFindNoCase( excludedProperties, key ) ); } ) .each( function( propertyName, propertyValue ){ // inject the property/method now - if( !isNull( arguments.propertyValue ) ) { + if ( !isNull( arguments.propertyValue ) ) { args.target.injectPropertyMixin( propertyName, propertyValue ); } // Do we need to do automatic generic getter/setters - if( generateAccessors and baseProperties.keyExists( propertyName ) ){ - - if( ! structKeyExists( args.target, "get#propertyName#" ) ){ + if ( generateAccessors and baseProperties.keyExists( propertyName ) ) { + if ( !structKeyExists( args.target, "get#propertyName#" ) ) { args.target.injectMixin( "get" & propertyName, variables.genericGetter ); } - if( ! structKeyExists( args.target, "set#propertyName#" ) ){ + if ( !structKeyExists( args.target, "set#propertyName#" ) ) { args.target.injectMixin( "set" & propertyName, variables.genericSetter ); } - } } ); @@ -875,8 +970,8 @@ /** * Generic setter for Virtual Inheritance */ - private function genericSetter() { - var propName = getFunctionCalledName().replaceNoCase( 'set', '' ); + private function genericSetter(){ + var propName = getFunctionCalledName().replaceNoCase( "set", "" ); variables[ propName ] = arguments[ 1 ]; return this; } @@ -884,8 +979,8 @@ /** * Generic getter for Virtual Inheritance */ - private function genericGetter() { - var propName = getFunctionCalledName().replaceNoCase( 'get', '' ); + private function genericGetter(){ + var propName = getFunctionCalledName().replaceNoCase( "get", "" ); return variables[ propName ]; } From c296a2f1078e79f6dc4042ad3757160c7ec10351 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Wed, 14 Apr 2021 09:39:12 -0500 Subject: [PATCH 15/40] COLDBOX-998 #resolve Convert util to script and optimize --- system/core/util/Util.cfc | 676 +++++++++++++++++--------------------- 1 file changed, 302 insertions(+), 374 deletions(-) diff --git a/system/core/util/Util.cfc b/system/core/util/Util.cfc index 7a94dc949..0170ad57e 100644 --- a/system/core/util/Util.cfc +++ b/system/core/util/Util.cfc @@ -1,14 +1,78 @@ - - - +/** + * The main ColdBox utility library, it is built with tags to allow for dumb ACF10 compatibility + */ +component { + + /**************************************************************** + * SERVER/USER/CFML ENGINE HELPERS * + ****************************************************************/ + + /** + * Add a CFML Mapping to the running engine + * + * @name The name of the mapping + * @path The path of the mapping + * @mappings A struct of mappings to incorporate instead of one-offs + */ + Util function addMapping( string name, string path, struct mappings ){ + var mappingHelper = ""; + + // Detect server + if ( listFindNoCase( "Lucee", server.coldfusion.productname ) ) { + mappingHelper = new LuceeMappingHelper(); + } else { + mappingHelper = new CFMappingHelper(); + } + + if ( !isNull( arguments.mappings ) ) { + mappingHelper.addMappings( arguments.mappings ); + } else { + // Add / registration + if ( left( arguments.name, 1 ) != "/" ) { + arguments.name = "/#arguments.name#"; + } + + // Add mapping + mappingHelper.addMapping( arguments.name, arguments.path ); + } + + return this; + } + + /** + * Check if you are in cfthread or not for any CFML Engine + * + * @path The file target + */ + boolean function inThread(){ + var engine = "ADOBE"; + + if ( server.coldfusion.productname eq "Lucee" ) { + engine = "LUCEE"; + } + + switch ( engine ) { + case "ADOBE": { + if ( + findNoCase( + "cfthread", + createObject( "java", "java.lang.Thread" ) + .currentThread() + .getThreadGroup() + .getName() + ) + ) { + return true; + } + break; + } + case "LUCEE": { + return isInThread(); + } + } + return false; + } + /** * Get the hostname of the executing machine. */ @@ -55,158 +119,70 @@ The main ColdBox utility library, it is built with tags to allow for dumb ACF10 ); } } - - - - - - if ( structKeyExists( variables, "mixerUtil" ) ) { - return variables.mixerUtil; - } - variables.mixerUtil = new coldbox.system.core.dynamic.MixerUtil(); - return variables.mixerUtil; - - - - - - - + + /**************************************************************** + * CONVERSTION METHODS * + ****************************************************************/ + + /** + * Convert an array to struct argument notation + * + * @in The array to convert + */ + struct function arrayToStruct( required array in ){ return arguments.in.reduce( function( result, item, index ){ - var target = {}; - if ( !isNull( arguments.result ) ) { - target = arguments.result; - } - target[ arguments.index ] = arguments.item; - return target; - } ); - - - - - - - + arguments.result[ arguments.index ] = arguments.item; + return arguments.result; + }, {} ); + } + + /**************************************************************** + * FILE HELPERS * + ****************************************************************/ + + /** + * Get the last modified date of a file + * + * @filename The file target + */ + function fileLastModified( required filename ){ return getFileInfo( getAbsolutePath( arguments.filename ) ).lastModified; - - - - - - - - - - - - - + } + + /** + * Rip the extension of a filename. + * + * @filename The file target + */ + function ripExtension( required filename ){ + return reReplace( arguments.filename, "\.[^.]*$", "" ); + } + + /** + * Turn any system path, either relative or absolute, into a fully qualified one + * + * @path The file target + */ + function getAbsolutePath( required path ){ if ( fileExists( arguments.path ) ) { return arguments.path; } return expandPath( arguments.path ); - - - - - - - var engine = "ADOBE"; - - if ( server.coldfusion.productname eq "Lucee" ) { - engine = "LUCEE"; - } - - switch ( engine ) { - case "ADOBE": { - if ( - findNoCase( - "cfthread", - createObject( "java", "java.lang.Thread" ) - .currentThread() - .getThreadGroup() - .getName() - ) - ) { - return true; - } - break; - } - case "LUCEE": { - var version = listFirst( server.lucee.version, "." ); - - if ( version == 5 ) { - return isInThread(); - } + } - if ( - findNoCase( - "cfthread", - createObject( "java", "java.lang.Thread" ) - .currentThread() - .getThreadGroup() - .getName() - ) - ) { - return true; - } - break; - } - } - // end switch statement. + /**************************************************************** + * STRING HELPERS * + ****************************************************************/ - return false; - - - - - - - - + /** + * PlaceHolder Replacer for strings containing ${} patterns + * + * @str The string target + * @settings The structure of settings to use in the replacements + * + * @return The string with the replacements + */ + function placeHolderReplacer( required str, required settings ){ var returnString = arguments.str; var regex = "\$\{([0-9a-z\-\.\_]+)\}"; var lookup = 0; @@ -241,24 +217,21 @@ The main ColdBox utility library, it is built with tags to allow for dumb ACF10 } return returnString; - - - - - - - - + } + + /**************************************************************** + * ENVIRONMENT METHODS * + ****************************************************************/ + + /** + * Retrieve a Java System property or env value by name. It looks at properties first then environment variables + * + * @key The name of the setting to look up. + * @defaultValue The default value to use if the key does not exist in the system properties or the env + * + * @throws SystemSettingNotFound When the java system property or env is not found + */ + function getSystemSetting( required key, defaultValue ){ var value = getJavaSystem().getProperty( arguments.key ); if ( !isNull( local.value ) ) { return value; @@ -274,27 +247,20 @@ The main ColdBox utility library, it is built with tags to allow for dumb ACF10 } throw( - type = "SystemSettingNotFound", - message = "Could not find a Java System property or Env setting with key [#arguments.key#]." + type : "SystemSettingNotFound", + message: "Could not find a Java System property or Env setting with key [#arguments.key#]." ); - - - - - - - - + } + + /** + * Retrieve a Java System property value by key + * + * @key The name of the setting to look up. + * @defaultValue The default value to use if the key does not exist in the system properties or the env + * + * @throws SystemSettingNotFound When the java system property is not found + */ + function getSystemProperty( required key, defaultValue ){ var value = getJavaSystem().getProperty( arguments.key ); if ( !isNull( local.value ) ) { return value; @@ -308,24 +274,17 @@ The main ColdBox utility library, it is built with tags to allow for dumb ACF10 type = "SystemSettingNotFound", message = "Could not find a Java System property with key [#arguments.key#]." ); - - - - - - - - + } + + /** + * Retrieve a Java System environment value by name + * + * @key The name of the setting to look up. + * @defaultValue The default value to use if the key does not exist in the system properties or the env + * + * @throws SystemSettingNotFound When the java system property is not found + */ + function getEnv( required key, defaultValue ){ var value = getJavaSystem().getEnv( arguments.key ); if ( !isNull( local.value ) ) { return value; @@ -339,38 +298,42 @@ The main ColdBox utility library, it is built with tags to allow for dumb ACF10 type = "SystemSettingNotFound", message = "Could not find a environment variable with key [#arguments.key#]." ); - - - - - - + } + + /** + * Retrieve an instance of Java System + */ + function getJavaSystem(){ if ( !structKeyExists( variables, "javaSystem" ) ) { variables.javaSystem = createObject( "java", "java.lang.System" ); } return variables.javaSystem; - - - - - - - - - - + } + + /** + * Get the mixer utility + * + * @return coldbox.system.core.dynamic.MixerUtil + */ + function getMixerUtil(){ + if ( structKeyExists( variables, "mixerUtil" ) ) { + return variables.mixerUtil; + } + variables.mixerUtil = new coldbox.system.core.dynamic.MixerUtil(); + return variables.mixerUtil; + } + + /**************************************************************** + * COLDBOX TAXONOMY Methods * + ****************************************************************/ + + /** + * Checks if an object is of the passed in family type + * + * @family The family to covert it to: handler, interceptor + * @target The target object + */ + boolean function isFamilyType( required family, required target ){ var familyPath = ""; switch ( arguments.family ) { @@ -388,20 +351,17 @@ The main ColdBox utility library, it is built with tags to allow for dumb ACF10 } return isInstanceOf( arguments.target, familyPath ); - - - - - - - - + } + + /** + * Decorate an object as a ColdBox Family object + * + * @family The family to convert it to + * @target The target object + * + * @return The same target object + */ + function convertToColdBox( required family, required target ){ var familyPath = ""; switch ( arguments.family ) { @@ -439,100 +399,17 @@ The main ColdBox utility library, it is built with tags to allow for dumb ACF10 // Mix in fake super class arguments.target.$super = baseObject; - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + return arguments.target; + } + + /** + * Should we stop recursion or not due to class name found: Boolean + * + * @className The class name to check + * @stopRecursions An array of classes to stop processing for during inheritance trails + */ + private boolean function stopClassRecursion( required classname, required stopRecursions ){ // Try to find a match for ( var thisClass in arguments.stopRecursions ) { if ( compareNoCase( thisClass, arguments.classname ) eq 0 ) { @@ -540,37 +417,88 @@ The main ColdBox utility library, it is built with tags to allow for dumb ACF10 } } return false; - - - - - - - - - - var mappingHelper = ""; + } - // Detect server - if ( listFindNoCase( "Lucee", server.coldfusion.productname ) ) { - mappingHelper = new LuceeMappingHelper(); - } else { - mappingHelper = new CFMappingHelper(); + /** + * Returns a single-level metadata struct that includes all items inhereited from extending classes. + * + * @component The component instance or path to get the metadata from + * @stopRecursions An array of classes to stop processing for during inheritance trails + * @md A structure containing a copy of the metadata for this level of recursion. + * + * @return struct of metadata + */ + function getInheritedMetaData( + required component, + array stopRecursions = [], + struct md = {} + ){ + var loc = {}; + + // First time through, get metaData of component by path or instance + if ( arguments.md.isEmpty() ) { + arguments.md = ( + isObject( arguments.component ) ? getMetadata( arguments.component ) : getComponentMetadata( + arguments.component + ) + ); } - if ( !isNull( arguments.mappings ) ) { - mappingHelper.addMappings( arguments.mappings ); + // If it has a parent, stop and calculate it first, unless of course, we've reached a class we shouldn't recurse into. + if ( + structKeyExists( arguments.md, "extends" ) && + arguments.md.type eq "component" && + stopClassRecursion( md.extends.name, arguments.stopRecursions ) EQ FALSE + ) { + loc.parent = getInheritedMetaData( + component = arguments.component, + stopRecursions = arguments.stopRecursions, + md = arguments.md.extends + ); + // If we're at the end of the line, it's time to start working backwards so start with an empty struct to hold our condensesd metadata. } else { - // Add / registration - if ( left( arguments.name, 1 ) != "/" ) { - arguments.name = "/#arguments.name#"; - } + loc.parent = { "inheritanceTrail" : [] }; + } - // Add mapping - mappingHelper.addMapping( arguments.name, arguments.path ); + // Override ourselves into parent + for ( var thisKey in arguments.md ) { + // Functions and properties are an array of structs keyed on name, so I can treat them the same + if ( listFindNoCase( "functions,properties", thisKey ) ) { + if ( !structKeyExists( loc.parent, thisKey ) ) { + loc.parent[ thisKey ] = []; + } + + // For each function/property in me... + for ( var thisItem in arguments.md[ thisKey ] ) { + loc.parentItemCounter = 0; + loc.foundInParent = false; + // ...Look for an item of the same name in my parent... + for ( var thisParentItem in loc.parent[ thisKey ] ) { + loc.parentItemCounter++; + // ...And override it + if ( compareNoCase( thisItem.name, thisParentItem.name ) eq 0 ) { + loc.parent[ thisKey ][ loc.parentItemCounter ] = thisItem; + loc.foundInParent = true; + break; + } + } + // ..Or add it + if ( not loc.foundInParent ) { + arrayAppend( loc.parent[ thisKey ], thisItem ); + } + } + } + // Add in anything that's not inheritance or implementation + else if ( NOT listFindNoCase( "extends,implements", thisKey ) ) { + loc.parent[ thisKey ] = arguments.md[ thisKey ]; + } } - return this; - - - + // Store away the inheritance trail + arrayPrepend( loc.parent.inheritanceTrail, loc.parent.name ); + + // Return our results + return loc.parent; + } + +} From a060d92f38d5bf201b180022bbaf8292a7326fd0 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Wed, 14 Apr 2021 09:52:57 -0500 Subject: [PATCH 16/40] WIREBOX-113 #resolve new injection dsl: wirebox:asyncManager WIREBOX-114 #resolve New coldbox dsl => coldbox:appScheduler which gives you the appScheduler@coldbox instance --- system/ioc/Builder.cfc | 3 + system/ioc/dsl/ColdBoxDSL.cfc | 229 +++++++++++++++++++++------------- 2 files changed, 142 insertions(+), 90 deletions(-) diff --git a/system/ioc/Builder.cfc b/system/ioc/Builder.cfc index 796cd4020..d8c7bcc95 100644 --- a/system/ioc/Builder.cfc +++ b/system/ioc/Builder.cfc @@ -669,6 +669,9 @@ component serializable="false" accessors="true" { case "eventManager": { return variables.injector.getEventManager(); } + case "asyncManager": { + return variables.injector.getAsyncManager(); + } case "binder": { return variables.injector.getBinder(); } diff --git a/system/ioc/dsl/ColdBoxDSL.cfc b/system/ioc/dsl/ColdBoxDSL.cfc index 796a5e4d2..21898888d 100644 --- a/system/ioc/dsl/ColdBoxDSL.cfc +++ b/system/ioc/dsl/ColdBoxDSL.cfc @@ -1,10 +1,10 @@ /** -* Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp -* www.ortussolutions.com -* --- -* Process DSL functions via ColdBox -**/ -component implements="coldbox.system.ioc.dsl.IDSLBuilder" accessors="true"{ + * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp + * www.ortussolutions.com + * --- + * Process DSL functions via ColdBox + **/ +component implements="coldbox.system.ioc.dsl.IDSLBuilder" accessors="true" { /** * Injector Reference @@ -35,10 +35,10 @@ component implements="coldbox.system.ioc.dsl.IDSLBuilder" accessors="true"{ * @return coldbox.system.ioc.dsl.IDSLBuilder */ function init( required injector ){ - variables.injector = arguments.injector; - variables.coldbox = variables.injector.getColdBox(); - variables.cacheBox = variables.injector.getCacheBox(); - variables.log = variables.injector.getLogBox().getLogger( this ); + variables.injector = arguments.injector; + variables.coldbox = variables.injector.getColdBox(); + variables.cacheBox = variables.injector.getCacheBox(); + variables.log = variables.injector.getLogBox().getLogger( this ); return this; } @@ -54,8 +54,11 @@ component implements="coldbox.system.ioc.dsl.IDSLBuilder" accessors="true"{ function process( required definition, targetObject ){ var DSLNamespace = listFirst( arguments.definition.dsl, ":" ); - switch( DSLNamespace ){ - case "coldbox" : case "box" : { return getColdboxDSL( argumentCollection=arguments ); } + switch ( DSLNamespace ) { + case "coldbox": + case "box": { + return getColdboxDSL( argumentCollection = arguments ); + } } // else ignore not our DSL @@ -70,141 +73,187 @@ component implements="coldbox.system.ioc.dsl.IDSLBuilder" accessors="true"{ * @targetObject The target object we are building the DSL dependency for. If empty, means we are just requesting building */ private function getColdBoxDSL( required definition, targetObject ){ - var thisName = arguments.definition.name; - var thisType = arguments.definition.dsl; - var thisTypeLen = listLen( thisType, ":" ); - var thisLocationType = ""; - var thisLocationKey = ""; - var moduleSettings = ""; + var thisName = arguments.definition.name; + var thisType = arguments.definition.dsl; + var thisTypeLen = listLen( thisType, ":" ); + var thisLocationType = ""; + var thisLocationKey = ""; + var moduleSettings = ""; // Support shortcut for specifying name in the definition instead of the DSl for supporting namespaces - if( thisTypeLen eq 2 + if ( + thisTypeLen eq 2 and listFindNoCase( "setting,fwSetting,interceptor", listLast( thisType, ":" ) ) and len( thisName ) - ){ + ) { // Add the additional alias to the DSL - thisType = thisType & ":" & thisName; + thisType = thisType & ":" & thisName; thisTypeLen = 3; } // DSL stages - switch( thisTypeLen ){ + switch ( thisTypeLen ) { // coldbox only DSL - case 1 : { + case 1: { return variables.coldbox; } // coldbox:{key} stage 2 - case 2 : { + case 2: { thisLocationKey = getToken( thisType, 2, ":" ); - switch( thisLocationKey ){ + switch ( thisLocationKey ) { // Config Struct - case "configSettings" : { return variables.coldbox.getConfigSettings(); } - case "dataMarshaller" : { return variables.coldbox.getDataMarshaller(); } - case "flash" : { return variables.coldbox.getRequestService().getFlashScope(); } - case "fwSettings" : case "coldboxSettings" : { return variables.coldbox.getColdboxSettings(); } - case "handlerService" : { return variables.coldbox.getHandlerService(); } - case "interceptorService" : { return variables.coldbox.getInterceptorService(); } - case "loaderService" : { return variables.coldbox.getLoaderService(); } - case "moduleService" : { return variables.coldbox.getModuleService(); } - case "requestContext" : { return variables.coldbox.getRequestService().getContext(); } - case "requestService" : { return variables.coldbox.getRequestService(); } - case "router" : { return variables.injector.getInstance( "router@coldbox" ); } - case "routingService" : { return variables.coldbox.getRoutingService(); } - case "renderer" : { return variables.coldbox.getRenderer(); } - case "moduleconfig" : { return variables.coldbox.getSetting( "modules" ); } - case "asyncManager" : { return variables.coldbox.getAsyncManager(); } - } // end of services + case "configSettings": { + return variables.coldbox.getConfigSettings(); + } + case "dataMarshaller": { + return variables.coldbox.getDataMarshaller(); + } + case "flash": { + return variables.coldbox.getRequestService().getFlashScope(); + } + case "fwSettings": + case "coldboxSettings": { + return variables.coldbox.getColdboxSettings(); + } + case "handlerService": { + return variables.coldbox.getHandlerService(); + } + case "interceptorService": { + return variables.coldbox.getInterceptorService(); + } + case "loaderService": { + return variables.coldbox.getLoaderService(); + } + case "moduleService": { + return variables.coldbox.getModuleService(); + } + case "requestContext": { + return variables.coldbox.getRequestService().getContext(); + } + case "requestService": { + return variables.coldbox.getRequestService(); + } + case "router": { + return variables.injector.getInstance( "router@coldbox" ); + } + case "routingService": { + return variables.coldbox.getRoutingService(); + } + case "renderer": { + return variables.coldbox.getRenderer(); + } + case "moduleconfig": { + return variables.coldbox.getSetting( "modules" ); + } + case "asyncManager": { + return variables.coldbox.getAsyncManager(); + } + case "appScheduler": { + return variables.injector.getInstance( "appScheduler@coldbox" ); + } + } + // end of services break; } - //coldbox:{key}:{target} Usually for named factories - case 3 : { + // coldbox:{key}:{target} Usually for named factories + case 3: { thisLocationType = getToken( thisType, 2, ":" ); thisLocationKey = getToken( thisType, 3, ":" ); - switch( thisLocationType ){ - case "setting" : case "configSettings" : { + switch ( thisLocationType ) { + case "setting": + case "configSettings": { // module setting? - if( find( "@", thisLocationKey ) ){ + if ( find( "@", thisLocationKey ) ) { moduleSettings = variables.coldbox.getSetting( "modules" ); - if( structKeyExists( moduleSettings, listlast(thisLocationKey, "@" )) - and structKeyExists( moduleSettings[ listlast(thisLocationKey, "@" ) ],"settings" ) - and structKeyExists( moduleSettings[ listlast(thisLocationKey, "@" ) ].settings,listFirst( thisLocationKey, "@" ) ) - ){ - return moduleSettings[ listlast(thisLocationKey, "@" ) ].settings[ listFirst( thisLocationKey, "@" ) ]; - } - else { + if ( + structKeyExists( moduleSettings, listLast( thisLocationKey, "@" ) ) + and structKeyExists( + moduleSettings[ listLast( thisLocationKey, "@" ) ], + "settings" + ) + and structKeyExists( + moduleSettings[ listLast( thisLocationKey, "@" ) ].settings, + listFirst( thisLocationKey, "@" ) + ) + ) { + return moduleSettings[ listLast( thisLocationKey, "@" ) ].settings[ + listFirst( thisLocationKey, "@" ) + ]; + } else { throw( - type = "ColdBoxDSL.InvalidDSL", + type = "ColdBoxDSL.InvalidDSL", message = "The DSL provided was not valid: #arguments.definition.toString()#", - detail="The module requested: #listlast( thisLocationKey, "@" )# does not exist in the loaded modules. Loaded modules are #structKeyList(moduleSettings)#" + detail = "The module requested: #listLast( thisLocationKey, "@" )# does not exist in the loaded modules. Loaded modules are #structKeyList( moduleSettings )#" ); } } // just get setting return variables.coldbox.getSetting( thisLocationKey ); } - case "modulesettings" : { + case "modulesettings": { moduleSettings = variables.coldbox.getSetting( "modules" ); - if( structKeyExists( moduleSettings, thisLocationKey ) ){ + if ( structKeyExists( moduleSettings, thisLocationKey ) ) { return moduleSettings[ thisLocationKey ].settings; - } - else { + } else { throw( - type = "ColdBoxDSL.InvalidDSL", + type = "ColdBoxDSL.InvalidDSL", message = "The DSL provided was not valid: #arguments.definition.toString()#", - detail="The module requested: #thisLocationKey# does not exist in the loaded modules. Loaded modules are #structKeyList(moduleSettings)#" + detail = "The module requested: #thisLocationKey# does not exist in the loaded modules. Loaded modules are #structKeyList( moduleSettings )#" ); } } - case "moduleconfig" : { + case "moduleconfig": { moduleSettings = variables.coldbox.getSetting( "modules" ); - if( structKeyExists( moduleSettings, thisLocationKey ) ){ + if ( structKeyExists( moduleSettings, thisLocationKey ) ) { return moduleSettings[ thisLocationKey ]; - } - else { + } else { throw( - type = "ColdBoxDSL.InvalidDSL", + type = "ColdBoxDSL.InvalidDSL", message = "The DSL provided was not valid: #arguments.definition.toString()#", - detail="The module requested: #thisLocationKey# does not exist in the loaded modules. Loaded modules are #structKeyList(moduleSettings)#" + detail = "The module requested: #thisLocationKey# does not exist in the loaded modules. Loaded modules are #structKeyList( moduleSettings )#" ); } } - case "fwSetting" : case "coldboxSetting" : { return variables.coldbox.getColdBoxSetting( thisLocationKey ); } - case "interceptor" : { return variables.coldbox.getInterceptorService().getInterceptor( thisLocationKey, true ); } - }//end of services + case "fwSetting": + case "coldboxSetting": { + return variables.coldbox.getColdBoxSetting( thisLocationKey ); + } + case "interceptor": { + return variables.coldbox.getInterceptorService().getInterceptor( thisLocationKey, true ); + } + } + // end of services break; } - //coldbox:{key}:{target}:{token} + // coldbox:{key}:{target}:{token} case 4: { - thisLocationType = getToken(thisType,2,":"); - thisLocationKey = getToken(thisType,3,":"); - thisLocationToken = getToken(thisType,4,":"); - switch(thisLocationType){ - case "modulesettings" : { - + thisLocationType = getToken( thisType, 2, ":" ); + thisLocationKey = getToken( thisType, 3, ":" ); + thisLocationToken = getToken( thisType, 4, ":" ); + switch ( thisLocationType ) { + case "modulesettings": { moduleSettings = variables.coldbox.getSetting( "modules" ); - if( structKeyExists( moduleSettings, thisLocationKey ) ){ - if( structKeyExists( moduleSettings[ thisLocationKey ].settings, thisLocationToken ) ) { - return moduleSettings[ thisLocationKey ].settings[ thisLocationToken]; + if ( structKeyExists( moduleSettings, thisLocationKey ) ) { + if ( structKeyExists( moduleSettings[ thisLocationKey ].settings, thisLocationToken ) ) { + return moduleSettings[ thisLocationKey ].settings[ thisLocationToken ]; } else { throw( - type = "ColdBoxDSL.InvalidDSL", + type = "ColdBoxDSL.InvalidDSL", message = "ColdBox DSL cannot find dependency using definition: #arguments.definition.toString()#", - detail = "The setting requested: #thisLocationToken# does not exist in this module. Loaded settings are #structKeyList(moduleSettings[ thisLocationKey ].settings)#" + detail = "The setting requested: #thisLocationToken# does not exist in this module. Loaded settings are #structKeyList( moduleSettings[ thisLocationKey ].settings )#" ); } - } - else { + } else { throw( - type = "ColdBoxDSL.InvalidDSL", + type = "ColdBoxDSL.InvalidDSL", message = "The DSL provided was not valid: #arguments.definition.toString()#", - detail = "The module requested: #thisLocationKey# does not exist in the loaded modules. Loaded modules are #structKeyList(moduleSettings)#" + detail = "The module requested: #thisLocationKey# does not exist in the loaded modules. Loaded modules are #structKeyList( moduleSettings )#" ); } - } } break; @@ -213,10 +262,10 @@ component implements="coldbox.system.ioc.dsl.IDSLBuilder" accessors="true"{ // If we get here we have a problem. throw( - type = "ColdBoxDSL.InvalidDSL", + type = "ColdBoxDSL.InvalidDSL", message = "The DSL provided was not valid: #arguments.definition.toString()#", - detail = "Unknown DSL" + detail = "Unknown DSL" ); } -} \ No newline at end of file +} From 2d770cb841540e00a87603cb574ce4c773aa5fad Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Wed, 14 Apr 2021 10:11:18 -0500 Subject: [PATCH 17/40] COLDBOX-999 #resolve New SchedulerService that mointors and registers application scheduled tasks in an HMVC fashion --- system/async/tasks/Scheduler.cfc | 6 ++ system/web/Controller.cfc | 52 ++++------ system/web/services/SchedulerService.cfc | 126 ++++++++++++++++------- system/web/tasks/ColdBoxScheduler.cfc | 31 +++++- test-harness/config/Scheduler.cfc | 32 ++++-- 5 files changed, 162 insertions(+), 85 deletions(-) diff --git a/system/async/tasks/Scheduler.cfc b/system/async/tasks/Scheduler.cfc index 9caeeface..b10550cd2 100644 --- a/system/async/tasks/Scheduler.cfc +++ b/system/async/tasks/Scheduler.cfc @@ -66,6 +66,12 @@ component accessors="true" singleton { return this; } + /** + * Usually where concrete implementations add their tasks and configs + */ + function configure(){ + } + /** * Set the timezone for all tasks to use using the timezone string identifier * diff --git a/system/web/Controller.cfc b/system/web/Controller.cfc index 18f749b64..180de253d 100644 --- a/system/web/Controller.cfc +++ b/system/web/Controller.cfc @@ -76,7 +76,7 @@ component serializable="false" accessors="true" { function init( required appRootPath, appKey = "cbController" ){ // These will be lazy loaded on first use since the framework isn't ready to create it yet variables.renderer = ""; - variables.wireBox = ""; + variables.wireBox = ""; // Create Utility variables.util = new coldbox.system.core.util.Util(); @@ -112,7 +112,6 @@ component serializable="false" accessors="true" { // LogBox Default Configuration & Creation variables.logBox = services.loaderService.createDefaultLogBox(); variables.log = variables.logBox.getLogger( this ); - variables.log.info( "+ LogBox created" ); // Setup the ColdBox Services @@ -121,6 +120,7 @@ component serializable="false" accessors="true" { services.handlerService = new coldbox.system.web.services.HandlerService( this ); services.routingService = new coldbox.system.web.services.RoutingService( this ); services.moduleService = new coldbox.system.web.services.ModuleService( this ); + services.schedulerService = new coldbox.system.web.services.SchedulerService( this ); variables.log.info( "+ ColdBox services created" ); @@ -221,6 +221,13 @@ component serializable="false" accessors="true" { return services.routingService; } + /** + * Get the scheduling service + */ + function getSchedulerService(){ + return services.schedulerService; + } + /**************************************************************** * Setting Methods * ****************************************************************/ @@ -362,8 +369,8 @@ component serializable="false" accessors="true" { if ( len( trim( arguments.event ) ) eq 0 ) { arguments.event = getSetting( "DefaultEvent" ); } - // Query String Struct to String - if( isStruct( arguments.queryString ) ){ + // Query String Struct to String + if ( isStruct( arguments.queryString ) ) { arguments.queryString = arguments.queryString .reduce( function( result, key, value ){ arguments.result.append( "#encodeForURL( arguments.key )#=#encodeForURL( arguments.value )#" ); @@ -422,19 +429,9 @@ component serializable="false" accessors="true" { // If the routestring ends with '/' we do not want to // double append '/' if ( right( routeString, 1 ) NEQ "/" ) { - routeString = routeString & "/" & replace( - arguments.queryString, - "&", - "/", - "all" - ); + routeString = routeString & "/" & replace( arguments.queryString, "&", "/", "all" ); } else { - routeString = routeString & replace( - arguments.queryString, - "&", - "/", - "all" - ); + routeString = routeString & replace( arguments.queryString, "&", "/", "all" ); } routeString = replace( routeString, "=", "/", "all" ); } @@ -590,10 +587,7 @@ component serializable="false" accessors="true" { ); } - throw( - type = "InvalidArgumentException", - message = "The named route '#arguments.name#' does not exist" - ); + throw( type = "InvalidArgumentException", message = "The named route '#arguments.name#' does not exist" ); } /** @@ -672,10 +666,7 @@ component serializable="false" accessors="true" { // Do action Rendering services.requestService .getContext() - .renderdata( - type = results.ehBean.getActionMetadata( "renderdata" ), - data = results.data - ); + .renderdata( type = results.ehBean.getActionMetadata( "renderdata" ), data = results.data ); } // Are we caching @@ -883,11 +874,9 @@ component serializable="false" accessors="true" { // Verify if event was overridden if ( arguments.defaultEvent and arguments.event NEQ oRequestContext.getCurrentEvent() ) { // Validate the overridden event - results.ehBean = services.handlerService.getHandlerBean( - oRequestContext.getCurrentEvent() - ); + results.ehBean = services.handlerService.getHandlerBean( oRequestContext.getCurrentEvent() ); // Get new handler to follow execution - oHandler = services.handlerService.getHandler( results.ehBean, oRequestContext ); + oHandler = services.handlerService.getHandler( results.ehBean, oRequestContext ); } // Invoke onMissingAction event @@ -1108,12 +1097,7 @@ component serializable="false" accessors="true" { ){ if ( ( - ( - len( arguments.inclusion ) AND listFindNoCase( - arguments.inclusion, - arguments.action - ) - ) + ( len( arguments.inclusion ) AND listFindNoCase( arguments.inclusion, arguments.action ) ) OR ( NOT len( arguments.inclusion ) ) ) diff --git a/system/web/services/SchedulerService.cfc b/system/web/services/SchedulerService.cfc index 11ed17f93..a1c0fd1a1 100644 --- a/system/web/services/SchedulerService.cfc +++ b/system/web/services/SchedulerService.cfc @@ -1,9 +1,10 @@ /** - * This class manages Scheduler cfc's that represent collection of scheduled tasks. - * - * It can also be linked to a ColdBox instance to enhance the schedulers and tasks so they can work within a ColdBox context + * Copyright Since 2005 ColdBox Framework by Luis Majano and Ortus Solutions, Corp + * www.ortussolutions.com + * --- + * This service manages all schedulers in ColdBox in an HMVC fashion */ -component accessors="true" singleton { +component extends="coldbox.system.web.services.BaseService" accessors="true" { /** * -------------------------------------------------------------------------- @@ -17,49 +18,105 @@ component accessors="true" singleton { property name="schedulers" type="struct"; /** - * The async manager link + * Constructor */ - property name="asyncManager"; + function init( required controller ){ + variables.controller = arguments.controller; + // Register a fresh collection of schedulers + variables.schedulers = structNew( "ordered" ); + + return this; + } /** - * Constructor - * - * @asyncManager The async manager we are linked to + * Once configuration loads prepare for operation */ - function init( required asyncManager ){ - // The async manager - variables.asyncManager = arguments.asyncManager; - // The collection of tasks we will run - variables.schedulers = structNew( "ordered" ); + function onConfigurationLoad(){ + // Prepare references for faster access + variables.log = variables.controller.getLogBox().getLogger( this ); + variables.interceptorService = variables.controller.getInterceptorService(); + variables.wirebox = variables.controller.getWireBox(); + variables.appMapping = variables.controller.getSetting( "AppMapping" ); + variables.appPath = variables.controller.getSetting( "applicationPath" ); + // Load up the global app scheduler + loadGlobalScheduler(); + } - return this; + /** + * Process a ColdBox service shutdown + */ + function onShutdown(){ + variables.schedulers.each( function( name, thisScheduler ){ + variables.log.info( "† Shutting down Scheduler (#arguments.name#)..." ); + arguments.thisScheduler.shutdown(); + } ); + } + + /** + * Load the application's global scheduler + */ + function loadGlobalScheduler(){ + var appScheduler = "config.Scheduler"; + var baseScheduler = "coldbox.system.web.tasks.ColdBoxScheduler"; + var schedulerName = "appScheduler@coldbox"; + + // Check if base scheduler has been mapped? + if ( NOT variables.wirebox.getBinder().mappingExists( baseScheduler ) ) { + // feed the base class + variables.wirebox + .registerNewInstance( name = baseScheduler, instancePath = baseScheduler ) + .addDIConstructorArgument( name = "name", value = "baseScheduler" ); + } + + // Check if the convetion exists, else just build out a simple scheduler + if ( fileExists( appPath & "config/Scheduler.cfc" ) ) { + // Log it + variables.log.info( "Loading App ColdBox Task Scheduler at => config/Scheduler.cfc" ); + var appSchedulerPath = ( + variables.appMapping.len() ? "#variables.appMapping#.#appScheduler#" : appScheduler + ); + // Process as a Scheduler.cfc with virtual inheritance + wirebox + .registerNewInstance( name = schedulerName, instancePath = appSchedulerPath ) + .setVirtualInheritance( baseScheduler ) + .setThreadSafe( true ) + .setScope( variables.wirebox.getBinder().SCOPES.SINGLETON ) + .addDIConstructorArgument( name = "name", value = schedulerName ); + } + // Load up a base scheduler + else { + // Log it + variables.log.info( "Loading Base ColdBox Task Scheduler" ); + // Register scheduler with WireBox + variables.wirebox + .registerNewInstance( name = schedulerName, instancePath = baseScheduler ) + .setThreadSafe( true ) + .setScope( variables.wirebox.getBinder().SCOPES.SINGLETON ) + .addDIConstructorArgument( name = "name", value = schedulerName ); + } + + // Create, register, configure it and start it up baby! + var appScheduler = registerScheduler( variables.wirebox.getInstance( schedulerName ) ); + // Register the Scheduler as an Interceptor as well. + variables.controller.getInterceptorService().registerInterceptor( interceptorObject = appScheduler ); + // Configure it + appScheduler.configure(); + // Start it up + appScheduler.startup(); } /** - * Register a new scheduler in this manager by name and cfc instantiation path + * Register a new scheduler in this manager using the scheduler name * - * @name The name of the scheduler - * @path The instantiation path to the cfc that represents the scheduler or empty to use the default core scheduler class + * @scheduler The scheduler object to register in the service * - * @return The created and registered scheduler Object + * @return The registered scheduler Object: coldbox.system.web.tasks.ColdBoxScheduler */ - Scheduler function registerScheduler( required name, path = "" ){ - // Build it - var oScheduler = ( - variables.asyncManager.hasColdBox() ? buildColdBoxScheduler( argumentCollection = arguments ) : buildSimpleScheduler( - argumentCollection = arguments - ) - ); + function registerScheduler( required scheduler ){ // Register it - variables.schedulers[ arguments.name ] = oScheduler; + variables.schedulers[ arguments.scheduler.getName() ] = arguments.scheduler; // Return it - return oScheduler; - } - - private function buildSimpleScheduler( required name, required path ){ - } - - private function buildColdBoxScheduler( required name, required path ){ + return arguments.scheduler; } /** @@ -84,7 +141,6 @@ component accessors="true" singleton { structDelete( variables.schedulers, arguments.name ); return true; } - return false; } diff --git a/system/web/tasks/ColdBoxScheduler.cfc b/system/web/tasks/ColdBoxScheduler.cfc index 94a69a431..917559b46 100644 --- a/system/web/tasks/ColdBoxScheduler.cfc +++ b/system/web/tasks/ColdBoxScheduler.cfc @@ -55,7 +55,7 @@ component required controller ){ // Super init - super.init( argumentCollection = arguments ); + super.init( arguments.name, arguments.asyncManager ); // Controller variables.controller = arguments.controller; // Register Log object @@ -89,10 +89,31 @@ component // Set default timezone into the task .setTimezone( getTimezone().getId() ); - // Super init - super.init( arguments.name ); - // Overwrite task object with ColdBox one - variables.tasks[ arguments.name ].task = oColdBoxTask; + // Register the task by name + variables.tasks[ arguments.name ] = { + // task name + "name" : arguments.name, + // task object + "task" : oColdBoxTask, + // task scheduled future object + "future" : "", + // when it registers + "registeredAt" : now(), + // when it's scheduled + "scheduledAt" : "", + // Tracks if the task has been disabled for startup purposes + "disabled" : false, + // If there is an error scheduling the task + "error" : false, + // Any error messages when scheduling + "errorMessage" : "", + // The exception stacktrace if something went wrong scheduling the task + "stacktrace" : "", + // Server Host + "inetHost" : variables.util.discoverInetHost(), + // Server IP + "localIp" : variables.util.getServerIp() + }; return oColdBoxTask; } diff --git a/test-harness/config/Scheduler.cfc b/test-harness/config/Scheduler.cfc index 8e219d15d..27a2e3840 100644 --- a/test-harness/config/Scheduler.cfc +++ b/test-harness/config/Scheduler.cfc @@ -1,18 +1,28 @@ component { - property name="userService" inject="UserService"; - function configure(){ - task( "vistacaballo-notifications" ) - .call( () => runEvent( "tasks.sendNotifications" ) ) - .dailyAt( "9:00" ) - .environments( [ "staging", "production"] ) - .before( ( task ) => notifyJorge ) - .after( ( task, results ) => notifyJorge ) - .onFailure( ( task, exception ) => {} ) - .onSuccess( ( task, results ) => {} ) - .onOneServer(); + task( "testharness-Heartbeat" ) + .call( function() { + if ( randRange(1, 5) eq 1 ){ + throw( message = "I am throwing up randomly!", type="RandomThrowup" ); + } + writeDump( var='====> I am in a test harness test schedule!', output="console" ); + } ) + .every( "5", "seconds" ) + .before( function( task ) { + writeDump( var='====> Running before the task!', output="console" ); + } ) + .after( function( task, results ){ + writeDump( var='====> Running after the task!', output="console" ); + } ) + .onFailure( function( task, exception ){ + writeDump( var='====> test schedule just failed!! #exception.message#', output="console" ); + } ) + .onSuccess( function( task, results ){ + writeDump( var='====> Test scheduler success!!', output="console" ); + writeDump( var="====> Stats: #task.getStats().toString()#", output="console" ); + } ); } From 7f72c75182afd42c7e4cb6b757bfe4fdce98ddd5 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Wed, 14 Apr 2021 11:27:00 -0500 Subject: [PATCH 18/40] COLDBOX-999 HMVC Support for app schedulers completed --- system/web/services/ModuleService.cfc | 150 ++++++++---------- system/web/services/SchedulerService.cfc | 74 +++++---- system/web/tasks/ColdBoxScheduledTask.cfc | 5 +- test-harness/config/Scheduler.cfc | 3 +- test-harness/models/PhotosService.cfc | 4 + .../resourcesTest/config/Scheduler.cfc | 19 ++- 6 files changed, 127 insertions(+), 128 deletions(-) diff --git a/system/web/services/ModuleService.cfc b/system/web/services/ModuleService.cfc index 403ca3d9d..e85f13f51 100755 --- a/system/web/services/ModuleService.cfc +++ b/system/web/services/ModuleService.cfc @@ -136,9 +136,7 @@ component extends="coldbox.system.web.services.BaseService" { } } - variables.logger.info( - "+ Registered All Modules in #numberFormat( getTickCount() - totalTime )# ms" - ); + variables.logger.info( "+ Registered All Modules in #numberFormat( getTickCount() - totalTime )# ms" ); // interception variables.interceptorService.announce( @@ -178,7 +176,7 @@ component extends="coldbox.system.web.services.BaseService" { boolean force = false ){ // Module To Load - var sTime = getTickCount(); + var sTime = getTickCount(); var modName = arguments.moduleName; var modulesConfiguration = controller.getSetting( "modules" ); var appSettings = controller.getConfigSettings(); @@ -187,9 +185,7 @@ component extends="coldbox.system.web.services.BaseService" { // Check if incoming invocation path is sent, if so, register as new module if ( len( arguments.invocationPath ) ) { // Check if passed module name is already registered - if ( - structKeyExists( variables.moduleRegistry, arguments.moduleName ) AND !arguments.force - ) { + if ( structKeyExists( variables.moduleRegistry, arguments.moduleName ) AND !arguments.force ) { variables.logger.info( "> The module #arguments.moduleName# has already been registered, so skipping registration" ); @@ -197,10 +193,8 @@ component extends="coldbox.system.web.services.BaseService" { } // register new incoming location variables.moduleRegistry[ arguments.moduleName ] = { - locationPath : "/" & replace( arguments.invocationPath, ".", "/", "all" ), - physicalPath : expandPath( - "/" & replace( arguments.invocationPath, ".", "/", "all" ) - ), + locationPath : "/" & replace( arguments.invocationPath, ".", "/", "all" ), + physicalPath : expandPath( "/" & replace( arguments.invocationPath, ".", "/", "all" ) ), invocationPath : arguments.invocationPath }; } @@ -329,21 +323,31 @@ component extends="coldbox.system.web.services.BaseService" { interceptors : [], interceptorSettings : { customInterceptionPoints : "" }, layoutSettings : { defaultLayout : "" }, + // Routing + resources routes : [], resources : [], + // Module Conventions conventions : { - handlersLocation : "handlers", - layoutsLocation : "layouts", - viewsLocation : "views", - modelsLocation : "models", - includesLocation : "includes", - routerLocation : "config.Router" + handlersLocation : "handlers", + layoutsLocation : "layouts", + viewsLocation : "views", + modelsLocation : "models", + includesLocation : "includes", + routerLocation : "config.Router", + schedulerLocation : "config.Scheduler" }, - childModules : [], - parent : arguments.parent, - router : "", - routerInvocationPath : modulesInvocationPath & "." & modName, - routerPhysicalPath : modLocation + // My Children + childModules : [], + // My Daddy! + parent : arguments.parent, + // Module Router + router : "", + routerInvocationPath : modulesInvocationPath & "." & modName, + routerPhysicalPath : modLocation, + // Task Scheduler + scheduler : "", + schedulerInvocationPath : modulesInvocationPath & "." & modName, + schedulerPhysicalpath : modLocation }; // Load Module configuration from cfc and store it in module Config Cache @@ -352,9 +356,7 @@ component extends="coldbox.system.web.services.BaseService" { // Verify if module has been disabled if ( mConfig.disabled ) { if ( variables.logger.canInfo() ) { - variables.logger.info( - "> Skipping module: #arguments.moduleName# as it has been disabled!" - ); + variables.logger.info( "> Skipping module: #arguments.moduleName# as it has been disabled!" ); } return false; } else { @@ -385,8 +387,12 @@ component extends="coldbox.system.web.services.BaseService" { "all" )#"; mConfig.modelsPhysicalPath &= "/#mConfig.conventions.modelsLocation#"; + // Router mConfig.routerInvocationPath &= ".#mConfig.conventions.routerLocation#"; mConfig.routerPhysicalPath &= "/#mConfig.conventions.routerLocation.replace( ".", "/", "all" )#.cfc"; + // Scheduler + mConfig.schedulerInvocationPath &= ".#mConfig.conventions.schedulerLocation#"; + mConfig.schedulerPhysicalPath &= "/#mConfig.conventions.schedulerLocation.replace( ".", "/", "all" )#.cfc"; // Register CFML Mapping if it exists, for loading purposes if ( len( trim( mConfig.cfMapping ) ) ) { @@ -432,7 +438,8 @@ component extends="coldbox.system.web.services.BaseService" { } } } - } // end inception loading + } + // end inception loading // Log Registration Time mConfig.registrationTime = getTickCount() - sTime; @@ -479,9 +486,7 @@ component extends="coldbox.system.web.services.BaseService" { } } - variables.logger.info( - "+ Activated All Modules in #numberFormat( getTickCount() - totalTime )# ms" - ); + variables.logger.info( "+ Activated All Modules in #numberFormat( getTickCount() - totalTime )# ms" ); // interception variables.interceptorService.announce( @@ -500,24 +505,22 @@ component extends="coldbox.system.web.services.BaseService" { * @return The Service */ ModuleService function activateModule( required moduleName ){ - var sTime = getTickCount(); + var sTime = getTickCount(); var modules = variables.registeredModules; // If module not registered, throw exception if ( isNull( modules[ arguments.moduleName ] ) ) { throw( - message : "Cannot activate module: #arguments.moduleName#. Already processed #structKeyList( modules )#", - detail : "The module has not been registered, register the module first and then activate it.", - type : "IllegalModuleState" + message: "Cannot activate module: #arguments.moduleName#. Already processed #structKeyList( modules )#", + detail : "The module has not been registered, register the module first and then activate it.", + type : "IllegalModuleState" ); } // Check if module already activated if ( modules[ arguments.moduleName ].activated ) { // Log it - variables.logger.debug( - "==> Module '#arguments.moduleName#' already activated, skipping activation." - ); + variables.logger.debug( "==> Module '#arguments.moduleName#' already activated, skipping activation." ); return this; } @@ -535,9 +538,7 @@ component extends="coldbox.system.web.services.BaseService" { // Do we have dependencies to activate first mConfig.dependencies.each( function( thisDependency ){ - variables.logger.debug( - "==> Activating '#moduleName#' dependency: #thisDependency#" - ); + variables.logger.debug( "==> Activating '#moduleName#' dependency: #thisDependency#" ); // Activate dependency first activateModule( thisDependency ); } ); @@ -594,10 +595,7 @@ component extends="coldbox.system.web.services.BaseService" { ); } else { // just register with no namespace - binder.mapDirectory( - packagePath = packagePath, - process = mConfig.autoProcessModels - ); + binder.mapDirectory( packagePath = packagePath, process = mConfig.autoProcessModels ); } // Register Default Module Export if it exists as @moduleName, so you can do getInstance( "@moduleName" ) @@ -642,11 +640,7 @@ component extends="coldbox.system.web.services.BaseService" { } // Store Inherited Entry Point - mConfig.inheritedEntryPoint = parentEntryPoint & reReplace( - mConfig.entryPoint, - "^/", - "" - ); + mConfig.inheritedEntryPoint = parentEntryPoint & reReplace( mConfig.entryPoint, "^/", "" ); // Register Module Routing Entry Point + Struct Literals for routes and resources appRouter.addModuleRoutes( @@ -681,9 +675,7 @@ component extends="coldbox.system.web.services.BaseService" { var conventionsRouteExists = mConfig.router .getRoutes() .find( function( item ){ - return ( - item.pattern == "/:handler/:action?" || item.pattern == ":handler/:action?" - ); + return ( item.pattern == "/:handler/:action?" || item.pattern == ":handler/:action?" ); } ); if ( conventionsRouteExists == 0 ) { mConfig.router.route( "/:handler/:action?" ).end(); @@ -716,14 +708,20 @@ component extends="coldbox.system.web.services.BaseService" { // Register Executors if any are registered mConfig.executors.each( function( key, config ){ arguments.config.name = arguments.key; - variables.controller - .getAsyncManager() - .newExecutor( argumentCollection = arguments.config ); - variables.logger.info( - "+ Registered Module (#moduleName#) Executor: #arguments.key#" - ); + variables.controller.getAsyncManager().newExecutor( argumentCollection = arguments.config ); + variables.logger.info( "+ Registered Module (#moduleName#) Executor: #arguments.key#" ); } ); + // Register Scheduler if it exists as scheduler@moduleName + if ( fileExists( mConfig.schedulerPhysicalPath ) ) { + mConfig.scheduler = variables.controller + .getSchedulerService() + .loadScheduler( + name: "cbScheduler@#arguments.moduleName#", + path: mConfig.schedulerInvocationPath + ); + } + // Call on module configuration object onLoad() if found if ( structKeyExists( variables.mConfigCache[ arguments.moduleName ], "onLoad" ) ) { variables.mConfigCache[ arguments.moduleName ].onLoad(); @@ -751,7 +749,9 @@ component extends="coldbox.system.web.services.BaseService" { ); // Log it - variables.logger.info( "+ Module (#arguments.moduleName#) Activated (#mConfig.activationTime#ms) => { version: #mConfig.version#, from: #mConfig.path# }" ); + variables.logger.info( + "+ Module (#arguments.moduleName#) Activated (#mConfig.activationTime#ms) => { version: #mConfig.version#, from: #mConfig.path# }" + ); } // end lock @@ -836,10 +836,7 @@ component extends="coldbox.system.web.services.BaseService" { var mConfig = appConfig.modules[ arguments.moduleName ]; // Before unloading a module interception - variables.interceptorService.announce( - "preModuleUnload", - { moduleName : arguments.moduleName } - ); + variables.interceptorService.announce( "preModuleUnload", { moduleName : arguments.moduleName } ); // Call on module configuration object onLoad() if found if ( structKeyExists( variables.mConfigCache[ arguments.moduleName ], "onUnload" ) ) { @@ -854,17 +851,17 @@ component extends="coldbox.system.web.services.BaseService" { } } + // Unregister scheduler if loaded + if ( isObject( mConfig.scheduler ) ) { + variables.controller.getSchedulerService().removeScheduler( mConfig.scheduler.getName() ); + } + // Unregister app Helpers if ( arrayLen( mConfig.applicationHelper ) ) { controller.setSetting( "applicationHelper", arrayFilter( controller.getSetting( "applicationHelper" ), function( helper ){ - return ( - !arrayFindNoCase( - appConfig.modules[ moduleName ].applicationHelper, - helper - ) - ); + return ( !arrayFindNoCase( appConfig.modules[ moduleName ].applicationHelper, helper ) ); } ) ); } @@ -879,9 +876,7 @@ component extends="coldbox.system.web.services.BaseService" { // Remove SES if enabled. if ( controller.settingExists( "sesBaseURL" ) ) { - variables.wirebox - .getInstance( "router@coldbox" ) - .removeModuleRoutes( arguments.moduleName ); + variables.wirebox.getInstance( "router@coldbox" ).removeModuleRoutes( arguments.moduleName ); } // Remove executors @@ -896,10 +891,7 @@ component extends="coldbox.system.web.services.BaseService" { structDelete( variables.mConfigCache, arguments.moduleName ); // After unloading a module interception - variables.interceptorService.announce( - "postModuleUnload", - { moduleName : arguments.moduleName } - ); + variables.interceptorService.announce( "postModuleUnload", { moduleName : arguments.moduleName } ); // Log it if ( variables.logger.canInfo() ) { @@ -941,9 +933,7 @@ component extends="coldbox.system.web.services.BaseService" { var appSettings = controller.getConfigSettings(); // Build a new router for this module so we can track its routes - arguments.config.router = variables.wirebox.getInstance( - "coldbox.system.web.routing.Router" - ); + arguments.config.router = variables.wirebox.getInstance( "coldbox.system.web.routing.Router" ); // MixIn Variables Scope oConfig @@ -1023,9 +1013,7 @@ component extends="coldbox.system.web.services.BaseService" { // Dependencies if ( structKeyExists( oConfig, "dependencies" ) ) { // set it always as an array - mConfig.dependencies = isSimpleValue( oConfig.dependencies ) ? listToArray( - oConfig.dependencies - ) : oConfig.dependencies; + mConfig.dependencies = isSimpleValue( oConfig.dependencies ) ? listToArray( oConfig.dependencies ) : oConfig.dependencies; } // Application Helpers if ( structKeyExists( oConfig, "applicationHelper" ) ) { diff --git a/system/web/services/SchedulerService.cfc b/system/web/services/SchedulerService.cfc index a1c0fd1a1..685a8858a 100644 --- a/system/web/services/SchedulerService.cfc +++ b/system/web/services/SchedulerService.cfc @@ -38,6 +38,7 @@ component extends="coldbox.system.web.services.BaseService" accessors="true" { variables.wirebox = variables.controller.getWireBox(); variables.appMapping = variables.controller.getSetting( "AppMapping" ); variables.appPath = variables.controller.getSetting( "applicationPath" ); + variables.baseScheduler = "coldbox.system.web.tasks.ColdBoxScheduler"; // Load up the global app scheduler loadGlobalScheduler(); } @@ -56,53 +57,58 @@ component extends="coldbox.system.web.services.BaseService" accessors="true" { * Load the application's global scheduler */ function loadGlobalScheduler(){ - var appScheduler = "config.Scheduler"; - var baseScheduler = "coldbox.system.web.tasks.ColdBoxScheduler"; - var schedulerName = "appScheduler@coldbox"; + var appSchedulerConvention = "config.Scheduler"; + var schedulerName = "appScheduler@coldbox"; + var schedulerPath = variables.baseScheduler; // Check if base scheduler has been mapped? - if ( NOT variables.wirebox.getBinder().mappingExists( baseScheduler ) ) { + if ( NOT variables.wirebox.getBinder().mappingExists( variables.baseScheduler ) ) { // feed the base class variables.wirebox - .registerNewInstance( name = baseScheduler, instancePath = baseScheduler ) - .addDIConstructorArgument( name = "name", value = "baseScheduler" ); + .registerNewInstance( name = variables.baseScheduler, instancePath = variables.baseScheduler ) + .addDIConstructorArgument( name = "name", value = "variables.baseScheduler" ); } - // Check if the convetion exists, else just build out a simple scheduler - if ( fileExists( appPath & "config/Scheduler.cfc" ) ) { - // Log it - variables.log.info( "Loading App ColdBox Task Scheduler at => config/Scheduler.cfc" ); - var appSchedulerPath = ( - variables.appMapping.len() ? "#variables.appMapping#.#appScheduler#" : appScheduler + // Check if the convention exists, else just build out a simple scheduler + if ( fileExists( variables.appPath & "config/Scheduler.cfc" ) ) { + schedulerPath = ( + variables.appMapping.len() ? "#variables.appMapping#.#appSchedulerConvention#" : appSchedulerConvention ); - // Process as a Scheduler.cfc with virtual inheritance - wirebox - .registerNewInstance( name = schedulerName, instancePath = appSchedulerPath ) - .setVirtualInheritance( baseScheduler ) - .setThreadSafe( true ) - .setScope( variables.wirebox.getBinder().SCOPES.SINGLETON ) - .addDIConstructorArgument( name = "name", value = schedulerName ); - } - // Load up a base scheduler - else { - // Log it - variables.log.info( "Loading Base ColdBox Task Scheduler" ); - // Register scheduler with WireBox - variables.wirebox - .registerNewInstance( name = schedulerName, instancePath = baseScheduler ) - .setThreadSafe( true ) - .setScope( variables.wirebox.getBinder().SCOPES.SINGLETON ) - .addDIConstructorArgument( name = "name", value = schedulerName ); } + // Load, create, register and activate + loadScheduler( schedulerName, schedulerPath ); + } + + /** + * Load a scheduler cfc by path and name, usually this is called from module services or ways to register + * a-la-carte schedulers + * + * @name The name to register the scheduler with + * @path The path to instantiate the scheduler cfc + * + * @return The created, configured, registered, and activated scheduler + */ + function loadScheduler( required name, required path ){ + // Log it + variables.log.info( "Loading ColdBox Task Scheduler (#arguments.name#) at => #arguments.path#..." ); + // Process as a Scheduler.cfc with virtual inheritance + wirebox + .registerNewInstance( name = arguments.name, instancePath = arguments.path ) + .setVirtualInheritance( variables.baseScheduler ) + .setThreadSafe( true ) + .setScope( variables.wirebox.getBinder().SCOPES.SINGLETON ) + .addDIConstructorArgument( name = "name", value = arguments.name ); // Create, register, configure it and start it up baby! - var appScheduler = registerScheduler( variables.wirebox.getInstance( schedulerName ) ); + var oScheduler = registerScheduler( variables.wirebox.getInstance( arguments.name ) ); // Register the Scheduler as an Interceptor as well. - variables.controller.getInterceptorService().registerInterceptor( interceptorObject = appScheduler ); + variables.controller.getInterceptorService().registerInterceptor( interceptorObject = oScheduler ); // Configure it - appScheduler.configure(); + oScheduler.configure(); // Start it up - appScheduler.startup(); + oScheduler.startup(); + // Return it + return oScheduler; } /** diff --git a/system/web/tasks/ColdBoxScheduledTask.cfc b/system/web/tasks/ColdBoxScheduledTask.cfc index f4f439790..0a252a4d6 100644 --- a/system/web/tasks/ColdBoxScheduledTask.cfc +++ b/system/web/tasks/ColdBoxScheduledTask.cfc @@ -72,7 +72,7 @@ component extends="coldbox.system.async.tasks.ScheduledTask" accessors="true" { * * @environment A string, a list, or an array of environments */ - ColdBoxScheduledTask function setEnvironments( required environment ){ + ColdBoxScheduledTask function onEnvironment( required environment ){ if ( isSimpleValue( arguments.environment ) ) { arguments.environment = listToArray( arguments.environment ); } @@ -110,6 +110,9 @@ component extends="coldbox.system.async.tasks.ScheduledTask" accessors="true" { variables.controller.getSetting( "environment" ) ) ) { + variables.log.info( + "Skipping task (#getName()#) as it is disabled in the current environment: #variables.controller.getSetting( "environment" )#" + ); return true; } diff --git a/test-harness/config/Scheduler.cfc b/test-harness/config/Scheduler.cfc index 27a2e3840..a04a0981d 100644 --- a/test-harness/config/Scheduler.cfc +++ b/test-harness/config/Scheduler.cfc @@ -20,8 +20,7 @@ component { writeDump( var='====> test schedule just failed!! #exception.message#', output="console" ); } ) .onSuccess( function( task, results ){ - writeDump( var='====> Test scheduler success!!', output="console" ); - writeDump( var="====> Stats: #task.getStats().toString()#", output="console" ); + writeDump( var="====> Test scheduler success : Stats: #task.getStats().toString()#", output="console" ); } ); } diff --git a/test-harness/models/PhotosService.cfc b/test-harness/models/PhotosService.cfc index e5ec304b4..29feb54cc 100644 --- a/test-harness/models/PhotosService.cfc +++ b/test-harness/models/PhotosService.cfc @@ -37,4 +37,8 @@ component singleton accessors="true" { function get(){ } + function getRandom(){ + return randRange( 1, 1000 ); + } + } diff --git a/test-harness/modules/resourcesTest/config/Scheduler.cfc b/test-harness/modules/resourcesTest/config/Scheduler.cfc index 64e9d38cc..05a5322e1 100644 --- a/test-harness/modules/resourcesTest/config/Scheduler.cfc +++ b/test-harness/modules/resourcesTest/config/Scheduler.cfc @@ -1,18 +1,17 @@ component { - property name="userService" inject="UserService"; + property name="photosService" inject="PhotosService"; function configure(){ - task( "vistacaballo-notifications" ) - .call( () => runEvent( "tasks.sendNotifications" ) ) - .dailyAt( "9:00" ) - .environments( [ "staging", "production"] ) - .before( ( task ) => notifyJorge ) - .after( ( results ) => notifyJorge ) - .onFailure( ( task, exception ) => {} ) - .onSuccess( ( task, results ) => {} ) - .onOneServer(); + task( "photoNumbers" ) + .call( function(){ + var random = variables.photosService.getRandom(); + writeDump( var="xxxxxxx> Photo numbers: #random#", output="console" ); + return random; + } ) + .every( 5, "seconds" ) + .onEnvironment( "development" ); } From 45fe84031d83fc59161ed902404ccc02e5349481 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Wed, 14 Apr 2021 11:39:42 -0500 Subject: [PATCH 19/40] global life cycle callbacks for schedulers --- system/async/tasks/ScheduledTask.cfc | 14 ++++++- system/async/tasks/Scheduler.cfc | 51 ++++++++++++++++++++++++-- system/web/tasks/ColdBoxScheduler.cfc | 2 + test-harness/config/Scheduler.cfc | 53 +++++++++++++++++++++++++++ 4 files changed, 116 insertions(+), 4 deletions(-) diff --git a/system/async/tasks/ScheduledTask.cfc b/system/async/tasks/ScheduledTask.cfc index ef3dd38a5..e58ec6155 100644 --- a/system/async/tasks/ScheduledTask.cfc +++ b/system/async/tasks/ScheduledTask.cfc @@ -257,7 +257,10 @@ component accessors="true" { variables.stats.neverRun = false; try { - // Before Interceptor + // Before Interceptors + if ( hasScheduler() ) { + getScheduler().beforeAnyTask( this ); + } if ( isClosure( variables.beforeTask ) ) { variables.beforeTask( this ); } @@ -273,12 +276,18 @@ component accessors="true" { if ( isClosure( variables.afterTask ) ) { variables.afterTask( this, variables.stats.lastResult ); } + if ( hasScheduler() ) { + getScheduler().afterAnyTask( this, variables.stats.lastResult ); + } // store successes and call success interceptor variables.stats.totalSuccess = variables.stats.totalSuccess + 1; if ( isClosure( variables.onTaskSuccess ) ) { variables.onTaskSuccess( this, variables.stats.lastResult ); } + if ( hasScheduler() ) { + getScheduler().onAnyTaskSuccess( this, variables.stats.lastResult ); + } } catch ( any e ) { // store failures variables.stats.totalFailures = variables.stats.totalFailures + 1; @@ -286,6 +295,9 @@ component accessors="true" { if ( isClosure( variables.onTaskFailure ) ) { variables.onTaskFailure( this, e ); } + if ( hasScheduler() ) { + getScheduler().onAnyTaskError( this, e ); + } } finally { // Store finalization stats variables.stats.lastRun = now(); diff --git a/system/async/tasks/Scheduler.cfc b/system/async/tasks/Scheduler.cfc index b10550cd2..7ee1f8f5e 100644 --- a/system/async/tasks/Scheduler.cfc +++ b/system/async/tasks/Scheduler.cfc @@ -95,6 +95,8 @@ component accessors="true" singleton { var oTask = variables.executor // Give me the task broda! .newTask( arguments.name ) + // Register ourselves in the task + .setScheduler( this ) // Set default timezone into the task .setTimezone( getTimezone().getId() ); @@ -129,7 +131,7 @@ component accessors="true" singleton { /** * -------------------------------------------------------------------------- - * Life - Cycle Methods + * Startup/Shutdown Methods * -------------------------------------------------------------------------- */ @@ -214,20 +216,63 @@ component accessors="true" singleton { return this; } + /** + * -------------------------------------------------------------------------- + * Life - Cycle Callbacks + * -------------------------------------------------------------------------- + */ + /** * Called before the scheduler is going to be shutdown - * @abstract */ function onShutdown(){ } /** * Called after the scheduler has registered all schedules - * @abstract */ function onStartup(){ } + /** + * Called whenever ANY task fails + * + * @task The task that got executed + * @exception The ColdFusion exception object + * + */ + function onAnyTaskError( required task, required exception ){ + } + + /** + * Called whenever ANY task succeeds + * + * @task The task that got executed + * @result The result (if any) that the task produced + * + */ + function onAnyTaskSuccess( required task, result ){ + } + + /** + * Called before ANY task runs + * + * @task The task about to be executed + * + */ + function beforeAnyTask( required task ){ + } + + /** + * Called after ANY task runs + * + * @task The task that got executed + * @result The result (if any) that the task produced + * + */ + function afterAnyTask( required task, result ){ + } + /** * -------------------------------------------------------------------------- * Utility Methods diff --git a/system/web/tasks/ColdBoxScheduler.cfc b/system/web/tasks/ColdBoxScheduler.cfc index 917559b46..57d7ce1d9 100644 --- a/system/web/tasks/ColdBoxScheduler.cfc +++ b/system/web/tasks/ColdBoxScheduler.cfc @@ -86,6 +86,8 @@ component "coldbox.system.web.tasks.ColdBoxScheduledTask", { name : arguments.name, executor : variables.executor } ) + // Set ourselves into the task + .setScheduler( this ) // Set default timezone into the task .setTimezone( getTimezone().getId() ); diff --git a/test-harness/config/Scheduler.cfc b/test-harness/config/Scheduler.cfc index a04a0981d..7500d26c3 100644 --- a/test-harness/config/Scheduler.cfc +++ b/test-harness/config/Scheduler.cfc @@ -25,4 +25,57 @@ component { } + /** + * Called before the scheduler is going to be shutdown + */ + function onShutdown(){ + writeDump( var="Bye bye from the Global App Scheduler!", output="console" ); + } + + /** + * Called after the scheduler has registered all schedules + */ + function onStartup(){ + writeDump( var="The App Scheduler is in da house!!!!!", output="console" ); + } + + /** + * Called whenever ANY task fails + * + * @task The task that got executed + * @exception The ColdFusion exception object + */ + function onAnyTaskError( required task, required exception ){ + writeDump( var="the #arguments.task.getname()# task just went kabooooooom!", output="console" ); + } + + /** + * Called whenever ANY task succeeds + * + * @task The task that got executed + * @result The result (if any) that the task produced + */ + function onAnyTaskSuccess( required task, result ){ + writeDump( var="the #arguments.task.getname()# task completed!", output="console" ); + } + + /** + * Called before ANY task runs + * + * @task The task about to be executed + */ + function beforeAnyTask( required task ){ + writeDump( var="I am running before the task: #arguments.task.getName()#", output="console" ); + } + + /** + * Called after ANY task runs + * + * @task The task that got executed + * @result The result (if any) that the task produced + */ + function afterAnyTask( required task, result ){ + writeDump( var="I am running after the task: #arguments.task.getName()#", output="console" ); + } + } \ No newline at end of file From 13b69de25eccbcfd6bb03097f7f3ee06de6311ed Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Wed, 14 Apr 2021 14:50:56 -0500 Subject: [PATCH 20/40] ACF Incompatibilities with code syntax --- system/core/util/Util.cfc | 6 +++--- system/web/tasks/ColdBoxScheduledTask.cfc | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/system/core/util/Util.cfc b/system/core/util/Util.cfc index 0170ad57e..46e836ac0 100644 --- a/system/core/util/Util.cfc +++ b/system/core/util/Util.cfc @@ -127,10 +127,10 @@ component { /** * Convert an array to struct argument notation * - * @in The array to convert + * @target The array to convert */ - struct function arrayToStruct( required array in ){ - return arguments.in.reduce( function( result, item, index ){ + struct function arrayToStruct( required array target ){ + return arguments.target.reduce( function( result, item, index ){ arguments.result[ arguments.index ] = arguments.item; return arguments.result; }, {} ); diff --git a/system/web/tasks/ColdBoxScheduledTask.cfc b/system/web/tasks/ColdBoxScheduledTask.cfc index 0a252a4d6..4f02f6abf 100644 --- a/system/web/tasks/ColdBoxScheduledTask.cfc +++ b/system/web/tasks/ColdBoxScheduledTask.cfc @@ -106,7 +106,8 @@ component extends="coldbox.system.async.tasks.ScheduledTask" accessors="true" { // Environments Check if ( - variables.environments.len() && !variables.environments.containsNoCase( + variables.environments.len() && !arrayContainsNoCase( + variables.environments, variables.controller.getSetting( "environment" ) ) ) { From 0bb2aabfb76bef769c72bf2a663d19076cc2cb49 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Wed, 14 Apr 2021 15:51:59 -0500 Subject: [PATCH 21/40] left dump, removing it --- system/ioc/Builder.cfc | 2 -- 1 file changed, 2 deletions(-) diff --git a/system/ioc/Builder.cfc b/system/ioc/Builder.cfc index d8c7bcc95..9cdeeb400 100644 --- a/system/ioc/Builder.cfc +++ b/system/ioc/Builder.cfc @@ -197,8 +197,6 @@ component serializable="false" accessors="true" { constructorArgCollection ); } catch ( any e ) { - writeDump( var = e ); - abort; var reducedTagContext = e.tagContext .reduce( function( result, file ){ if ( !result.done ) { From 6b1c07e666d565c1a38b20759ed11723ea341dd7 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Wed, 14 Apr 2021 18:10:28 -0500 Subject: [PATCH 22/40] tons of frequency selectors completed --- system/async/tasks/ScheduledTask.cfc | 311 ++++++++++++++++-- system/async/time/ChronoUnit.cfc | 14 + system/web/tasks/ColdBoxScheduledTask.cfc | 2 +- tests/index.cfm | 2 +- tests/specs/async/tasks/ScheduledTaskSpec.cfc | 150 +++++++++ 5 files changed, 451 insertions(+), 28 deletions(-) create mode 100644 tests/specs/async/tasks/ScheduledTaskSpec.cfc diff --git a/system/async/tasks/ScheduledTask.cfc b/system/async/tasks/ScheduledTask.cfc index e58ec6155..28ba24120 100644 --- a/system/async/tasks/ScheduledTask.cfc +++ b/system/async/tasks/ScheduledTask.cfc @@ -22,6 +22,11 @@ component accessors="true" { */ property name="spacedDelay" type="numeric"; + /** + * The time unit string used to schedule the task + */ + property name="timeunit"; + /** * The task closure or CFC to execute in the task */ @@ -63,6 +68,26 @@ component accessors="true" { */ property name="stats" type="struct"; + /** + * The before task closure + */ + property name="beforeTask"; + + /** + * The after task closure + */ + property name="afterTask"; + + /** + * The task success closure + */ + property name="onTaskSuccess"; + + /** + * The task failure closure + */ + property name="onTaskFailure"; + /** * Constructor * @@ -78,29 +103,31 @@ component accessors="true" { method = "run" ){ // Utility class - variables.util = new coldbox.system.core.util.Util(); + variables.util = new coldbox.system.core.util.Util(); // Link up the executor and name - variables.executor = arguments.executor; - variables.name = arguments.name; + variables.executor = arguments.executor; + variables.name = arguments.name; // time unit helper - variables.chronoUnit = new coldbox.system.async.time.ChronoUnit(); + variables.chronoUnitHelper = new coldbox.system.async.time.ChronoUnit(); + variables.timeUnitHelper = new coldbox.system.async.time.TimeUnit(); // System Helper - variables.System = createObject( "java", "java.lang.System" ); + variables.System = createObject( "java", "java.lang.System" ); // Init Properties - variables.task = arguments.task; - variables.method = arguments.method; + variables.task = arguments.task; + variables.method = arguments.method; // Default Frequencies - variables.period = 0; - variables.delay = 0; - variables.spacedDelay = 0; - variables.timeUnit = "milliseconds"; + variables.period = 0; + variables.delay = 0; + variables.spacedDelay = 0; + variables.timeUnit = "milliseconds"; + variables.noOverlap = false; // Constraints - variables.disabled = false; - variables.when = ""; + variables.disabled = false; + variables.when = ""; // Probable Scheduler or not - variables.scheduler = ""; + variables.scheduler = ""; // Prepare execution tracking stats - variables.stats = { + variables.stats = { // When task got created "created" : now(), // The last execution run timestamp @@ -335,7 +362,7 @@ component accessors="true" { ); } - // Start off the one-off task + // Start off a one-off task return variables.executor.schedule( task : this, delay : variables.delay, @@ -402,7 +429,7 @@ component accessors="true" { * @delay The delay that will be used before executing the task * @timeUnit The time unit to use, available units are: days, hours, microseconds, milliseconds, minutes, nanoseconds, and seconds. The default is milliseconds */ - Scheduledtask function delay( numeric delay, timeUnit = "milliseconds" ){ + ScheduledTask function delay( numeric delay, timeUnit = "milliseconds" ){ variables.delay = arguments.delay; variables.timeUnit = arguments.timeUnit; return this; @@ -414,7 +441,7 @@ component accessors="true" { * @delay The delay that will be used before executing the task * @timeUnit The time unit to use, available units are: days, hours, microseconds, milliseconds, minutes, nanoseconds, and seconds. The default is milliseconds */ - Scheduledtask function spacedDelay( numeric spacedDelay, timeUnit = "milliseconds" ){ + ScheduledTask function spacedDelay( numeric spacedDelay, timeUnit = "milliseconds" ){ variables.spacedDelay = arguments.spacedDelay; variables.timeUnit = arguments.timeUnit; return this; @@ -426,16 +453,231 @@ component accessors="true" { * @period The period of execution * @timeUnit The time unit to use, available units are: days, hours, microseconds, milliseconds, minutes, nanoseconds, and seconds. The default is milliseconds */ - Scheduledtask function every( numeric period, timeUnit = "milliseconds" ){ + ScheduledTask function every( numeric period, timeUnit = "milliseconds" ){ variables.period = arguments.period; variables.timeUnit = arguments.timeUnit; return this; } + /** + * Set the period to be every minute from the time it get's scheduled + */ + ScheduledTask function everyMinute(){ + variables.period = 1; + variables.timeUnit = "minutes"; + return this; + } + + /** + * Set the period to be every hour from the time it get's scheduled + */ + ScheduledTask function everyHour(){ + variables.period = 1; + variables.timeUnit = "hours"; + return this; + } + + /** + * Set the period to be every day from the time it get's scheduled + */ + ScheduledTask function everyDay(){ + variables.period = 1; + variables.timeUnit = "days"; + return this; + } + + /** + * Set the period to be every week (7 days) from the time it get's scheduled + */ + ScheduledTask function everyWeek(){ + variables.period = 7; + variables.timeUnit = "days"; + return this; + } + + /** + * Set the period to be every month (30 days) from the time it get's scheduled + */ + ScheduledTask function everyMonth(){ + variables.period = 30; + variables.timeUnit = "days"; + return this; + } + + /** + * Set the period to be every year (365 days) from the time it get's scheduled + */ + ScheduledTask function everyYear(){ + variables.period = 365; + variables.timeUnit = "days"; + return this; + } + + /** + * Set the period to be hourly at a specific minute mark + * + * @minutes The minutes past the hour mark + */ + ScheduledTask function everyHourAt( required numeric minutes ){ + // Get times + var now = variables.chronoUnitHelper.toLocalDateTime( now() ); + var nextRun = now.withMinute( javacast( "int", arguments.minutes ) ); + // If we passed it, then move the hour by 1 + if ( now.compareTo( nextRun ) > 0 ) { + nextRun = nextRun.plusHours( javacast( "int", 1 ) ) + } + // Get the duration time for the next run and delay accordingly + this.delay( + variables.chronoUnitHelper + .duration() + .getNative() + .between( now, nextRun ) + .getSeconds(), + "seconds" + ); + // Set the period to every day in seconds + variables.period = variables.timeUnitHelper.get( "hours" ).toSeconds( 1 ); + variables.timeUnit = "seconds"; + + return this; + } + + /** + * Set the period to be daily at a specific time + * + * @time The specific time using 24 hour format => HH:mm + */ + ScheduledTask function everyDayAt( required string time ){ + // Check for mintues else add them + if ( !find( ":", arguments.time ) ) { + arguments.time &= ":00"; + } + // Validate it + validateTime( arguments.time ); + // Get times + var now = variables.chronoUnitHelper.toLocalDateTime( now() ); + var nextRun = now + .withHour( javacast( "int", getToken( arguments.time, 1, ":" ) ) ) + .withMinute( javacast( "int", getToken( arguments.time, 2, ":" ) ) ); + // If we passed it, then move the day + if ( now.compareTo( nextRun ) > 0 ) { + nextRun = nextRun.plusDays( javacast( "int", 1 ) ) + } + // Get the duration time for the next run and delay accordingly + this.delay( + variables.chronoUnitHelper + .duration() + .getNative() + .between( now, nextRun ) + .getSeconds(), + "seconds" + ); + // Set the period to every day in seconds + variables.period = variables.timeUnitHelper.get( "DAYS" ).toSeconds( 1 ); + variables.timeUnit = "seconds"; + + return this; + } + + /** + * Set the period to be weekly at a specific time at a specific day of the week + * + * @dayOfWeek The day of the week from 1 (Monday) -> 7 (Sunday) + * @time The specific time using 24 hour format => HH:mm + */ + ScheduledTask function everyWeekOn( required dayOfWeek, required string time ){ + return this; + } + + /** + * Set the period to be weekly at a specific time at a specific day of the week + * + * @day Which day of the month + * @time The specific time using 24 hour format => HH:mm + */ + ScheduledTask function everyMonthOn( required day, required string time ){ + return this; + } + + /** + * Set the period to be weekly at a specific time at a specific day of the week + * + * @month The month in numeric format 1-12 + * @day Which day of the month + * @time The specific time using 24 hour format => HH:mm, defaults to 00:00 + */ + ScheduledTask function everyYearOn( + required numeric month, + required numeric day, + required string time = "00:00" + ){ + return this; + } + + /** + * Set the period to be the first day of the month + * + * @time The specific time using 24 hour format => HH:mm, defaults to 00:00 + */ + ScheduledTask function firstDayOfTheMonth( string time = "00:00" ){ + return this; + } + + /** + * Set the period to be the first business day of the month + * + * @time The specific time using 24 hour format => HH:mm, defaults to 00:00 + */ + ScheduledTask function firstBusinessDayOfTheMonth( string time = "00:00" ){ + return this; + } + + /** + * Set the period to be the last day of the month + * + * @time The specific time using 24 hour format => HH:mm, defaults to 00:00 + */ + ScheduledTask function lastDayOfTheMonth( string time = "00:00" ){ + return this; + } + + /** + * Set the period to be the last business day of the month + * + * @time The specific time using 24 hour format => HH:mm, defaults to 00:00 + */ + ScheduledTask function lastBusinessDayOfTheMonth( string time = "00:00" ){ + return this; + } + + /** + * Set the period to be on saturday and sundays + * + * @time The specific time using 24 hour format => HH:mm, defaults to 00:00 + */ + ScheduledTask function weekends( string time = "00:00" ){ + return this; + } + + /** + * Set the period to be from Monday - Friday + * + * @time The specific time using 24 hour format => HH:mm, defaults to 00:00 + */ + ScheduledTask function weekdays( string time = "00:00" ){ + return this; + } + + /** + * -------------------------------------------------------------------------- + * TimeUnit Methods + * -------------------------------------------------------------------------- + */ + /** * Set the time unit in days */ - Scheduledtask function inDays(){ + ScheduledTask function inDays(){ variables.timeUnit = "days"; return this; } @@ -443,7 +685,7 @@ component accessors="true" { /** * Set the time unit in hours */ - Scheduledtask function inHours(){ + ScheduledTask function inHours(){ variables.timeUnit = "hours"; return this; } @@ -451,7 +693,7 @@ component accessors="true" { /** * Set the time unit in microseconds */ - Scheduledtask function inMicroseconds(){ + ScheduledTask function inMicroseconds(){ variables.timeUnit = "microseconds"; return this; } @@ -459,7 +701,7 @@ component accessors="true" { /** * Set the time unit in milliseconds */ - Scheduledtask function inMilliseconds(){ + ScheduledTask function inMilliseconds(){ variables.timeUnit = "milliseconds"; return this; } @@ -467,7 +709,7 @@ component accessors="true" { /** * Set the time unit in minutes */ - Scheduledtask function inMinutes(){ + ScheduledTask function inMinutes(){ variables.timeUnit = "minutes"; return this; } @@ -475,7 +717,7 @@ component accessors="true" { /** * Set the time unit in nanoseconds */ - Scheduledtask function inNanoseconds(){ + ScheduledTask function inNanoseconds(){ variables.timeUnit = "nanoseconds"; return this; } @@ -483,9 +725,26 @@ component accessors="true" { /** * Set the time unit in seconds */ - Scheduledtask function inSeconds(){ + ScheduledTask function inSeconds(){ variables.timeUnit = "seconds"; return this; } + /** + * Validates an incoming string to adhere to either: HH:mm + * + * @time The time to check + * + * @throws InvalidTimeException - If the time is invalid, else it just continues operation + */ + private function validateTime( required time ){ + // Regex check + if ( !reFind( "^[0-2][0-4]\:[0-5][0-9]$", arguments.time ) ) { + throw( + message = "Invalid time representation. Time is represented in 24 hour minute format => HH:mm", + type = "InvalidTimeException" + ); + } + } + } diff --git a/system/async/time/ChronoUnit.cfc b/system/async/time/ChronoUnit.cfc index b2ae81216..90279f122 100644 --- a/system/async/time/ChronoUnit.cfc +++ b/system/async/time/ChronoUnit.cfc @@ -140,4 +140,18 @@ component singleton { return arguments.target; } + /** + * Build out a new Duration class + */ + Duration function duration(){ + return new coldbox.system.async.time.Duration( argumentCollection = arguments ); + } + + /** + * Build out a new Period class + */ + Period function period(){ + return new coldbox.system.async.time.Period( argumentCollection = arguments ); + } + } diff --git a/system/web/tasks/ColdBoxScheduledTask.cfc b/system/web/tasks/ColdBoxScheduledTask.cfc index 4f02f6abf..880762151 100644 --- a/system/web/tasks/ColdBoxScheduledTask.cfc +++ b/system/web/tasks/ColdBoxScheduledTask.cfc @@ -49,7 +49,7 @@ component extends="coldbox.system.async.tasks.ScheduledTask" accessors="true" { * @task The closure or cfc that represents the task (optional) * @method The method on the cfc to call, defaults to "run" (optional) */ - ScheduledTask function init( + ColdBoxScheduledTask function init( required name, required executor, any task = "", diff --git a/tests/index.cfm b/tests/index.cfm index e27793c1a..2987b59fe 100644 --- a/tests/index.cfm +++ b/tests/index.cfm @@ -33,7 +33,7 @@ - + diff --git a/tests/specs/async/tasks/ScheduledTaskSpec.cfc b/tests/specs/async/tasks/ScheduledTaskSpec.cfc new file mode 100644 index 000000000..2ca9f1b39 --- /dev/null +++ b/tests/specs/async/tasks/ScheduledTaskSpec.cfc @@ -0,0 +1,150 @@ +component extends="tests.specs.async.BaseAsyncSpec" { + + /*********************************** BDD SUITES ***********************************/ + + function beforeAll(){ + variables.asyncManager = new coldbox.system.async.AsyncManager(); + super.beforeAll(); + } + + function run( testResults, testBox ){ + // all your suites go here. + describe( "Scheduled Task", function(){ + beforeEach( function( currentSpec ){ + variables.scheduler = asyncManager.newScheduler( "bdd-test" ); + } ); + + afterEach( function( currentSpec ){ + variables.scheduler.shutdown(); + } ); + + it( "can be created", function(){ + var t = scheduler.task( "test" ); + expect( t.getName() ).toBe( "test" ); + expect( t.hasScheduler() ).toBeTrue(); + } ); + + it( "can call when", function(){ + var t = scheduler + .task( "test" ) + .when( function(){ + return true; + } ); + expect( isClosure( t.getWhen() ) ).toBeTrue(); + } ); + + it( "can set a timezone", function(){ + var t = scheduler.task( "test" ).setTimezone( "America/Chicago" ); + expect( t.getTimezone().toString() ).toBe( "America/Chicago" ); + } ); + + it( "can be disabled", function(){ + var t = scheduler.task( "test" ); + expect( t.isDisabled() ).toBeFalse(); + expect( t.disable().isDisabled() ).toBeTrue(); + } ); + + it( "can call before", function(){ + var t = scheduler + .task( "test" ) + .before( function(){ + return true; + } ); + expect( isClosure( t.getBeforeTask() ) ).toBeTrue(); + } ); + it( "can call after", function(){ + var t = scheduler + .task( "test" ) + .after( function(){ + return true; + } ); + expect( isClosure( t.getafterTask() ) ).toBeTrue(); + } ); + it( "can call onTaskSuccess", function(){ + var t = scheduler + .task( "test" ) + .onSuccess( function(){ + return true; + } ); + expect( isClosure( t.getonTaskSuccess() ) ).toBeTrue(); + } ); + it( "can call onTaskFailure", function(){ + var t = scheduler + .task( "test" ) + .onFailure( function(){ + return true; + } ); + expect( isClosure( t.getonTaskFailure() ) ).toBeTrue(); + } ); + + describe( "can register multiple frequencies using everyXXX() method calls", function(){ + var timeUnits = [ + "days", + "hours", + "microseconds", + "milliseconds", + "minutes", + "nanoseconds", + "seconds" + ]; + + timeUnits.each( function( thisUnit ){ + it( + title = "can register every 5 #thisUnit# as a time unit", + body = function( data ){ + var t = scheduler.task( "test" ).every( 5, data.unit ); + expect( t.getPeriod() ).toBe( 5 ); + expect( t.getTimeUnit() ).toBe( data.unit ); + }, + data = { unit : thisUnit } + ); + } ); + + it( "can register using everyMinute()", function(){ + var t = scheduler.task( "test" ).everyMinute(); + expect( t.getPeriod() ).toBe( 1 ); + expect( t.getTimeUnit() ).toBe( "minutes" ); + } ); + + it( "can register everyHour()", function(){ + var t = scheduler.task( "test" ).everyHour(); + expect( t.getPeriod() ).toBe( 1 ); + expect( t.getTimeUnit() ).toBe( "hours" ); + } ); + + it( "can register everyWeek()", function(){ + var t = scheduler.task( "test" ).everyWeek(); + expect( t.getPeriod() ).toBe( 7 ); + expect( t.getTimeUnit() ).toBe( "days" ); + } ); + + it( "can register everyMonth()", function(){ + var t = scheduler.task( "test" ).everyMonth(); + expect( t.getPeriod() ).toBe( 30 ); + expect( t.getTimeUnit() ).toBe( "days" ); + } ); + + it( "can register everyYear()", function(){ + var t = scheduler.task( "test" ).everyYear(); + expect( t.getPeriod() ).toBe( 365 ); + expect( t.getTimeUnit() ).toBe( "days" ); + } ); + + it( "can register everyHourAt()", function(){ + var t = scheduler.task( "test" ).everyHourAt( 15 ); + expect( t.getDelay() ).notToBeEmpty(); + expect( t.getPeriod() ).toBe( 3600 ); + expect( t.getTimeUnit() ).toBe( "seconds" ); + } ); + + it( "can register everyDayAt()", function(){ + var t = scheduler.task( "test" ).everyDayAt( "04:00" ); + expect( t.getDelay() ).notToBeEmpty(); + expect( t.getPeriod() ).toBe( 86400 ); + expect( t.getTimeUnit() ).toBe( "seconds" ); + } ); + } ); + } ); + } + +} From 4267d79a58ac5864bdffdbe1cdec4fcd1fb78700 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Wed, 14 Apr 2021 18:12:33 -0500 Subject: [PATCH 23/40] days of the week constraints --- system/async/tasks/ScheduledTask.cfc | 67 +++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/system/async/tasks/ScheduledTask.cfc b/system/async/tasks/ScheduledTask.cfc index 28ba24120..d8a2f443b 100644 --- a/system/async/tasks/ScheduledTask.cfc +++ b/system/async/tasks/ScheduledTask.cfc @@ -655,7 +655,7 @@ component accessors="true" { * * @time The specific time using 24 hour format => HH:mm, defaults to 00:00 */ - ScheduledTask function weekends( string time = "00:00" ){ + ScheduledTask function onWeekends( string time = "00:00" ){ return this; } @@ -664,7 +664,70 @@ component accessors="true" { * * @time The specific time using 24 hour format => HH:mm, defaults to 00:00 */ - ScheduledTask function weekdays( string time = "00:00" ){ + ScheduledTask function onWeekdays( string time = "00:00" ){ + return this; + } + + /** + * Set the period to be on Mondays + * + * @time The specific time using 24 hour format => HH:mm, defaults to 00:00 + */ + ScheduledTask function onMondays( string time = "00:00" ){ + return this; + } + + /** + * Set the period to be on Tuesdays + * + * @time The specific time using 24 hour format => HH:mm, defaults to 00:00 + */ + ScheduledTask function onTuesdays( string time = "00:00" ){ + return this; + } + + /** + * Set the period to be on Wednesdays + * + * @time The specific time using 24 hour format => HH:mm, defaults to 00:00 + */ + ScheduledTask function onWednesdays( string time = "00:00" ){ + return this; + } + + /** + * Set the period to be on Thursdays + * + * @time The specific time using 24 hour format => HH:mm, defaults to 00:00 + */ + ScheduledTask function onThursdays( string time = "00:00" ){ + return this; + } + + /** + * Set the period to be on Fridays + * + * @time The specific time using 24 hour format => HH:mm, defaults to 00:00 + */ + ScheduledTask function onFridays( string time = "00:00" ){ + return this; + } + + /** + * Set the period to be on Saturdays + * + * @time The specific time using 24 hour format => HH:mm, defaults to 00:00 + */ + ScheduledTask function onSaturdays( string time = "00:00" ){ + return this; + } + + /** + * Set the period to be on Sundays + * + * @time The specific time using 24 hour format => HH:mm, defaults to 00:00 + */ + ScheduledTask function onSundays( string time = "00:00" ){ return this; } From a372f65dac7ee70ed2f1e9c999e2793d3751a6ff Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Wed, 14 Apr 2021 18:21:43 -0500 Subject: [PATCH 24/40] WIREBOX-112 virtual inheritance causes double inits on objects that do not have a constructor and their parent does. --- system/ioc/Builder.cfc | 12 +++++++++++- tests/specs/ioc/InjectorCreationTest.cfc | 2 +- .../config/samples/InjectorCreationTestsBinder.cfc | 7 ++++--- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/system/ioc/Builder.cfc b/system/ioc/Builder.cfc index 9cdeeb400..b77ba6737 100644 --- a/system/ioc/Builder.cfc +++ b/system/ioc/Builder.cfc @@ -943,7 +943,17 @@ component serializable="false" accessors="true" { .getVariablesMixin() // filter out overrides .filter( function( key, value ){ - return ( !targetVariables.keyExists( key ) AND NOT listFindNoCase( excludedProperties, key ) ); + // If it's a function and not excluded and not overriden, then inject it + if ( + isCustomFunction( arguments.value ) AND + !listFindNoCase( excludedProperties, arguments.key ) AND + !targetVariables.keyExists( arguments.key ) + ) { + return true; + } + + // Check for just data now + return ( !listFindNoCase( excludedProperties, arguments.key ) ); } ) .each( function( propertyName, propertyValue ){ // inject the property/method now diff --git a/tests/specs/ioc/InjectorCreationTest.cfc b/tests/specs/ioc/InjectorCreationTest.cfc index f4b8a1722..7589c8170 100755 --- a/tests/specs/ioc/InjectorCreationTest.cfc +++ b/tests/specs/ioc/InjectorCreationTest.cfc @@ -229,6 +229,6 @@ function testVirtualInheritanceCreation(){ var c = injector.getInstance( "virtually-inherited-class" ); - expect( c.getData() ).toBe( "My Data" ); + expect( c.getData() ).toBe( "Default Data" ); } } diff --git a/tests/specs/ioc/config/samples/InjectorCreationTestsBinder.cfc b/tests/specs/ioc/config/samples/InjectorCreationTestsBinder.cfc index bd8e496d9..eb0ee8d14 100755 --- a/tests/specs/ioc/config/samples/InjectorCreationTestsBinder.cfc +++ b/tests/specs/ioc/config/samples/InjectorCreationTestsBinder.cfc @@ -1,4 +1,5 @@ -component extends = "coldbox.system.ioc.config.Binder"{ +component extends="coldbox.system.ioc.config.Binder" { + /** * Configure WireBox, that's it! */ @@ -138,7 +139,7 @@ map( "virtually-inherited-class" ) .to( "tests.resources.ChildClass" ) - .virtualInheritance( "tests.resources.VirtualParentClass" ) - .initArg( name = "data", value = "My Data" ); + .virtualInheritance( "tests.resources.VirtualParentClass" ); } + } From 840410a79fd7c3fc908339ed04ab5c988dbc9d80 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Thu, 15 Apr 2021 11:11:11 -0500 Subject: [PATCH 25/40] tooooons of more scheduling frequencies are now alive!!!! COLDBOX-994 --- system/async/tasks/ScheduledTask.cfc | 332 +++++++++++++----- system/async/time/ChronoUnit.cfc | 5 +- tests/specs/async/tasks/ScheduledTaskSpec.cfc | 58 ++- 3 files changed, 289 insertions(+), 106 deletions(-) diff --git a/system/async/tasks/ScheduledTask.cfc b/system/async/tasks/ScheduledTask.cfc index d8a2f443b..a1fd65439 100644 --- a/system/async/tasks/ScheduledTask.cfc +++ b/system/async/tasks/ScheduledTask.cfc @@ -88,6 +88,11 @@ component accessors="true" { */ property name="onTaskFailure"; + /** + * The constraint of what day of the month we need to run on: 1-31 + */ + property name="dayOfTheMonth" type="numeric"; + /** * Constructor * @@ -124,6 +129,7 @@ component accessors="true" { // Constraints variables.disabled = false; variables.when = ""; + variables.dayOfTheMonth = 0; // Probable Scheduler or not variables.scheduler = ""; // Prepare execution tracking stats @@ -436,7 +442,7 @@ component accessors="true" { } /** - * Set the spaced delay between the executions of this scheduled task + * Run the task every custom spaced delay of execution, meaning no overlaps * * @delay The delay that will be used before executing the task * @timeUnit The time unit to use, available units are: days, hours, microseconds, milliseconds, minutes, nanoseconds, and seconds. The default is milliseconds @@ -448,7 +454,7 @@ component accessors="true" { } /** - * Set the period of execution for the schedule + * Run the task every custom period of execution * * @period The period of execution * @timeUnit The time unit to use, available units are: days, hours, microseconds, milliseconds, minutes, nanoseconds, and seconds. The default is milliseconds @@ -460,7 +466,7 @@ component accessors="true" { } /** - * Set the period to be every minute from the time it get's scheduled + * Run the task every minute from the time it get's scheduled */ ScheduledTask function everyMinute(){ variables.period = 1; @@ -469,7 +475,7 @@ component accessors="true" { } /** - * Set the period to be every hour from the time it get's scheduled + * Run the task every hour from the time it get's scheduled */ ScheduledTask function everyHour(){ variables.period = 1; @@ -478,53 +484,46 @@ component accessors="true" { } /** - * Set the period to be every day from the time it get's scheduled - */ - ScheduledTask function everyDay(){ - variables.period = 1; - variables.timeUnit = "days"; - return this; - } - - /** - * Set the period to be every week (7 days) from the time it get's scheduled - */ - ScheduledTask function everyWeek(){ - variables.period = 7; - variables.timeUnit = "days"; - return this; - } - - /** - * Set the period to be every month (30 days) from the time it get's scheduled + * Set the period to be hourly at a specific minute mark and 00 seconds + * + * @minutes The minutes past the hour mark */ - ScheduledTask function everyMonth(){ - variables.period = 30; - variables.timeUnit = "days"; - return this; - } + ScheduledTask function everyHourAt( required numeric minutes ){ + var now = variables.chronoUnitHelper.toLocalDateTime( now(), getTimezone() ); + var nextRun = now.withMinute( javacast( "int", arguments.minutes ) ).withSecond( javacast( "int", 0 ) ); + // If we passed it, then move the hour by 1 + if ( now.compareTo( nextRun ) > 0 ) { + nextRun = nextRun.plusHours( javacast( "int", 1 ) ); + } + // Get the duration time for the next run and delay accordingly + this.delay( + variables.chronoUnitHelper + .duration() + .getNative() + .between( now, nextRun ) + .getSeconds(), + "seconds" + ); + // Set the period to be every hour + variables.period = variables.timeUnitHelper.get( "hours" ).toSeconds( 1 ); + variables.timeUnit = "seconds"; - /** - * Set the period to be every year (365 days) from the time it get's scheduled - */ - ScheduledTask function everyYear(){ - variables.period = 365; - variables.timeUnit = "days"; return this; } /** - * Set the period to be hourly at a specific minute mark - * - * @minutes The minutes past the hour mark + * Run the task every day at midnight */ - ScheduledTask function everyHourAt( required numeric minutes ){ - // Get times - var now = variables.chronoUnitHelper.toLocalDateTime( now() ); - var nextRun = now.withMinute( javacast( "int", arguments.minutes ) ); - // If we passed it, then move the hour by 1 + ScheduledTask function everyDay(){ + var now = variables.chronoUnitHelper.toLocalDateTime( now(), getTimezone() ); + // Set at midnight + var nextRun = now + .withHour( javacast( "int", 0 ) ) + .withMinute( javacast( "int", 0 ) ) + .withSecond( javacast( "int", 0 ) ); + // If we passed it, then move to the next day if ( now.compareTo( nextRun ) > 0 ) { - nextRun = nextRun.plusHours( javacast( "int", 1 ) ) + nextRun = nextRun.plusDays( javacast( "int", 1 ) ); } // Get the duration time for the next run and delay accordingly this.delay( @@ -536,14 +535,15 @@ component accessors="true" { "seconds" ); // Set the period to every day in seconds - variables.period = variables.timeUnitHelper.get( "hours" ).toSeconds( 1 ); + variables.period = variables.timeUnitHelper.get( "days" ).toSeconds( 1 ); variables.timeUnit = "seconds"; return this; } /** - * Set the period to be daily at a specific time + * Set the period to be daily at a specific time in 24 hour format: HH:mm + * We will always add 0 seconds for you. * * @time The specific time using 24 hour format => HH:mm */ @@ -552,14 +552,15 @@ component accessors="true" { if ( !find( ":", arguments.time ) ) { arguments.time &= ":00"; } - // Validate it + // Validate time format validateTime( arguments.time ); // Get times - var now = variables.chronoUnitHelper.toLocalDateTime( now() ); + var now = variables.chronoUnitHelper.toLocalDateTime( now(), getTimezone() ); var nextRun = now .withHour( javacast( "int", getToken( arguments.time, 1, ":" ) ) ) - .withMinute( javacast( "int", getToken( arguments.time, 2, ":" ) ) ); - // If we passed it, then move the day + .withMinute( javacast( "int", getToken( arguments.time, 2, ":" ) ) ) + .withSecond( javacast( "int", 0 ) ); + // If we passed it, then move to the next day if ( now.compareTo( nextRun ) > 0 ) { nextRun = nextRun.plusDays( javacast( "int", 1 ) ) } @@ -580,73 +581,230 @@ component accessors="true" { } /** - * Set the period to be weekly at a specific time at a specific day of the week - * - * @dayOfWeek The day of the week from 1 (Monday) -> 7 (Sunday) - * @time The specific time using 24 hour format => HH:mm + * Run the task every Sunday at midnight */ - ScheduledTask function everyWeekOn( required dayOfWeek, required string time ){ + ScheduledTask function everyWeek(){ + var now = variables.chronoUnitHelper.toLocalDateTime( now(), getTimezone() ); + // Set at midnight + var nextRun = now + // Sunday + .with( variables.chronoUnitHelper.ChronoField.DAY_OF_WEEK, javacast( "int", 7 ) ) + // Midnight + .withHour( javacast( "int", 0 ) ) + .withMinute( javacast( "int", 0 ) ) + .withSecond( javacast( "int", 0 ) ); + // If we passed it, then move to the next week + if ( now.compareTo( nextRun ) > 0 ) { + nextRun = nextRun.plusWeeks( javacast( "int", 1 ) ); + } + // Get the duration time for the next run and delay accordingly + this.delay( + variables.chronoUnitHelper + .duration() + .getNative() + .between( now, nextRun ) + .getSeconds(), + "seconds" + ); + // Set the period to every week in seconds + variables.period = variables.timeUnitHelper.get( "days" ).toSeconds( 7 ); + variables.timeUnit = "seconds"; return this; } /** - * Set the period to be weekly at a specific time at a specific day of the week + * Run the task weekly on the given day of the week and time * - * @day Which day of the month - * @time The specific time using 24 hour format => HH:mm + * @dayOfWeek The day of the week from 1 (Monday) -> 7 (Sunday) + * @time The specific time using 24 hour format => HH:mm, defaults to midnight */ - ScheduledTask function everyMonthOn( required day, required string time ){ + ScheduledTask function everyWeekOn( required numeric dayOfWeek, string time = "00:00" ){ + var now = variables.chronoUnitHelper.toLocalDateTime( now(), getTimezone() ); + // Check for mintues else add them + if ( !find( ":", arguments.time ) ) { + arguments.time &= ":00"; + } + // Validate time format + validateTime( arguments.time ); + var nextRun = now + // Given day + .with( variables.chronoUnitHelper.ChronoField.DAY_OF_WEEK, javacast( "int", arguments.dayOfWeek ) ) + // Given time + .withHour( javacast( "int", getToken( arguments.time, 1, ":" ) ) ) + .withMinute( javacast( "int", getToken( arguments.time, 2, ":" ) ) ) + .withSecond( javacast( "int", 0 ) ); + // If we passed it, then move to the next week + if ( now.compareTo( nextRun ) > 0 ) { + nextRun = nextRun.plusWeeks( javacast( "int", 1 ) ); + } + // Get the duration time for the next run and delay accordingly + this.delay( + variables.chronoUnitHelper + .duration() + .getNative() + .between( now, nextRun ) + .getSeconds(), + "seconds" + ); + // Set the period to every week in seconds + variables.period = variables.timeUnitHelper.get( "days" ).toSeconds( 7 ); + variables.timeUnit = "seconds"; return this; } /** - * Set the period to be weekly at a specific time at a specific day of the week - * - * @month The month in numeric format 1-12 - * @day Which day of the month - * @time The specific time using 24 hour format => HH:mm, defaults to 00:00 + * Run the task on the first day of every month at midnight */ - ScheduledTask function everyYearOn( - required numeric month, - required numeric day, - required string time = "00:00" - ){ - return this; - } + ScheduledTask function everyMonth(){ + var now = variables.chronoUnitHelper.toLocalDateTime( now(), getTimezone() ); + // Set at midnight + var nextRun = now + // First day of the month + .with( variables.chronoUnitHelper.ChronoField.DAY_OF_MONTH, javacast( "int", 1 ) ) + // Midnight + .withHour( javacast( "int", 0 ) ) + .withMinute( javacast( "int", 0 ) ) + .withSecond( javacast( "int", 0 ) ); - /** - * Set the period to be the first day of the month - * - * @time The specific time using 24 hour format => HH:mm, defaults to 00:00 - */ - ScheduledTask function firstDayOfTheMonth( string time = "00:00" ){ + if ( now.compareTo( nextRun ) > 0 ) { + nextRun = nextRun.plusMonths( javacast( "int", 1 ) ); + } + // Get the duration time for the next run and delay accordingly + this.delay( + variables.chronoUnitHelper + .duration() + .getNative() + .between( now, nextRun ) + .getSeconds(), + "seconds" + ); + // Set the period to one day. And make sure we add a constraint for it + // Mostly because every month is different + variables.period = variables.timeUnitHelper.get( "days" ).toSeconds( 1 ); + variables.timeUnit = "seconds"; + variables.dayOfTheMonth = 1; return this; } /** - * Set the period to be the first business day of the month + * Run the task every month on a specific day and time * - * @time The specific time using 24 hour format => HH:mm, defaults to 00:00 + * @day Which day of the month + * @time The specific time using 24 hour format => HH:mm, defaults to midnight */ - ScheduledTask function firstBusinessDayOfTheMonth( string time = "00:00" ){ + ScheduledTask function everyMonthOn( required numeric day, string time = "00:00" ){ + var now = variables.chronoUnitHelper.toLocalDateTime( now(), getTimezone() ); + // Check for mintues else add them + if ( !find( ":", arguments.time ) ) { + arguments.time &= ":00"; + } + // Validate time format + validateTime( arguments.time ); + // Get new time + var nextRun = now + // First day of the month + .with( variables.chronoUnitHelper.ChronoField.DAY_OF_MONTH, javacast( "int", arguments.day ) ) + // Specific Time + .withHour( javacast( "int", getToken( arguments.time, 1, ":" ) ) ) + .withMinute( javacast( "int", getToken( arguments.time, 2, ":" ) ) ) + .withSecond( javacast( "int", 0 ) ); + // Have we passed it + if ( now.compareTo( nextRun ) > 0 ) { + nextRun = nextRun.plusMonths( javacast( "int", 1 ) ); + } + // Get the duration time for the next run and delay accordingly + this.delay( + variables.chronoUnitHelper + .duration() + .getNative() + .between( now, nextRun ) + .getSeconds(), + "seconds" + ); + // Set the period to one day. And make sure we add a constraint for it + // Mostly because every month is different + variables.period = variables.timeUnitHelper.get( "days" ).toSeconds( 1 ); + variables.timeUnit = "seconds"; + variables.dayOfTheMonth = arguments.day; return this; } /** - * Set the period to be the last day of the month - * - * @time The specific time using 24 hour format => HH:mm, defaults to 00:00 + * Run the task on the first day of the year at midnight */ - ScheduledTask function lastDayOfTheMonth( string time = "00:00" ){ + ScheduledTask function everyYear(){ + var now = variables.chronoUnitHelper.toLocalDateTime( now(), getTimezone() ); + // Set at midnight + var nextRun = now + // First day of the month + .with( variables.chronoUnitHelper.ChronoField.DAY_OF_YEAR, javacast( "int", 1 ) ) + // Midnight + .withHour( javacast( "int", 0 ) ) + .withMinute( javacast( "int", 0 ) ) + .withSecond( javacast( "int", 0 ) ); + + if ( now.compareTo( nextRun ) > 0 ) { + nextRun = nextRun.plusYears( javacast( "int", 1 ) ); + } + // Get the duration time for the next run and delay accordingly + this.delay( + variables.chronoUnitHelper + .duration() + .getNative() + .between( now, nextRun ) + .getSeconds(), + "seconds" + ); + // Set the period to + variables.period = variables.timeUnitHelper.get( "days" ).toSeconds( 365 ); + variables.timeUnit = "seconds"; return this; } /** - * Set the period to be the last business day of the month + * Set the period to be weekly at a specific time at a specific day of the week * + * @month The month in numeric format 1-12 + * @day Which day of the month * @time The specific time using 24 hour format => HH:mm, defaults to 00:00 */ - ScheduledTask function lastBusinessDayOfTheMonth( string time = "00:00" ){ + ScheduledTask function everyYearOn( + required numeric month, + required numeric day, + required string time = "00:00" + ){ + var now = variables.chronoUnitHelper.toLocalDateTime( now(), getTimezone() ); + // Check for mintues else add them + if ( !find( ":", arguments.time ) ) { + arguments.time &= ":00"; + } + // Validate time format + validateTime( arguments.time ); + var nextRun = now + // Specific month + .with( variables.chronoUnitHelper.ChronoField.MONTH_OF_YEAR, javacast( "int", arguments.month ) ) + // Specific day of the month + .with( variables.chronoUnitHelper.ChronoField.DAY_OF_MONTH, javacast( "int", arguments.day ) ) + // Midnight + .withHour( javacast( "int", getToken( arguments.time, 1, ":" ) ) ) + .withMinute( javacast( "int", getToken( arguments.time, 2, ":" ) ) ) + .withSecond( javacast( "int", 0 ) ); + // Have we passed it? + if ( now.compareTo( nextRun ) > 0 ) { + nextRun = nextRun.plusYears( javacast( "int", 1 ) ); + } + // Get the duration time for the next run and delay accordingly + this.delay( + variables.chronoUnitHelper + .duration() + .getNative() + .between( now, nextRun ) + .getSeconds(), + "seconds" + ); + // Set the period to + variables.period = variables.timeUnitHelper.get( "days" ).toSeconds( 365 ); + variables.timeUnit = "seconds"; return this; } @@ -802,9 +960,9 @@ component accessors="true" { */ private function validateTime( required time ){ // Regex check - if ( !reFind( "^[0-2][0-4]\:[0-5][0-9]$", arguments.time ) ) { + if ( !reFind( "^[0-2][0-9]\:[0-5][0-9]$", arguments.time ) ) { throw( - message = "Invalid time representation. Time is represented in 24 hour minute format => HH:mm", + message = "Invalid time representation (#arguments.time#). Time is represented in 24 hour minute format => HH:mm", type = "InvalidTimeException" ); } diff --git a/system/async/time/ChronoUnit.cfc b/system/async/time/ChronoUnit.cfc index 90279f122..ebc7205ab 100644 --- a/system/async/time/ChronoUnit.cfc +++ b/system/async/time/ChronoUnit.cfc @@ -9,8 +9,9 @@ component singleton { // TimeZone Helpers - this.ZoneOffset = createObject( "java", "java.time.ZoneOffset" ); - this.ZoneId = createObject( "java", "java.time.ZoneId" ); + this.ZoneOffset = createObject( "java", "java.time.ZoneOffset" ); + this.ZoneId = createObject( "java", "java.time.ZoneId" ); + this.ChronoField = createObject( "java", "java.time.temporal.ChronoField" ); // The java chrono unit we model variables.jChronoUnit = createObject( "java", "java.time.temporal.ChronoUnit" ); diff --git a/tests/specs/async/tasks/ScheduledTaskSpec.cfc b/tests/specs/async/tasks/ScheduledTaskSpec.cfc index 2ca9f1b39..49e7852c8 100644 --- a/tests/specs/async/tasks/ScheduledTaskSpec.cfc +++ b/tests/specs/async/tasks/ScheduledTaskSpec.cfc @@ -11,7 +11,7 @@ component extends="tests.specs.async.BaseAsyncSpec" { // all your suites go here. describe( "Scheduled Task", function(){ beforeEach( function( currentSpec ){ - variables.scheduler = asyncManager.newScheduler( "bdd-test" ); + variables.scheduler = asyncManager.newScheduler( "bdd-test" ).setTimezone( "America/Chicago" ); } ); afterEach( function( currentSpec ){ @@ -112,35 +112,59 @@ component extends="tests.specs.async.BaseAsyncSpec" { expect( t.getTimeUnit() ).toBe( "hours" ); } ); + it( "can register everyHourAt()", function(){ + var t = scheduler.task( "test" ).everyHourAt( 15 ); + expect( t.getDelay() ).notToBeEmpty(); + expect( t.getPeriod() ).toBe( 3600 ); + expect( t.getTimeUnit() ).toBe( "seconds" ); + } ); + + it( "can register everyDay()", function(){ + var t = scheduler.task( "test" ).everyDay(); + expect( t.getPeriod() ).toBe( 86400 ); + expect( t.getTimeUnit() ).toBe( "seconds" ); + } ); + + it( "can register everyDayAt()", function(){ + var t = scheduler.task( "test" ).everyDayAt( "04:00" ); + expect( t.getDelay() ).notToBeEmpty(); + expect( t.getPeriod() ).toBe( 86400 ); + expect( t.getTimeUnit() ).toBe( "seconds" ); + } ); + it( "can register everyWeek()", function(){ var t = scheduler.task( "test" ).everyWeek(); - expect( t.getPeriod() ).toBe( 7 ); - expect( t.getTimeUnit() ).toBe( "days" ); + expect( t.getPeriod() ).toBe( 604800 ); + expect( t.getTimeUnit() ).toBe( "seconds" ); + } ); + + it( "can register everyWeekOn()", function(){ + var t = scheduler.task( "test" ).everyWeekOn( 4, "09:00" ); + expect( t.getPeriod() ).toBe( 604800 ); + expect( t.getTimeUnit() ).toBe( "seconds" ); } ); it( "can register everyMonth()", function(){ var t = scheduler.task( "test" ).everyMonth(); - expect( t.getPeriod() ).toBe( 30 ); - expect( t.getTimeUnit() ).toBe( "days" ); + expect( t.getPeriod() ).toBe( 86400 ); + expect( t.getTimeUnit() ).toBe( "seconds" ); } ); - it( "can register everyYear()", function(){ - var t = scheduler.task( "test" ).everyYear(); - expect( t.getPeriod() ).toBe( 365 ); - expect( t.getTimeUnit() ).toBe( "days" ); + it( "can register everyMonthOn()", function(){ + var t = scheduler.task( "test" ).everyMonthOn( 10, "09:00" ); + expect( t.getPeriod() ).toBe( 86400 ); + expect( t.getTimeUnit() ).toBe( "seconds" ); } ); - it( "can register everyHourAt()", function(){ - var t = scheduler.task( "test" ).everyHourAt( 15 ); - expect( t.getDelay() ).notToBeEmpty(); - expect( t.getPeriod() ).toBe( 3600 ); + it( "can register everyYear()", function(){ + var t = scheduler.task( "test" ).everyYear(); + expect( t.getPeriod() ).toBe( 31536000 ); expect( t.getTimeUnit() ).toBe( "seconds" ); } ); - it( "can register everyDayAt()", function(){ - var t = scheduler.task( "test" ).everyDayAt( "04:00" ); - expect( t.getDelay() ).notToBeEmpty(); - expect( t.getPeriod() ).toBe( 86400 ); + it( "can register everyYearOn()", function(){ + var t = scheduler.task( "test" ).everyYearOn( 4, 15, "09:00" ); + expect( t.getPeriod() ).toBe( 31536000 ); expect( t.getTimeUnit() ).toBe( "seconds" ); } ); } ); From ccf14439307452ce319e4942e53dba35d64c7437 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Thu, 15 Apr 2021 11:16:51 -0500 Subject: [PATCH 26/40] more more more more frequencies: COLDBOX-994 --- system/async/tasks/ScheduledTask.cfc | 79 ++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/system/async/tasks/ScheduledTask.cfc b/system/async/tasks/ScheduledTask.cfc index a1fd65439..1ed748acf 100644 --- a/system/async/tasks/ScheduledTask.cfc +++ b/system/async/tasks/ScheduledTask.cfc @@ -93,6 +93,21 @@ component accessors="true" { */ property name="dayOfTheMonth" type="numeric"; + /** + * The constraint of what day of the week we need to run on: 1 (Monday) -7 (Sunday) + */ + property name="dayOfTheWeek" type="numeric"; + + /** + * Constraint to run only on weekends + */ + property name="weekends" type="boolean"; + + /** + * Constraint to run only on weekdays + */ + property name="weekdays" type="boolean"; + /** * Constructor * @@ -130,6 +145,9 @@ component accessors="true" { variables.disabled = false; variables.when = ""; variables.dayOfTheMonth = 0; + variables.dayOfTheWeek = 0; + variables.weekends = false; + variables.weekdays = false; // Probable Scheduler or not variables.scheduler = ""; // Prepare execution tracking stats @@ -814,6 +832,37 @@ component accessors="true" { * @time The specific time using 24 hour format => HH:mm, defaults to 00:00 */ ScheduledTask function onWeekends( string time = "00:00" ){ + // Check for mintues else add them + if ( !find( ":", arguments.time ) ) { + arguments.time &= ":00"; + } + // Validate time format + validateTime( arguments.time ); + // Get times + var now = variables.chronoUnitHelper.toLocalDateTime( now(), getTimezone() ); + var nextRun = now + .withHour( javacast( "int", getToken( arguments.time, 1, ":" ) ) ) + .withMinute( javacast( "int", getToken( arguments.time, 2, ":" ) ) ) + .withSecond( javacast( "int", 0 ) ); + // If we passed it, then move to the next day + if ( now.compareTo( nextRun ) > 0 ) { + nextRun = nextRun.plusDays( javacast( "int", 1 ) ) + } + // Get the duration time for the next run and delay accordingly + this.delay( + variables.chronoUnitHelper + .duration() + .getNative() + .between( now, nextRun ) + .getSeconds(), + "seconds" + ); + // Set the period to every day in seconds + variables.period = variables.timeUnitHelper.get( "DAYS" ).toSeconds( 1 ); + variables.timeUnit = "seconds"; + // Constraint to only run on weekends + variables.weekends = true; + return this; } @@ -823,6 +872,36 @@ component accessors="true" { * @time The specific time using 24 hour format => HH:mm, defaults to 00:00 */ ScheduledTask function onWeekdays( string time = "00:00" ){ + // Check for mintues else add them + if ( !find( ":", arguments.time ) ) { + arguments.time &= ":00"; + } + // Validate time format + validateTime( arguments.time ); + // Get times + var now = variables.chronoUnitHelper.toLocalDateTime( now(), getTimezone() ); + var nextRun = now + .withHour( javacast( "int", getToken( arguments.time, 1, ":" ) ) ) + .withMinute( javacast( "int", getToken( arguments.time, 2, ":" ) ) ) + .withSecond( javacast( "int", 0 ) ); + // If we passed it, then move to the next day + if ( now.compareTo( nextRun ) > 0 ) { + nextRun = nextRun.plusDays( javacast( "int", 1 ) ) + } + // Get the duration time for the next run and delay accordingly + this.delay( + variables.chronoUnitHelper + .duration() + .getNative() + .between( now, nextRun ) + .getSeconds(), + "seconds" + ); + // Set the period to every day in seconds + variables.period = variables.timeUnitHelper.get( "DAYS" ).toSeconds( 1 ); + variables.timeUnit = "seconds"; + // Constraint to only run on weekdays + variables.weekdays = true; return this; } From 47e472733123f89e6a39bee9b98f4736c8735da2 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Thu, 15 Apr 2021 11:19:39 -0500 Subject: [PATCH 27/40] We can now schedule tasks on weekends and weekdays COLDBOX-994 --- system/async/tasks/ScheduledTask.cfc | 2 ++ tests/specs/async/tasks/ScheduledTaskSpec.cfc | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/system/async/tasks/ScheduledTask.cfc b/system/async/tasks/ScheduledTask.cfc index 1ed748acf..1e934e570 100644 --- a/system/async/tasks/ScheduledTask.cfc +++ b/system/async/tasks/ScheduledTask.cfc @@ -862,6 +862,7 @@ component accessors="true" { variables.timeUnit = "seconds"; // Constraint to only run on weekends variables.weekends = true; + variables.weekdays = false; return this; } @@ -902,6 +903,7 @@ component accessors="true" { variables.timeUnit = "seconds"; // Constraint to only run on weekdays variables.weekdays = true; + variables.weekends = false; return this; } diff --git a/tests/specs/async/tasks/ScheduledTaskSpec.cfc b/tests/specs/async/tasks/ScheduledTaskSpec.cfc index 49e7852c8..94833b06a 100644 --- a/tests/specs/async/tasks/ScheduledTaskSpec.cfc +++ b/tests/specs/async/tasks/ScheduledTaskSpec.cfc @@ -168,6 +168,24 @@ component extends="tests.specs.async.BaseAsyncSpec" { expect( t.getTimeUnit() ).toBe( "seconds" ); } ); } ); + + describe( "can register frequencies with constraints", function(){ + it( "can register to fire onWeekends()", function(){ + var t = scheduler.task( "test" ).onWeekends( "09:00" ); + expect( t.getPeriod() ).toBe( 86400 ); + expect( t.getTimeUnit() ).toBe( "seconds" ); + expect( t.getWeekends() ).toBeTrue(); + expect( t.getWeekdays() ).toBeFalse(); + } ); + + it( "can register to fire onWeekdays()", function(){ + var t = scheduler.task( "test" ).onWeekdays( "09:00" ); + expect( t.getPeriod() ).toBe( 86400 ); + expect( t.getTimeUnit() ).toBe( "seconds" ); + expect( t.getWeekends() ).toBeFalse(); + expect( t.getWeekdays() ).toBeTrue(); + } ); + } ); } ); } From 37906b34e4c4f91c817e0baa70b1ec8ebf2ad15d Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Thu, 15 Apr 2021 11:46:47 -0500 Subject: [PATCH 28/40] shortcuts for every day of the week --- system/async/tasks/ScheduledTask.cfc | 27 ++++++++++--------- tests/specs/async/tasks/ScheduledTaskSpec.cfc | 24 +++++++++++++++++ 2 files changed, 38 insertions(+), 13 deletions(-) diff --git a/system/async/tasks/ScheduledTask.cfc b/system/async/tasks/ScheduledTask.cfc index 1e934e570..2225aed6b 100644 --- a/system/async/tasks/ScheduledTask.cfc +++ b/system/async/tasks/ScheduledTask.cfc @@ -94,7 +94,7 @@ component accessors="true" { property name="dayOfTheMonth" type="numeric"; /** - * The constraint of what day of the week we need to run on: 1 (Monday) -7 (Sunday) + * The constraint of what day of the week this runs on: 1-7 */ property name="dayOfTheWeek" type="numeric"; @@ -145,7 +145,6 @@ component accessors="true" { variables.disabled = false; variables.when = ""; variables.dayOfTheMonth = 0; - variables.dayOfTheWeek = 0; variables.weekends = false; variables.weekdays = false; // Probable Scheduler or not @@ -625,8 +624,9 @@ component accessors="true" { "seconds" ); // Set the period to every week in seconds - variables.period = variables.timeUnitHelper.get( "days" ).toSeconds( 7 ); - variables.timeUnit = "seconds"; + variables.period = variables.timeUnitHelper.get( "days" ).toSeconds( 7 ); + variables.timeUnit = "seconds"; + variables.dayOfTheWeek = 7; return this; } @@ -665,8 +665,9 @@ component accessors="true" { "seconds" ); // Set the period to every week in seconds - variables.period = variables.timeUnitHelper.get( "days" ).toSeconds( 7 ); - variables.timeUnit = "seconds"; + variables.period = variables.timeUnitHelper.get( "days" ).toSeconds( 7 ); + variables.timeUnit = "seconds"; + variables.dayOfTheWeek = arguments.dayOfWeek; return this; } @@ -913,7 +914,7 @@ component accessors="true" { * @time The specific time using 24 hour format => HH:mm, defaults to 00:00 */ ScheduledTask function onMondays( string time = "00:00" ){ - return this; + return this.everyWeekOn( 1, arguments.time ); } /** @@ -922,7 +923,7 @@ component accessors="true" { * @time The specific time using 24 hour format => HH:mm, defaults to 00:00 */ ScheduledTask function onTuesdays( string time = "00:00" ){ - return this; + return this.everyWeekOn( 2, arguments.time ); } /** @@ -931,7 +932,7 @@ component accessors="true" { * @time The specific time using 24 hour format => HH:mm, defaults to 00:00 */ ScheduledTask function onWednesdays( string time = "00:00" ){ - return this; + return this.everyWeekOn( 3, arguments.time ); } /** @@ -940,7 +941,7 @@ component accessors="true" { * @time The specific time using 24 hour format => HH:mm, defaults to 00:00 */ ScheduledTask function onThursdays( string time = "00:00" ){ - return this; + return this.everyWeekOn( 4, arguments.time ); } /** @@ -949,7 +950,7 @@ component accessors="true" { * @time The specific time using 24 hour format => HH:mm, defaults to 00:00 */ ScheduledTask function onFridays( string time = "00:00" ){ - return this; + return this.everyWeekOn( 5, arguments.time ); } /** @@ -958,7 +959,7 @@ component accessors="true" { * @time The specific time using 24 hour format => HH:mm, defaults to 00:00 */ ScheduledTask function onSaturdays( string time = "00:00" ){ - return this; + return this.everyWeekOn( 6, arguments.time ); } /** @@ -967,7 +968,7 @@ component accessors="true" { * @time The specific time using 24 hour format => HH:mm, defaults to 00:00 */ ScheduledTask function onSundays( string time = "00:00" ){ - return this; + return this.everyWeekOn( 7, arguments.time ); } /** diff --git a/tests/specs/async/tasks/ScheduledTaskSpec.cfc b/tests/specs/async/tasks/ScheduledTaskSpec.cfc index 94833b06a..eafdeb84d 100644 --- a/tests/specs/async/tasks/ScheduledTaskSpec.cfc +++ b/tests/specs/async/tasks/ScheduledTaskSpec.cfc @@ -185,6 +185,30 @@ component extends="tests.specs.async.BaseAsyncSpec" { expect( t.getWeekends() ).toBeFalse(); expect( t.getWeekdays() ).toBeTrue(); } ); + + var daysOfTheWeek = [ + "mondays", + "tuesdays", + "wednesdays", + "thursdays", + "fridays", + "saturdays", + "sundays" + ]; + daysOfTheWeek.each( function( thisDay ){ + it( + title = "can register to fire on the #thisDay#", + body = function( data ){ + var t = scheduler.task( "test" ); + + invoke( t, "on#data.thisDay#" ); + + expect( t.getPeriod() ).toBe( 604800 ); + expect( t.getTimeUnit() ).toBe( "seconds" ); + }, + data = { thisDay : thisDay } + ); + } ); } ); } ); } From 5b9019c61dfce85b2999a4e9f28da8a9c80f5c01 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Thu, 15 Apr 2021 12:03:15 -0500 Subject: [PATCH 29/40] Added last and first business days of the month --- system/async/tasks/ScheduledTask.cfc | 109 ++++++++++++++++++ tests/specs/async/tasks/ScheduledTaskSpec.cfc | 15 +++ 2 files changed, 124 insertions(+) diff --git a/system/async/tasks/ScheduledTask.cfc b/system/async/tasks/ScheduledTask.cfc index 2225aed6b..fb0f7c043 100644 --- a/system/async/tasks/ScheduledTask.cfc +++ b/system/async/tasks/ScheduledTask.cfc @@ -108,6 +108,11 @@ component accessors="true" { */ property name="weekdays" type="boolean"; + /** + * Constraint to run only on the last business day of the month + */ + property name="lastBusinessDay" type="boolean"; + /** * Constructor * @@ -147,6 +152,7 @@ component accessors="true" { variables.dayOfTheMonth = 0; variables.weekends = false; variables.weekdays = false; + variables.lastBusinessDay = false; // Probable Scheduler or not variables.scheduler = ""; // Prepare execution tracking stats @@ -748,6 +754,109 @@ component accessors="true" { return this; } + /** + * Run the task on the first Monday of every month + * + * @time The specific time using 24 hour format => HH:mm, defaults to midnight + */ + ScheduledTask function onFirstBusinessDayOfTheMonth( string time = "00:00" ){ + var now = variables.chronoUnitHelper.toLocalDateTime( now(), getTimezone() ); + // Check for mintues else add them + if ( !find( ":", arguments.time ) ) { + arguments.time &= ":00"; + } + // Validate time format + validateTime( arguments.time ); + // Get new time + var nextRun = now + // First business day of the month + .with( + createObject( "java", "java.time.temporal.TemporalAdjusters" ).firstInMonth( + createObject( "java", "java.time.DayOfWeek" ).MONDAY + ) + ) + // Specific Time + .withHour( javacast( "int", getToken( arguments.time, 1, ":" ) ) ) + .withMinute( javacast( "int", getToken( arguments.time, 2, ":" ) ) ) + .withSecond( javacast( "int", 0 ) ); + // Have we passed it + if ( now.compareTo( nextRun ) > 0 ) { + nextRun = nextRun.plusMonths( javacast( "int", 1 ) ); + } + // Get the duration time for the next run and delay accordingly + this.delay( + variables.chronoUnitHelper + .duration() + .getNative() + .between( now, nextRun ) + .getSeconds(), + "seconds" + ); + // Set the period to one day. And make sure we add a constraint for it + // Mostly because every month is different + variables.period = variables.timeUnitHelper.get( "days" ).toSeconds( 1 ); + variables.timeUnit = "seconds"; + variables.dayOfTheMonth = 1; + return this; + } + + /** + * Run the task on the last business day of the month + * + * @time The specific time using 24 hour format => HH:mm, defaults to midnight + */ + ScheduledTask function onLastBusinessDayOfTheMonth( string time = "00:00" ){ + var now = variables.chronoUnitHelper.toLocalDateTime( now(), getTimezone() ); + // Check for mintues else add them + if ( !find( ":", arguments.time ) ) { + arguments.time &= ":00"; + } + // Validate time format + validateTime( arguments.time ); + + // Get the last day of the month + var lastDay = now.with( createObject( "java", "java.time.temporal.TemporalAdjusters" ).lastDayOfMonth() ); + // Verify if on weekend + switch ( lastDay.getDayOfWeek().getValue() ) { + // Sunday - 2 days + case 7: { + lastDay = lastDay.minusDays( 2 ); + break; + } + // Saturday - 1 day + case 6: { + lastDay = lastDay.minusDays( 1 ); + break; + } + } + + // Get new time + var nextRun = lastDay + // Specific Time + .withHour( javacast( "int", getToken( arguments.time, 1, ":" ) ) ) + .withMinute( javacast( "int", getToken( arguments.time, 2, ":" ) ) ) + .withSecond( javacast( "int", 0 ) ); + // Have we passed it + if ( now.compareTo( nextRun ) > 0 ) { + nextRun = nextRun.plusMonths( javacast( "int", 1 ) ); + } + // Get the duration time for the next run and delay accordingly + this.delay( + variables.chronoUnitHelper + .duration() + .getNative() + .between( now, nextRun ) + .getSeconds(), + "seconds" + ); + // Set the period to one day. And make sure we add a constraint for it + // Mostly because every month is different + variables.period = variables.timeUnitHelper.get( "days" ).toSeconds( 1 ); + variables.timeUnit = "seconds"; + variables.lastBusinessDay = true; + return this; + } + /** * Run the task on the first day of the year at midnight */ diff --git a/tests/specs/async/tasks/ScheduledTaskSpec.cfc b/tests/specs/async/tasks/ScheduledTaskSpec.cfc index eafdeb84d..938c42787 100644 --- a/tests/specs/async/tasks/ScheduledTaskSpec.cfc +++ b/tests/specs/async/tasks/ScheduledTaskSpec.cfc @@ -170,6 +170,21 @@ component extends="tests.specs.async.BaseAsyncSpec" { } ); describe( "can register frequencies with constraints", function(){ + it( "can register to fire onFirstBusinessDayOfTheMonth()", function(){ + var t = scheduler.task( "test" ).onFirstBusinessDayOfTheMonth( "09:00" ); + expect( t.getPeriod() ).toBe( 86400 ); + expect( t.getTimeUnit() ).toBe( "seconds" ); + expect( t.getDayOfTheMonth() ).toBe( 1 ); + } ); + + it( "can register to fire onLastBusinessDayOfTheMonth()", function(){ + var t = scheduler.task( "test" ).onLastBusinessDayOfTheMonth( "09:00" ); + expect( t.getPeriod() ).toBe( 86400 ); + expect( t.getTimeUnit() ).toBe( "seconds" ); + expect( t.getLastBusinessDay() ).toBeTrue(); + } ); + + it( "can register to fire onWeekends()", function(){ var t = scheduler.task( "test" ).onWeekends( "09:00" ); expect( t.getPeriod() ).toBe( 86400 ); From 4df42193b4967e114103fc6d33652a8abd6a74b8 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Thu, 15 Apr 2021 12:15:19 -0500 Subject: [PATCH 30/40] again, adobe and bad casting on their strings. --- system/async/tasks/ScheduledTask.cfc | 6 +++--- system/async/time/ChronoUnit.cfc | 14 +++++++++++--- tests/specs/async/tasks/SchedulerSpec.cfc | 4 ++-- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/system/async/tasks/ScheduledTask.cfc b/system/async/tasks/ScheduledTask.cfc index fb0f7c043..e3756bade 100644 --- a/system/async/tasks/ScheduledTask.cfc +++ b/system/async/tasks/ScheduledTask.cfc @@ -585,7 +585,7 @@ component accessors="true" { .withSecond( javacast( "int", 0 ) ); // If we passed it, then move to the next day if ( now.compareTo( nextRun ) > 0 ) { - nextRun = nextRun.plusDays( javacast( "int", 1 ) ) + nextRun = nextRun.plusDays( javacast( "int", 1 ) ); } // Get the duration time for the next run and delay accordingly this.delay( @@ -956,7 +956,7 @@ component accessors="true" { .withSecond( javacast( "int", 0 ) ); // If we passed it, then move to the next day if ( now.compareTo( nextRun ) > 0 ) { - nextRun = nextRun.plusDays( javacast( "int", 1 ) ) + nextRun = nextRun.plusDays( javacast( "int", 1 ) ); } // Get the duration time for the next run and delay accordingly this.delay( @@ -997,7 +997,7 @@ component accessors="true" { .withSecond( javacast( "int", 0 ) ); // If we passed it, then move to the next day if ( now.compareTo( nextRun ) > 0 ) { - nextRun = nextRun.plusDays( javacast( "int", 1 ) ) + nextRun = nextRun.plusDays( javacast( "int", 1 ) ); } // Get the duration time for the next run and delay accordingly this.delay( diff --git a/system/async/time/ChronoUnit.cfc b/system/async/time/ChronoUnit.cfc index ebc7205ab..2aec386d3 100644 --- a/system/async/time/ChronoUnit.cfc +++ b/system/async/time/ChronoUnit.cfc @@ -77,7 +77,11 @@ component singleton { function toLocalDateTime( required target, timezone ){ return this .toInstant( arguments.target ) - .atZone( isNull( arguments.timezone ) ? this.ZoneOffset.UTC : this.ZoneId.of( arguments.timezone ) ) + .atZone( + isNull( arguments.timezone ) ? this.ZoneOffset.UTC : this.ZoneId.of( + javacast( "string", arguments.timezone ) + ) + ) .toLocalDateTime(); } @@ -95,7 +99,11 @@ component singleton { function toLocalDate( required target, timezone ){ return this .toInstant( arguments.target ) - .atZone( isNull( arguments.timezone ) ? this.ZoneOffset.UTC : this.ZoneId.of( arguments.timezone ) ) + .atZone( + isNull( arguments.timezone ) ? this.ZoneOffset.UTC : this.ZoneId.of( + javacast( "string", arguments.timezone ) + ) + ) .toLocalDate(); } @@ -112,7 +120,7 @@ component singleton { * @return Java Timezone java.time.ZoneId */ function getTimezone( required timezone ){ - return this.ZoneId.of( arguments.timezone ); + return this.ZoneId.of( javacast( "string", arguments.timezone ) ); } /** diff --git a/tests/specs/async/tasks/SchedulerSpec.cfc b/tests/specs/async/tasks/SchedulerSpec.cfc index fc46635fb..74f6ec88c 100644 --- a/tests/specs/async/tasks/SchedulerSpec.cfc +++ b/tests/specs/async/tasks/SchedulerSpec.cfc @@ -26,7 +26,7 @@ component extends="tests.specs.async.BaseAsyncSpec" { } ); it( "can register a new task and get it's record", function(){ - var task = scheduler.task( "bddTest" ) + var task = scheduler.task( "bddTest" ); expect( scheduler.hasTask( "bddTest" ) ).toBeTrue(); expect( scheduler.getRegisteredTasks() ).toInclude( "bddTest" ); expect( scheduler.getTaskRecord( "bddTest" ).task.getName() ).toBe( "bddTest" ); @@ -39,7 +39,7 @@ component extends="tests.specs.async.BaseAsyncSpec" { } ); it( "can remove a task", function(){ - var task = scheduler.task( "bddTest" ) + var task = scheduler.task( "bddTest" ); expect( scheduler.hasTask( "bddTest" ) ).toBeTrue(); scheduler.removeTask( "bddTest" ); expect( scheduler.hasTask( "bddTest" ) ).toBeFalse(); From 6b97aefcfaee7c03adeeb7cd0eee467d961f6e1c Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Thu, 15 Apr 2021 14:28:38 -0500 Subject: [PATCH 31/40] wow the missing semi colon of death --- tests/specs/async/time/PeriodSpec.cfc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/specs/async/time/PeriodSpec.cfc b/tests/specs/async/time/PeriodSpec.cfc index d1e1e2537..273743863 100644 --- a/tests/specs/async/time/PeriodSpec.cfc +++ b/tests/specs/async/time/PeriodSpec.cfc @@ -66,7 +66,7 @@ component extends="tests.specs.async.BaseAsyncSpec" { } ); it( "can get total months", function(){ - var p = period.parse( "P10Y5M20D" ) + var p = period.parse( "P10Y5M20D" ); expect( p.toTotalMonths() ).toBe( 125 ); } ); @@ -152,7 +152,7 @@ component extends="tests.specs.async.BaseAsyncSpec" { var p = period.of( 2, 5, 10 ); var target = "2021-01-01"; - expect( p.addTo( target ) ).tobe( "2023-06-11" ) + expect( p.addTo( target ) ).tobe( "2023-06-11" ); } ); } ); } From 88fe5b2e3f6c7795bdba4b21d3c2bb486e3edd2e Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Thu, 15 Apr 2021 15:59:25 -0500 Subject: [PATCH 32/40] make sure we return cf arrays and not java arrays --- system/async/AsyncManager.cfc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/system/async/AsyncManager.cfc b/system/async/AsyncManager.cfc index 815f6fd16..ae73610b3 100644 --- a/system/async/AsyncManager.cfc +++ b/system/async/AsyncManager.cfc @@ -405,9 +405,12 @@ component accessors="true" singleton { } // build it up - return createObject( "java", "java.util.stream.IntStream" ) + var javaArray = createObject( "java", "java.util.stream.IntStream" ) .rangeClosed( arguments.from, arguments.to ) .toArray(); + var cfArray = []; + cfArray.append( javaArray, true ); + return cfArray; } /** From 8003309aee5239fcc92d664b27c7367a90377b1b Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Fri, 16 Apr 2021 08:55:08 -0500 Subject: [PATCH 33/40] adding vscode back in, to save custom settings --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index 9266971b3..6637a31e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,6 @@ # OS + IDE .DS_Store -/settings.xml .project -.settings/ -.vscode # Testing Logs test-harness/logs/*.log From 6683c129eb74d5157c0e612d9449631c5bc682f5 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Fri, 16 Apr 2021 08:56:13 -0500 Subject: [PATCH 34/40] adding in custom settings for the project and custom task executor --- .vscode/settings.json | 12 ++++++++++++ .vscode/tasks.json | 15 +++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..3e4aee550 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "cfml.mappings": [ + { + "logicalPath": "/coldbox", + "directoryPath": "." + }, + { + "logicalPath": "/testbox", + "directoryPath": "./testbox" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..3c132f14d --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,15 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Run Task", + "type": "shell", + "command": "box task run ${relativeFile}", + "group": "build", + "presentation": { + "reveal": "always", + "panel": "new" + } + } + ] +} \ No newline at end of file From 8289e0a5234f69e550bb5116dd79890b11ec4db7 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Fri, 16 Apr 2021 08:58:06 -0500 Subject: [PATCH 35/40] unused file --- .jsbeautifyrc | 42 ------------------------------------------ 1 file changed, 42 deletions(-) delete mode 100644 .jsbeautifyrc diff --git a/.jsbeautifyrc b/.jsbeautifyrc deleted file mode 100644 index cc0c245dc..000000000 --- a/.jsbeautifyrc +++ /dev/null @@ -1,42 +0,0 @@ -{ - "beautify.language": { - "js": ["js","json","cfc"], - "css": ["css", "scss"], - "html": ["htm", "html", "cfml", "cfm"] - }, - "brace_style": "collapse", - "indent_with_tabs": true, - "indent_size": 4, - "indent_level": 0, - "preserve_newlines": true, - "max_preserve_newlines": 5, - "html": { - "allowed_file_extensions": ["htm", "html", "xhtml", "shtml", "xml", "svg", "cfm", "cfml"], - "indent_scripts": "keep", - "indent_inner_html": true, - "indent_body_inner_html": true, - "indent_handlebars": true, - "unformatted": ["code", "pre"], - "content_unformatted": ["pre", "code"], - "wrap_line_length": 0 - }, - "css": { - "selector_separator_newline": true - }, - "js": { - "break_chained_methods": true, - "comma_first": false, - "e4x": false, - "jslint_happy": true, - "keep_function_indentation": true, - "keep_array_indentation": true, - "space_after_anon_function": false, - "spaceBeforeConditional": true, - "space_in_paren": true, - "space_in_empty_paren": false, - "space_before_conditional": true, - "operator_position": "before-newline", - "unescape_strings": false, - "wrap_line_length": 0 - } -} \ No newline at end of file From c4450bd706d084e27fbfd430c0019bf070d0f39f Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Fri, 16 Apr 2021 15:22:22 -0500 Subject: [PATCH 36/40] we can register tasks with no overlaps now --- .vscode/tasks.json | 24 +++++- system/async/tasks/ScheduledTask.cfc | 35 ++++++++- tests/specs/async/tasks/ScheduledTaskSpec.cfc | 76 +++++++++++-------- 3 files changed, 99 insertions(+), 36 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 3c132f14d..c9d4f8517 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,14 +2,32 @@ "version": "2.0.0", "tasks": [ { - "label": "Run Task", + "label": "Run CommandBox Task", "type": "shell", "command": "box task run ${relativeFile}", - "group": "build", + "group": { + "kind": "build", + "isDefault": true + }, "presentation": { "reveal": "always", "panel": "new" - } + }, + "problemMatcher": [] + }, + { + "label": "Run CommandBox Task", + "type": "shell", + "command": "box testbox run bundles=${relativeFile}", + "group": { + "kind": "build", + "isDefault": true + }, + "presentation": { + "reveal": "always", + "panel": "new" + }, + "problemMatcher": [] } ] } \ No newline at end of file diff --git a/system/async/tasks/ScheduledTask.cfc b/system/async/tasks/ScheduledTask.cfc index e3756bade..b9e2a43bf 100644 --- a/system/async/tasks/ScheduledTask.cfc +++ b/system/async/tasks/ScheduledTask.cfc @@ -113,6 +113,13 @@ component accessors="true" { */ property name="lastBusinessDay" type="boolean"; + /** + * By default tasks execute in an interval frequency which can cause overlaps if tasks + * take longer than their periods. With this boolean flag turned on, the schedulers + * don't kick off the intervals until the tasks finish executing. Meaning no overlaps. + */ + property name="noOverlaps" type="boolean"; + /** * Constructor * @@ -153,6 +160,7 @@ component accessors="true" { variables.weekends = false; variables.weekdays = false; variables.lastBusinessDay = false; + variables.noOverlaps = false; // Probable Scheduler or not variables.scheduler = ""; // Prepare execution tracking stats @@ -271,6 +279,14 @@ component accessors="true" { return this; } + /** + * Enable the task when disabled so we can run again + */ + ScheduledTask function enable(){ + variables.disabled = false; + return this; + } + /** * Verifies if we can schedule this task or not by looking at the following constraints: * @@ -369,7 +385,12 @@ component accessors="true" { * @return A ScheduledFuture from where you can monitor the task, an empty ScheduledFuture if the task was not registered */ ScheduledFuture function start(){ - // Startup a spaced frequency task + // If we have overlaps and the spaced delay is 0 then grab it from the period + if ( variables.noOverlaps and variables.spacedDelay eq 0 ) { + variables.spacedDelay = variables.period; + } + + // Startup a spaced frequency task: no overlaps if ( variables.spacedDelay > 0 ) { return variables.executor.scheduleWithFixedDelay( task : this, @@ -476,6 +497,18 @@ component accessors="true" { return this; } + /** + * Calling this method prevents task frequencies to overlap. By default all tasks are executed with an + * interval but ccould potentially overlap if they take longer to execute than the period. + * + * @period + * @timeUnit + */ + ScheduledTask function withNoOverlaps(){ + variables.noOverlaps = true; + return this; + } + /** * Run the task every custom period of execution * diff --git a/tests/specs/async/tasks/ScheduledTaskSpec.cfc b/tests/specs/async/tasks/ScheduledTaskSpec.cfc index 938c42787..43ce9d986 100644 --- a/tests/specs/async/tasks/ScheduledTaskSpec.cfc +++ b/tests/specs/async/tasks/ScheduledTaskSpec.cfc @@ -24,7 +24,7 @@ component extends="tests.specs.async.BaseAsyncSpec" { expect( t.hasScheduler() ).toBeTrue(); } ); - it( "can call when", function(){ + it( "can have truth based restrictions using when()", function(){ var t = scheduler .task( "test" ) .when( function(){ @@ -44,37 +44,39 @@ component extends="tests.specs.async.BaseAsyncSpec" { expect( t.disable().isDisabled() ).toBeTrue(); } ); - it( "can call before", function(){ - var t = scheduler - .task( "test" ) - .before( function(){ - return true; - } ); - expect( isClosure( t.getBeforeTask() ) ).toBeTrue(); - } ); - it( "can call after", function(){ - var t = scheduler - .task( "test" ) - .after( function(){ - return true; - } ); - expect( isClosure( t.getafterTask() ) ).toBeTrue(); - } ); - it( "can call onTaskSuccess", function(){ - var t = scheduler - .task( "test" ) - .onSuccess( function(){ - return true; - } ); - expect( isClosure( t.getonTaskSuccess() ) ).toBeTrue(); - } ); - it( "can call onTaskFailure", function(){ - var t = scheduler - .task( "test" ) - .onFailure( function(){ - return true; - } ); - expect( isClosure( t.getonTaskFailure() ) ).toBeTrue(); + describe( "can have life cycle methods", function(){ + it( "can call before", function(){ + var t = scheduler + .task( "test" ) + .before( function(){ + return true; + } ); + expect( isClosure( t.getBeforeTask() ) ).toBeTrue(); + } ); + it( "can call after", function(){ + var t = scheduler + .task( "test" ) + .after( function(){ + return true; + } ); + expect( isClosure( t.getafterTask() ) ).toBeTrue(); + } ); + it( "can call onTaskSuccess", function(){ + var t = scheduler + .task( "test" ) + .onSuccess( function(){ + return true; + } ); + expect( isClosure( t.getonTaskSuccess() ) ).toBeTrue(); + } ); + it( "can call onTaskFailure", function(){ + var t = scheduler + .task( "test" ) + .onFailure( function(){ + return true; + } ); + expect( isClosure( t.getonTaskFailure() ) ).toBeTrue(); + } ); } ); describe( "can register multiple frequencies using everyXXX() method calls", function(){ @@ -225,6 +227,16 @@ component extends="tests.specs.async.BaseAsyncSpec" { ); } ); } ); + + it( "can register tasks with no overlaps", function(){ + var t = scheduler + .task( "test" ) + .everyMinute() + .withNoOverlaps(); + expect( t.getPeriod() ).toBe( 1 ); + expect( t.getNoOverlaps() ).toBeTrue(); + expect( t.getTimeUnit() ).toBe( "minutes" ); + } ); } ); } From a6c27d38108ca78f039b8c50e19ce92151190ba0 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Fri, 16 Apr 2021 16:03:37 -0500 Subject: [PATCH 37/40] more frequenc9es and utilities --- system/async/tasks/ScheduledTask.cfc | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/system/async/tasks/ScheduledTask.cfc b/system/async/tasks/ScheduledTask.cfc index b9e2a43bf..65d1a65a4 100644 --- a/system/async/tasks/ScheduledTask.cfc +++ b/system/async/tasks/ScheduledTask.cfc @@ -120,6 +120,11 @@ component accessors="true" { */ property name="noOverlaps" type="boolean"; + /** + * Get the ColdBox utility object + */ + property name="util"; + /** * Constructor * @@ -598,7 +603,7 @@ component accessors="true" { } /** - * Set the period to be daily at a specific time in 24 hour format: HH:mm + * Run the task daily with a specific time in 24 hour format: HH:mm * We will always add 0 seconds for you. * * @time The specific time using 24 hour format => HH:mm @@ -970,7 +975,7 @@ component accessors="true" { } /** - * Set the period to be on saturday and sundays + * Run the task on saturday and sundays * * @time The specific time using 24 hour format => HH:mm, defaults to 00:00 */ From f5874ce2f954485f7b605967d8dbf50a1ba76875 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Fri, 16 Apr 2021 17:32:13 -0500 Subject: [PATCH 38/40] constraints now active --- system/async/tasks/ScheduledTask.cfc | 119 +++++++++++++++++----- system/web/tasks/ColdBoxScheduledTask.cfc | 16 ++- 2 files changed, 103 insertions(+), 32 deletions(-) diff --git a/system/async/tasks/ScheduledTask.cfc b/system/async/tasks/ScheduledTask.cfc index 65d1a65a4..bfa34315d 100644 --- a/system/async/tasks/ScheduledTask.cfc +++ b/system/async/tasks/ScheduledTask.cfc @@ -299,18 +299,7 @@ component accessors="true" { * - when closure */ boolean function isDisabled(){ - // Disabled bit - if ( variables.disabled ) { - return true; - } - - // When Closure that dictates if the task can be scheduled: true => yes, false => no - if ( isClosure( variables.when ) ) { - return !variables.when( this ); - } - - // Not disabled - return false; + return variables.disabled; } /** @@ -319,6 +308,69 @@ component accessors="true" { * -------------------------------------------------------------------------- */ + /** + * This method verifies if the running task is constrained to run on specific valid constraints: + * + * - when + * - dayOfTheMonth + * - dayOfTheWeek + * - lastBusinessDay + * - weekends + * - weekdays + * + * This method is called by the `run()` method at runtime to determine if the task can be ran at that point in time + */ + boolean function isConstrained(){ + var now = variables.chronoUnitHelper.toLocalDateTime( now(), getTimezone() ); + + // When Closure that dictates if the task can be scheduled/ran: true => yes, false => no + if ( isClosure( variables.when ) && !variables.when( this ) ) { + return true; + } + + // Do we have a day of the month constraint? and the same as the running date/time? Else skip it + if ( + variables.dayOfTheMonth > 0 && + now.getDayOfMonth() != variables.dayOfTheMonth + ) { + return true; + } + + // Do we have a last business day constraint + if ( + variables.lastBusinessDay && + now.getDayOfMonth() != getLastDayOfTheMonth().getDayOfMonth() + ) { + return true; + } + + // Do we have weekends? + if ( + variables.weekends && + now.getDayOfWeek() <= 5 + ) { + return true; + } + + // Do we have weekdays? + if ( + variables.weekdays && + now.getDayOfWeek() > 5 + ) { + return true; + } + + // Do we have day of the week? + if ( + variables.dayOfTheWeek > 0 && + now.getDayOfWeek() != variables.dayOfTheWeek + ) { + return true; + } + + return false; + } + /** * This is the runnable proxy method that executes your code by the executors */ @@ -330,6 +382,11 @@ component accessors="true" { return; } + // Check for specific Constraints + if ( isConstrained() ) { + return; + } + // Init now as it is running variables.stats.neverRun = false; @@ -839,21 +896,13 @@ component accessors="true" { } /** - * Run the task on the last business day of the month - * - * @time The specific time using 24 hour format => HH:mm, defaults to midnight + * This utility method gives us the last day of the month in Java format */ - ScheduledTask function onLastBusinessDayOfTheMonth( string time = "00:00" ){ - var now = variables.chronoUnitHelper.toLocalDateTime( now(), getTimezone() ); - // Check for mintues else add them - if ( !find( ":", arguments.time ) ) { - arguments.time &= ":00"; - } - // Validate time format - validateTime( arguments.time ); - + private function getLastDayOfTheMonth(){ // Get the last day of the month - var lastDay = now.with( createObject( "java", "java.time.temporal.TemporalAdjusters" ).lastDayOfMonth() ); + var lastDay = variables.chronoUnitHelper + .toLocalDateTime( now(), getTimezone() ) + .with( createObject( "java", "java.time.temporal.TemporalAdjusters" ).lastDayOfMonth() ); // Verify if on weekend switch ( lastDay.getDayOfWeek().getValue() ) { // Sunday - 2 days @@ -868,8 +917,24 @@ component accessors="true" { } } - // Get new time - var nextRun = lastDay + return lastDay; + } + + /** + * Run the task on the last business day of the month + * + * @time The specific time using 24 hour format => HH:mm, defaults to midnight + */ + ScheduledTask function onLastBusinessDayOfTheMonth( string time = "00:00" ){ + var now = variables.chronoUnitHelper.toLocalDateTime( now(), getTimezone() ); + // Check for mintues else add them + if ( !find( ":", arguments.time ) ) { + arguments.time &= ":00"; + } + // Validate time format + validateTime( arguments.time ); + // Get the last day of the month + var nextRun = getLastDayOfTheMonth() // Specific Time .withHour( javacast( "int", getToken( arguments.time, 1, ":" ) ) ) .withMinute( javacast( "int", getToken( arguments.time, 2, ":" ) ) ) diff --git a/system/web/tasks/ColdBoxScheduledTask.cfc b/system/web/tasks/ColdBoxScheduledTask.cfc index 880762151..cba350270 100644 --- a/system/web/tasks/ColdBoxScheduledTask.cfc +++ b/system/web/tasks/ColdBoxScheduledTask.cfc @@ -92,15 +92,21 @@ component extends="coldbox.system.async.tasks.ScheduledTask" accessors="true" { } /** - * Verifies if we can schedule this task or not by looking at the following constraints: + * This method verifies if the running task is constrained to run on specific valid constraints: * - * - disabled + * - when + * - dayOfTheMonth + * - dayOfTheWeek + * - lastBusinessDay + * - weekends + * - weekdays * - environments - * - when closure + * + * This method is called by the `run()` method at runtime to determine if the task can be ran at that point in time */ - boolean function isDisabled(){ + boolean function isConstrained(){ // Call super and if disabled already, then just exit out. - if ( super.isDisabled() ) { + if ( super.isConstrained() ) { return true; } From 93b701c0e2cceb93f60581ea00d4cd0b5a52f7e2 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Mon, 19 Apr 2021 09:40:31 -0500 Subject: [PATCH 39/40] tonload of more constrain tests and fixing issues with constraints stopping eexecution --- system/async/tasks/ScheduledTask.cfc | 44 +++++---- tests/specs/async/tasks/ScheduledTaskSpec.cfc | 96 +++++++++++++++++++ 2 files changed, 123 insertions(+), 17 deletions(-) diff --git a/system/async/tasks/ScheduledTask.cfc b/system/async/tasks/ScheduledTask.cfc index bfa34315d..4b2fd7bc7 100644 --- a/system/async/tasks/ScheduledTask.cfc +++ b/system/async/tasks/ScheduledTask.cfc @@ -162,6 +162,7 @@ component accessors="true" { variables.disabled = false; variables.when = ""; variables.dayOfTheMonth = 0; + variables.dayOfTheWeek = 0; variables.weekends = false; variables.weekdays = false; variables.lastBusinessDay = false; @@ -170,6 +171,8 @@ component accessors="true" { variables.scheduler = ""; // Prepare execution tracking stats variables.stats = { + // Save name just in case + "name" : arguments.name, // When task got created "created" : now(), // The last execution run timestamp @@ -321,7 +324,7 @@ component accessors="true" { * This method is called by the `run()` method at runtime to determine if the task can be ran at that point in time */ boolean function isConstrained(){ - var now = variables.chronoUnitHelper.toLocalDateTime( now(), getTimezone() ); + var now = getJavaNow(); // When Closure that dictates if the task can be scheduled/ran: true => yes, false => no if ( isClosure( variables.when ) && !variables.when( this ) ) { @@ -347,7 +350,7 @@ component accessors="true" { // Do we have weekends? if ( variables.weekends && - now.getDayOfWeek() <= 5 + now.getDayOfWeek().getValue() <= 5 ) { return true; } @@ -355,7 +358,7 @@ component accessors="true" { // Do we have weekdays? if ( variables.weekdays && - now.getDayOfWeek() > 5 + now.getDayOfWeek().getValue() > 5 ) { return true; } @@ -363,7 +366,7 @@ component accessors="true" { // Do we have day of the week? if ( variables.dayOfTheWeek > 0 && - now.getDayOfWeek() != variables.dayOfTheWeek + now.getDayOfWeek().getValue() != variables.dayOfTheWeek ) { return true; } @@ -607,7 +610,7 @@ component accessors="true" { * @minutes The minutes past the hour mark */ ScheduledTask function everyHourAt( required numeric minutes ){ - var now = variables.chronoUnitHelper.toLocalDateTime( now(), getTimezone() ); + var now = getJavaNow(); var nextRun = now.withMinute( javacast( "int", arguments.minutes ) ).withSecond( javacast( "int", 0 ) ); // If we passed it, then move the hour by 1 if ( now.compareTo( nextRun ) > 0 ) { @@ -633,7 +636,7 @@ component accessors="true" { * Run the task every day at midnight */ ScheduledTask function everyDay(){ - var now = variables.chronoUnitHelper.toLocalDateTime( now(), getTimezone() ); + var now = getJavaNow(); // Set at midnight var nextRun = now .withHour( javacast( "int", 0 ) ) @@ -673,7 +676,7 @@ component accessors="true" { // Validate time format validateTime( arguments.time ); // Get times - var now = variables.chronoUnitHelper.toLocalDateTime( now(), getTimezone() ); + var now = getJavaNow(); var nextRun = now .withHour( javacast( "int", getToken( arguments.time, 1, ":" ) ) ) .withMinute( javacast( "int", getToken( arguments.time, 2, ":" ) ) ) @@ -702,7 +705,7 @@ component accessors="true" { * Run the task every Sunday at midnight */ ScheduledTask function everyWeek(){ - var now = variables.chronoUnitHelper.toLocalDateTime( now(), getTimezone() ); + var now = getJavaNow(); // Set at midnight var nextRun = now // Sunday @@ -738,7 +741,7 @@ component accessors="true" { * @time The specific time using 24 hour format => HH:mm, defaults to midnight */ ScheduledTask function everyWeekOn( required numeric dayOfWeek, string time = "00:00" ){ - var now = variables.chronoUnitHelper.toLocalDateTime( now(), getTimezone() ); + var now = getJavaNow(); // Check for mintues else add them if ( !find( ":", arguments.time ) ) { arguments.time &= ":00"; @@ -776,7 +779,7 @@ component accessors="true" { * Run the task on the first day of every month at midnight */ ScheduledTask function everyMonth(){ - var now = variables.chronoUnitHelper.toLocalDateTime( now(), getTimezone() ); + var now = getJavaNow(); // Set at midnight var nextRun = now // First day of the month @@ -813,7 +816,7 @@ component accessors="true" { * @time The specific time using 24 hour format => HH:mm, defaults to midnight */ ScheduledTask function everyMonthOn( required numeric day, string time = "00:00" ){ - var now = variables.chronoUnitHelper.toLocalDateTime( now(), getTimezone() ); + var now = getJavaNow(); // Check for mintues else add them if ( !find( ":", arguments.time ) ) { arguments.time &= ":00"; @@ -855,7 +858,7 @@ component accessors="true" { * @time The specific time using 24 hour format => HH:mm, defaults to midnight */ ScheduledTask function onFirstBusinessDayOfTheMonth( string time = "00:00" ){ - var now = variables.chronoUnitHelper.toLocalDateTime( now(), getTimezone() ); + var now = getJavaNow(); // Check for mintues else add them if ( !find( ":", arguments.time ) ) { arguments.time &= ":00"; @@ -926,7 +929,7 @@ component accessors="true" { * @time The specific time using 24 hour format => HH:mm, defaults to midnight */ ScheduledTask function onLastBusinessDayOfTheMonth( string time = "00:00" ){ - var now = variables.chronoUnitHelper.toLocalDateTime( now(), getTimezone() ); + var now = getJavaNow(); // Check for mintues else add them if ( !find( ":", arguments.time ) ) { arguments.time &= ":00"; @@ -964,7 +967,7 @@ component accessors="true" { * Run the task on the first day of the year at midnight */ ScheduledTask function everyYear(){ - var now = variables.chronoUnitHelper.toLocalDateTime( now(), getTimezone() ); + var now = getJavaNow(); // Set at midnight var nextRun = now // First day of the month @@ -1004,7 +1007,7 @@ component accessors="true" { required numeric day, required string time = "00:00" ){ - var now = variables.chronoUnitHelper.toLocalDateTime( now(), getTimezone() ); + var now = getJavaNow(); // Check for mintues else add them if ( !find( ":", arguments.time ) ) { arguments.time &= ":00"; @@ -1052,7 +1055,7 @@ component accessors="true" { // Validate time format validateTime( arguments.time ); // Get times - var now = variables.chronoUnitHelper.toLocalDateTime( now(), getTimezone() ); + var now = getJavaNow(); var nextRun = now .withHour( javacast( "int", getToken( arguments.time, 1, ":" ) ) ) .withMinute( javacast( "int", getToken( arguments.time, 2, ":" ) ) ) @@ -1093,7 +1096,7 @@ component accessors="true" { // Validate time format validateTime( arguments.time ); // Get times - var now = variables.chronoUnitHelper.toLocalDateTime( now(), getTimezone() ); + var now = getJavaNow(); var nextRun = now .withHour( javacast( "int", getToken( arguments.time, 1, ":" ) ) ) .withMinute( javacast( "int", getToken( arguments.time, 2, ":" ) ) ) @@ -1262,4 +1265,11 @@ component accessors="true" { } } + /** + * Get a Java localDateTime object using the current date/time and timezone + */ + function getJavaNow(){ + return variables.chronoUnitHelper.toLocalDateTime( now(), getTimezone() ); + } + } diff --git a/tests/specs/async/tasks/ScheduledTaskSpec.cfc b/tests/specs/async/tasks/ScheduledTaskSpec.cfc index 43ce9d986..624468b59 100644 --- a/tests/specs/async/tasks/ScheduledTaskSpec.cfc +++ b/tests/specs/async/tasks/ScheduledTaskSpec.cfc @@ -237,6 +237,102 @@ component extends="tests.specs.async.BaseAsyncSpec" { expect( t.getNoOverlaps() ).toBeTrue(); expect( t.getTimeUnit() ).toBe( "minutes" ); } ); + + describe( "can have multiple constraints", function(){ + it( "can have a truth value constraint", function(){ + var t = scheduler + .task( "test" ) + .when( function(){ + return false; + } ); + expect( t.isConstrained() ).toBeTrue(); + } ); + + it( "can have a day of the month constraint", function(){ + var t = scheduler.task( "test" ); + t.setDayOfTheMonth( day( dateAdd( "d", 1, now() ) ) ); + expect( t.isConstrained() ).toBeTrue(); + + t.setDayOfTheMonth( day( now() ) ); + expect( t.isConstrained() ).toBeFalse(); + } ); + + it( "can have a last business day of the month constraint", function(){ + var t = scheduler.task( "test" ).setLastBusinessDay( true ); + expect( t.isConstrained() ).toBeTrue(); + + var mockNow = t.getJavaNow(); + prepareMock( t ).$( "getLastDayOfTheMonth", mockNow ); + + expect( t.isConstrained() ).toBeFalse(); + } ); + + it( "can have a day of the week constraint", function(){ + var t = scheduler.task( "test" ); + var mockNow = t.getJavaNow(); + // Reduce date enough to do computations on it + if ( mockNow.getDayOfWeek().getValue() > 6 ) { + mockNow = mockNow.minusDays( javacast( "long", 3 ) ); + } + + t.setDayOfTheWeek( mockNow.getDayOfWeek().getValue() + 1 ); + expect( t.isConstrained() ).toBeTrue( "Constrained!!" ); + + t.setDayOfTheWeek( + t.getJavaNow() + .getDayOfWeek() + .getValue() + ); + expect( t.isConstrained() ).toBeFalse( "Should execute" ); + } ); + + it( "can have a weekend constraint", function(){ + var t = scheduler.task( "test" ).setWeekends( true ); + + // build a weekend date + var mockNow = t.getJavaNow(); + var dayOfWeek = mockNow.getDayOfWeek().getValue(); + + if ( dayOfWeek < 6 ) { + mockNow = mockNow.plusDays( javacast( "long", 6 - dayOfWeek ) ); + } + + prepareMock( t ).$( "getJavaNow", mockNow ); + expect( t.isConstrained() ).toBeFalse( + "Weekend day (#mockNow.getDayOfWeek().getvalue()#) should pass" + ); + + // Test non weekend + mockNow = mockNow.minusDays( javacast( "long", 3 ) ); + t.$( "getJavaNow", mockNow ); + expect( t.isConstrained() ).toBeTrue( + "Weekday (#mockNow.getDayOfWeek().getvalue()#) should be constrained" + ); + } ); + + it( "can have a weekday constraint", function(){ + var t = scheduler.task( "test" ).setWeekdays( true ); + + // build a weekday date + var mockNow = t.getJavaNow(); + var dayOfWeek = mockNow.getDayOfWeek().getValue(); + if ( dayOfWeek >= 6 ) { + mockNow = mockNow.minusDays( javacast( "long", 3 ) ); + } + + prepareMock( t ).$( "getJavaNow", mockNow ); + expect( t.isConstrained() ).toBeFalse( + "Weekday (#mockNow.getDayOfWeek().getvalue()#) should pass" + ); + + // Test weekend + mockNow = mockNow.plusDays( javacast( "long", 6 - mockNow.getDayOfWeek().getValue() ) ); + t.$( "getJavaNow", mockNow ); + expect( t.isConstrained() ).toBeTrue( + "Weekend (#mockNow.getDayOfWeek().getvalue()#) should be constrained" + ); + } ); + } ); } ); } From 4bcad51ce904b9d40673a2e17c7b682bd56cc54d Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Mon, 19 Apr 2021 15:35:40 -0500 Subject: [PATCH 40/40] server fixation finalized and tested --- system/async/tasks/ScheduledTask.cfc | 16 +++- system/web/tasks/ColdBoxScheduledTask.cfc | 79 +++++++++++++++-- system/web/tasks/ColdBoxScheduler.cfc | 26 +++++- .../async/tasks/ColdBoxScheduledTaskSpec.cfc | 84 +++++++++++++++++++ 4 files changed, 192 insertions(+), 13 deletions(-) create mode 100644 tests/specs/async/tasks/ColdBoxScheduledTaskSpec.cfc diff --git a/system/async/tasks/ScheduledTask.cfc b/system/async/tasks/ScheduledTask.cfc index 4b2fd7bc7..c6574ef6f 100644 --- a/system/async/tasks/ScheduledTask.cfc +++ b/system/async/tasks/ScheduledTask.cfc @@ -380,17 +380,17 @@ component accessors="true" { function run(){ var sTime = getTickCount(); - // If disabled, skip run + // If disabled or paused if ( isDisabled() ) { return; } - // Check for specific Constraints + // Check for constraints of execution if ( isConstrained() ) { return; } - // Init now as it is running + // Mark the task as it wil run now for the first time variables.stats.neverRun = false; try { @@ -440,9 +440,19 @@ component accessors="true" { variables.stats.lastRun = now(); variables.stats.totalRuns = variables.stats.totalRuns + 1; variables.stats.lastExecutionTime = getTickCount() - sTime; + // Call internal cleanups event + cleanupTaskRun(); } } + /** + * This method is called ALWAYS after a task runs, wether in failure or success but used internally for + * any type of cleanups + */ + function cleanupTaskRun(){ + // no cleanups for now + } + /** * This method registers the task into the executor and sends it for execution and scheduling. * This will not register the task for execution if the disabled flag or the constraints allow it. diff --git a/system/web/tasks/ColdBoxScheduledTask.cfc b/system/web/tasks/ColdBoxScheduledTask.cfc index cba350270..809ca3f74 100644 --- a/system/web/tasks/ColdBoxScheduledTask.cfc +++ b/system/web/tasks/ColdBoxScheduledTask.cfc @@ -41,6 +41,11 @@ component extends="coldbox.system.async.tasks.ScheduledTask" accessors="true" { */ property name="cacheName"; + /** + * How long does the server fixation lock remain for. Deafult is 60 minutes. + */ + property name="serverLockTimeout" type="numeric"; + /** * Constructor * @@ -58,11 +63,13 @@ component extends="coldbox.system.async.tasks.ScheduledTask" accessors="true" { // init super.init( argumentCollection = arguments ); // seed environments - variables.environments = []; + variables.environments = []; // Can we run on all servers, or just one - variables.serverFixation = false; + variables.serverFixation = false; + // How long in minutes will the lock be set for before it expires. + variables.serverLockTimeout = 60; // CacheBox Region - variables.cacheName = "template"; + variables.cacheName = "template"; return this; } @@ -101,11 +108,12 @@ component extends="coldbox.system.async.tasks.ScheduledTask" accessors="true" { * - weekends * - weekdays * - environments + * - server fixation * * This method is called by the `run()` method at runtime to determine if the task can be ran at that point in time */ boolean function isConstrained(){ - // Call super and if disabled already, then just exit out. + // Call super and if constrained already, then just exit out. if ( super.isConstrained() ) { return true; } @@ -118,15 +126,74 @@ component extends="coldbox.system.async.tasks.ScheduledTask" accessors="true" { ) ) { variables.log.info( - "Skipping task (#getName()#) as it is disabled in the current environment: #variables.controller.getSetting( "environment" )#" + "Skipping task (#getName()#) as it is constrained in the current environment: #variables.controller.getSetting( "environment" )#" ); return true; } - // Not disabled + // Server fixation constrained + if ( variables.serverFixation && !canRunOnThisServer() ) { + return true; + } + + // Not constrained, run it! return false; } + /** + * This method is called ALWAYS after a task runs, wether in failure or success but used internally for + * any type of cleanups + */ + function cleanupTaskRun(){ + // Cleanup server fixation locks after task execution, wether failure or success + // This way, the tasks can run on a round-robin approach on a clustered environment + // and not fixated on a specific server + getCache().clear( "cbtasks-server-fixation-#replace( getName(), " ", "-", "all" )#" ); + } + + /** + * Verifies if a task can run on the executed server by using our distributed cache lock strategy + */ + boolean function canRunOnThisServer(){ + var keyName = "cbtasks-server-fixation-#replace( getName(), " ", "-", "all" )#"; + // Get or set the lock, first one wins! + getCache().getOrSet( + // key + keyName, + // producer + function(){ + return { + "task" : getName(), + "lockOn" : now(), + "serverHost" : getStats().inetHost, + "serverIp" : getStats().localIp + }; + }, + // timeout in minutes: defaults to 60 minutes and 0 last access timeout + variables.serverLockTimeout, + 0 + ); + // Get the lock now. At least one server must have set it by now + var serverLock = getCache().get( keyName ); + // If no lock something really went wrong, so constrain it and log it + if ( isNull( serverLock ) || !isStruct( serverLock ) ) { + variables.log.error( + "Server lock for task (#getName()#) is null or not a struct, something is wrong with the cache set, please verify it with key (#keyName#).", + ( !isNull( serverLock ) ? serverLock : "" ) + ); + return false; + } + // Else, it exists, check we are the same server that locked! If true, then we can run it baby! + else if ( serverLock.serverHost eq getStats().inetHost && serverLock.serverIp eq getStats().localIp ) { + return true; + } else { + variables.log.info( + "Skipping task (#getName()#) as it is constrained to run on one server (#serverLock.serverHost#/#serverLock.serverIp#). This server (#getStats().inetHost#/#getStats().localIp#) is different." + ); + return false; + } + } + /** * This method retrieves the selected CacheBox provider that will be used for server fixation and much more. * diff --git a/system/web/tasks/ColdBoxScheduler.cfc b/system/web/tasks/ColdBoxScheduler.cfc index 57d7ce1d9..6f41c7a72 100644 --- a/system/web/tasks/ColdBoxScheduler.cfc +++ b/system/web/tasks/ColdBoxScheduler.cfc @@ -40,6 +40,16 @@ component */ property name="log"; + /** + * The cache name to use for server fixation and more. By default we use the template region + */ + property name="cacheName"; + + /** + * The bit that can be used to set all tasks created by this scheduler to always run on one server + */ + property name="serverFixation" type="boolean"; + /** * Constructor * @@ -57,13 +67,17 @@ component // Super init super.init( arguments.name, arguments.asyncManager ); // Controller - variables.controller = arguments.controller; + variables.controller = arguments.controller; // Register Log object - variables.log = variables.controller.getLogBox().getLogger( this ); + variables.log = variables.controller.getLogBox().getLogger( this ); // Register CacheBox - variables.cacheBox = arguments.controller.getCacheBox(); + variables.cacheBox = arguments.controller.getCacheBox(); // Register WireBox - variables.wireBox = arguments.controller.getWireBox(); + variables.wireBox = arguments.controller.getWireBox(); + // CacheBox Region + variables.cacheName = "template"; + // Server fixation + variables.serverFixation = false; return this; } @@ -88,6 +102,10 @@ component ) // Set ourselves into the task .setScheduler( this ) + // Set the default cachename into the task + .setCacheName( getCacheName() ) + // Server fixation + .setServerFixation( getServerFixation() ) // Set default timezone into the task .setTimezone( getTimezone().getId() ); diff --git a/tests/specs/async/tasks/ColdBoxScheduledTaskSpec.cfc b/tests/specs/async/tasks/ColdBoxScheduledTaskSpec.cfc new file mode 100644 index 000000000..2666e4b01 --- /dev/null +++ b/tests/specs/async/tasks/ColdBoxScheduledTaskSpec.cfc @@ -0,0 +1,84 @@ +component extends="tests.resources.BaseIntegrationTest" { + + /*********************************** BDD SUITES ***********************************/ + + function beforeAll(){ + variables.asyncManager = new coldbox.system.async.AsyncManager(); + super.beforeAll(); + } + + function run( testResults, testBox ){ + // all your suites go here. + describe( "ColdBox Scheduled Task", function(){ + beforeEach( function( currentSpec ){ + variables.scheduler = new coldbox.system.web.tasks.ColdBoxScheduler( + "bdd-tests", + variables.asyncManager, + getController() + ); + } ); + + afterEach( function( currentSpec ){ + getCache( "template" ).clearAll(); + variables.scheduler.shutdown(); + } ); + + it( "can register a ColdBox enhanced task", function(){ + var t = scheduler.task( "cbTask" ); + expect( t.hasScheduler() ).toBeTrue(); + expect( t.getCacheName() ).toBe( "template" ); + expect( t.getServerFixation() ).toBeFalse(); + expect( scheduler.getTaskRecord( "cbTask" ).task ).toBe( t ); + } ); + + it( "can register environment constraints", function(){ + var t = scheduler.task( "cbTask" ).onEnvironment( "development" ); + expect( t.getEnvironments() ).toInclude( "development" ); + } ); + + it( "can register server fixation", function(){ + var t = scheduler.task( "cbTask" ).onOneServer(); + expect( t.getServerFixation() ).toBeTrue(); + } ); + + it( "can be constrained by environment", function(){ + var t = scheduler.task( "cbTask" ); + + expect( t.isConstrained() ).toBeFalse(); + + // Constrain it + t.onEnvironment( "bogus" ); + expect( t.getEnvironments() ).toInclude( "bogus" ); + expect( t.isConstrained() ).toBeTrue(); + } ); + + it( "can be constrained by server", function(){ + var t = scheduler.task( "cbTask" ).onOneServer(); + var cacheKey = "cbtasks-server-fixation-#replace( t.getName(), " ", "-", "all" )#"; + + expect( t.isConstrained() ).toBeFalse(); + expect( t.getCache().getKeys() ).toInclude( cacheKey ); + + // Constrain it + t.cleanupTaskRun(); + expect( t.getCache().getKeys() ).notToInclude( cacheKey ); + t.getCache() + .set( + cacheKey, + { + "task" : t.getName(), + "lockOn" : now(), + "serverHost" : "10.10.10.10", + "serverIp" : "my.macdaddy.bogus.bdd.server" + }, + 5, + 0 + ); + expect( t.getCache().getKeys() ).toInclude( cacheKey ); + expect( t.isConstrained() ).toBeTrue(); + t.cleanupTaskRun(); + } ); + } ); + } + +}