First let's change the class to extend
OnPremiseAspectEngineBase
(which partially implements
IOnPremiseAspectEngine
). This has the type arguments of
IStarSignData
- the interface extending
aspect data which will be added to the
flow data, and
IAspectPropertyMetaData
- instead of
IElementPropertyMetaData
.
The existing constructor needs to change to match the OnPremiseAspectEngineBase
class. So it takes the additional arguments of a data file path, and a temporary data file path. This is the location of the data file that the aspect engine will use, and where to make a temporary copy if required.
The constructor will also read the data file containing the star signs into memory. This is done in another method so that it can also be used by the RefreshData
method when a new data file is downloaded (this is not applicable for this example as star sign data will not change).
public class SimpleOnPremiseEngine : OnPremiseAspectEngineBase<IStarSignData, IAspectPropertyMetaData>
{
public SimpleOnPremiseEngine(
string dataFilePath,
ILogger<AspectEngineBase<IStarSignData, IAspectPropertyMetaData>> logger,
Func<IPipeline, FlowElementBase<IStarSignData, IAspectPropertyMetaData>, IStarSignData> aspectDataFactory,
string tempDataFilePath)
: base(logger, aspectDataFactory, tempDataFilePath)
{
_dataFile = dataFilePath;
Init();
}
The Init
method in this example will simply read a CSV file with the start and end dates of each star sign, which looks like:
Aries,21/03,19/04
Taurus,20/04,20/05
Gemini,21/05,20/06
Cancer,21/06,22/07
...
and add each to a list of a new class named StarSign
which has the following simple implementation:
internal class StarSign
{
public StarSign(string name, DateTime start, DateTime end)
{
Name = name;
Start = start.AddYears(-start.Year + 1);
End = end.AddYears(-end.Year + 1);
}
public string Name { get; private set; }
public DateTime Start { get; private set; }
public DateTime End { get; private set; }
}
Note that the year of the start and end date are both set to 1, as the year should be ignored, but the year 0 cannot be used in a DateTime
.
The new Init
method looks like this:
private void Init()
{
var starSigns = new List<StarSign>();
using (TextReader reader = File.OpenText(_dataFile))
{
string line = reader.ReadLine();
while (line != null)
{
var columns = line.Split(',');
starSigns.Add(new StarSign(
columns[0],
DateTime.ParseExact(columns[1], @"yy/MM/dd", CultureInfo.InvariantCulture),
DateTime.ParseExact(columns[2], @"yy/MM/dd", CultureInfo.InvariantCulture)));
line = reader.ReadLine();
}
}
_starSigns = starSigns;
}
Now the abstract methods can be implemented to create a functional aspect engine.
public class SimpleOnPremiseEngine : OnPremiseAspectEngineBase<IStarSignData, IAspectPropertyMetaData>
{
public SimpleOnPremiseEngine(
string dataFilePath,
ILogger<AspectEngineBase<IStarSignData, IAspectPropertyMetaData>> logger,
Func<IPipeline, FlowElementBase<IStarSignData, IAspectPropertyMetaData>, IStarSignData> aspectDataFactory,
string tempDataFilePath)
: base(logger, aspectDataFactory, tempDataFilePath)
{
_dataFile = dataFilePath;
Init();
}
private string _dataFile;
private IList<StarSign> _starSigns;
private void Init()
{
var starSigns = new List<StarSign>();
using (TextReader reader = File.OpenText(_dataFile))
{
string line = reader.ReadLine();
while (line != null)
{
var columns = line.Split(',');
starSigns.Add(new StarSign(
columns[0],
DateTime.ParseExact(columns[1], @"yy/MM/dd", CultureInfo.InvariantCulture),
DateTime.ParseExact(columns[2], @"yy/MM/dd", CultureInfo.InvariantCulture)));
line = reader.ReadLine();
}
}
_starSigns = starSigns;
}
public override string ElementDataKey => "starsign";
public override IEvidenceKeyFilter EvidenceKeyFilter =>
new EvidenceKeyFilterWhitelist(new List<string>() { "date-of-birth" });
public override IList<IAspectPropertyMetaData> Properties => new List<IAspectPropertyMetaData>() {
new AspectPropertyMetaData(this, "starsign", typeof(string), "starsign", new List<string>(){"free"}, true),
};
public override string DataSourceTier => "free";
public override void RefreshData(string dataFileIdentifier)
{
Init();
}
public override void RefreshData(string dataFileIdentifier, Stream data)
{
throw new NotImplementedException();
}
protected override void ProcessEngine(IFlowData data, IStarSignData aspectData)
{
StarSignData starSignData = (StarSignData)aspectData;
if (data.TryGetEvidence("date-of-birth", out DateTime dateOfBirth))
{
var monthAndDay = new DateTime(1, dateOfBirth.Month, dateOfBirth.Day);
foreach (var starSign in _starSigns)
{
if (monthAndDay > starSign.Start &&
monthAndDay < starSign.End)
{
starSignData.StarSign = starSign.Name;
break;
}
}
}
else
{
starSignData.StarSign = "Unknown";
}
}
protected override void UnmanagedResourcesCleanup()
{
}
}
First let's change the class to extend
OnPremiseAspectEngineBase
(which partially implements
OnPremiseAspectEngine
). This has the type arguments of
StarSignData
- the interface extending
aspect data which will be added to the
flow data, and
AspectPropertyMetaData
- instead of
ElementPropertyMetaData
.
The existing constructor needs to change to match the OnPremiseAspectEngineBase
class. So it takes the additional arguments of a data file path, and a temporary data file path. This is the location of the data file that the aspect engine will use, and where to make a temporary copy if required.
The constructor will also read the data file containing the star signs into memory. This is done in another method so that it can also be used by the refreshData
method when a new data file is downloaded (this is not applicable for this example as star sign data is static).
public class SimpleOnPremiseEngine extends OnPremiseAspectEngineBase<StarSignData, AspectPropertyMetaData> {
public SimpleOnPremiseEngine(
String dataFile,
Logger logger,
ElementDataFactory<StarSignData> elementDataFactory,
String tempDir) throws IOException {
super(logger, elementDataFactory, tempDir);
this.dataFile = dataFile;
init();
}
The init
method in this example will simply read a CSV file with the start and end dates of each star sign, which looks like:
Aries,21/03,19/04
Taurus,20/04,20/05
Gemini,21/05,20/06
Cancer,21/06,22/07
...
and add each to a list of a new class named StarSign
which has the following simple implementation:
public class StarSign {
private final Calendar end;
private final Calendar start;
private final String name;
public StarSign(String name, String start, String end) {
this.name = name;
this.start = Calendar.getInstance();
String[] startDate = start.split("/");
this.start.set(
0,
Integer.parseInt(startDate[1]) - 1,
Integer.parseInt(startDate[0]));
this.end = Calendar.getInstance();
String[] endDate = end.split("/");
this.end.set(
0,
Integer.parseInt(endDate[1]) - 1,
Integer.parseInt(endDate[0]));
}
public String getName() {
return name;
}
public Calendar getStart() {
return start;
}
public Calendar getEnd() {
return end;
}
}
Note that the year of the start and end date are both set to 0, as the year should be ignored.
The new init
method looks like this:
private void init() throws IOException {
List<StarSign> starSigns = new ArrayList<>();
try (FileReader fileReader = new FileReader(dataFile)) {
try (BufferedReader reader = new BufferedReader(fileReader)) {
String line;
while ((line = reader.readLine()) != null) {
String[] columns = line.split(",");
starSigns.add(new StarSign(
columns[0],
columns[1],
columns[2]));
}
}
}
this.starSigns = starSigns;
}
Now the abstract methods can be implemented to create a functional aspect engine.
public class SimpleOnPremiseEngine extends OnPremiseAspectEngineBase<StarSignData, AspectPropertyMetaData> {
public SimpleOnPremiseEngine(
String dataFile,
Logger logger,
ElementDataFactory<StarSignData> elementDataFactory,
String tempDir) throws IOException {
super(logger, elementDataFactory, tempDir);
this.dataFile = dataFile;
init();
}
private final String dataFile;
private List<StarSign> starSigns;
private void init() throws IOException {
List<StarSign> starSigns = new ArrayList<>();
try (FileReader fileReader = new FileReader(dataFile)) {
try (BufferedReader reader = new BufferedReader(fileReader)) {
String line;
while ((line = reader.readLine()) != null) {
String[] columns = line.split(",");
starSigns.add(new StarSign(
columns[0],
columns[1],
columns[2]));
}
}
}
this.starSigns = starSigns;
}
@Override
public String getTempDataDirPath() {
return dataFile;
}
@Override
public Date getDataFilePublishedDate(String dataFileIdentifier) {
return null;
}
@Override
public Date getDataFileUpdateAvailableTime(String dataFileIdentifier) {
return null;
}
@Override
public void refreshData(String dataFileIdentifier) {
try {
init();
} catch (IOException e) {
logger.warn("There was an exception refreshing the data file" +
"'" + dataFileIdentifier + "'",
e);
}
}
@Override
public void refreshData(String dataFileIdentifier, byte[] data) {
}
@Override
protected void processEngine(FlowData data, StarSignData aspectData) throws Exception {
StarSignDataInternal starSignData = (StarSignDataInternal)aspectData;
TryGetResult<Date> date = data.tryGetEvidence("date-of-birth", Date.class);
if (date.hasValue()) {
Calendar dob = Calendar.getInstance();
dob.setTime(date.getValue());
Calendar monthAndDay = Calendar.getInstance();
monthAndDay.set(
0,
dob.get(Calendar.MONTH),
dob.get(Calendar.DATE));
for (StarSign starSign : starSigns) {
if (monthAndDay.compareTo(starSign.getStart()) >= 0 &&
monthAndDay.compareTo(starSign.getEnd()) <= 0) {
starSignData.setStarSign(starSign.getName());
break;
}
}
}
else
{
starSignData.setStarSign("Unknown");
}
}
@Override
public String getElementDataKey() {
return "starsign";
}
@Override
public EvidenceKeyFilter getEvidenceKeyFilter() {
return new EvidenceKeyFilterWhitelist(
Collections.singletonList("date-of-birth"),
String.CASE_INSENSITIVE_ORDER);
}
@Override
public List<AspectPropertyMetaData> getProperties() {
return Collections.singletonList(
(AspectPropertyMetaData) new AspectPropertyMetaDataDefault(
"starsign",
this,
"starsign",
String.class,
Collections.singletonList("free"),
true));
}
@Override
public String getDataSourceTier() {
return "free";
}
@Override
protected void unmanagedResourcesCleanup() {
}
}
First let's change the class to extend engine
.
The existing constructor needs to change to match the engine
class. So it takes the additional argument of a data file path. This is the location of the data file that the aspect engine will use.
The constructor will also read the data file containing the star signs into memory. This is done in another method so that it can also be used when a new data file is downloaded (this is not applicable for this example as star sign data is static).
// Astrology flowElement
class Astrology extends FiftyOnePipelineEngines.Engine {
constructor ({ datafile }) {
super(...arguments);
// Create a datafile including a filesystem watcher that checks if
// the datafile has changed. Test by changing the names of the
// starsigns to see it update
this.dataFile = new FiftyOnePipelineEngines
.DataFile(
{
flowElement: this,
path: datafile,
autoUpdate: false,
fileSystemWatcher: true
}
);
this.registerDataFile(this.dataFile);
// datakey used to categorise data coming back from
// this flowElement in a pipeline
this.dataKey = 'astrology';
// A filter (in this case a basic list) stating which evidence the
// flowElement is interested in, in this case a query string
this.evidenceKeyFilter = new FiftyOnePipelineCore
.BasicListEvidenceKeyFilter(['query.dateOfBirth']);
// Update the datafile
this.refresh();
}
The refresh
method in this example will simply read a JSON file with the start and end dates of each star sign, which looks like:
[
[
"Aries",
"21/03",
"19/04"
],
[
"Taurus",
"20/04",
"20/05"
],
...
and parse into objects.
The new refresh
method looks like this:
// A function called when the datafile is updated / refreshed. In this
// case it simply loads the JSON from the file into the engine's memory.
refresh () {
const engine = this;
fs.readFile(this.dataFile.path, 'utf8', function (err, data) {
if (err) {
return console.error(err);
}
data = JSON.parse(data);
// Load the datafile into memory and parse it to make it
// more easily readable
data = data.map(function (e) {
const start = e[1].split('/');
const end = e[2].split('/');
return {
starsign: e[0],
startMonth: parseInt(start[1]),
startDate: parseInt(start[0]),
endMonth: parseInt(end[1]),
endDate: parseInt(end[0])
};
});
engine.data = data;
});
}
Now the abstract methods can be implemented to create a functional aspect engine.
//! [constructor]
// Astrology flowElement
class Astrology extends FiftyOnePipelineEngines.Engine {
constructor ({ datafile }) {
super(...arguments);
// Create a datafile including a filesystem watcher that checks if
// the datafile has changed. Test by changing the names of the
// starsigns to see it update
this.dataFile = new FiftyOnePipelineEngines
.DataFile(
{
flowElement: this,
path: datafile,
autoUpdate: false,
fileSystemWatcher: true
}
);
this.registerDataFile(this.dataFile);
// datakey used to categorise data coming back from
// this flowElement in a pipeline
this.dataKey = 'astrology';
// A filter (in this case a basic list) stating which evidence the
// flowElement is interested in, in this case a query string
this.evidenceKeyFilter = new FiftyOnePipelineCore
.BasicListEvidenceKeyFilter(['query.dateOfBirth']);
// Update the datafile
this.refresh();
}
//! [constructor]
//! [refresh]
// A function called when the datafile is updated / refreshed. In this
// case it simply loads the JSON from the file into the engine's memory.
refresh () {
const engine = this;
fs.readFile(this.dataFile.path, 'utf8', function (err, data) {
if (err) {
return console.error(err);
}
data = JSON.parse(data);
// Load the datafile into memory and parse it to make it
// more easily readable
data = data.map(function (e) {
const start = e[1].split('/');
const end = e[2].split('/');
return {
starsign: e[0],
startMonth: parseInt(start[1]),
startDate: parseInt(start[0]),
endMonth: parseInt(end[1]),
endDate: parseInt(end[0])
};
});
engine.data = data;
});
}
//! [refresh]
// Internal processing function
processInternal (flowData) {
let dateOfBirth = flowData.evidence.get('query.dateOfBirth');
// Collect data to save back into the flowData under this engine
const result = {};
// Lookup the date of birth using the provided and now parsed datafile
if (dateOfBirth) {
dateOfBirth = dateOfBirth.split('-');
const month = parseInt(dateOfBirth[1]);
const day = parseInt(dateOfBirth[2]);
result.starSign = this.data.filter(function (date) {
// Find starsigns in the correct month
if (date.startMonth === month) {
if (date.startDate > day) {
return false;
} else if (date.endMonth === month && date.endDate <= day) {
return false;
} else {
return true;
}
} else if (date.endMonth === month) {
if (date.endDate < day) {
return false;
} else {
return true;
}
} else {
return false;
}
})[0].starsign;
};
// Save the data into an extension of the elementData class
// (in this case a simple dictionary subclass)
const data = new FiftyOnePipelineCore.ElementDataDictionary({
flowElement: this, contents: result
});
// Set this data on the flowElement
flowData.setElementData(data);
}
}