In previous steps, we spoke about general integration of MoSKito and WebUI and adding custom counters. Today, we are going to dive deeper and build own stats object.
Since the last post, we can count burgers and ingredients. This is great, this way we know how many burgers we’ve sold, and which ingredients we need to order. However, burgers can have different prices. To know how many burgers we’ve sold may be good for the kitchen folks, but for business people money is the most important thing.
Now let’s make things tense and count sales. For the sake of example, we will not only count our global burger sales, but also sales and average earnings per ingredient. This would give us an opportunity to find out which burgers are more popular. I mean, if we would really sell burgers… which we are not… or are we?
I. Building SalesStats
So first thing we need is an own stats object. We want to count the earning for an ingredient as well as the average earnings per ingredient. Average is something we can calculate on the fly. This means that we basically need to count two things, number of sales, and the cumulated earnings. That leads to the following class (some details omitted):
package de.zaunberg.burgershop.service.stats; import net.anotheria.moskito.core.predefined.Constants; import net.anotheria.moskito.core.producers.GenericStats; import net.anotheria.moskito.core.stats.StatValue; import net.anotheria.moskito.core.stats.TimeUnit; import net.anotheria.moskito.core.stats.impl.StatValueFactory; public class SalesStats extends GenericStats{ /** * The number of sales. */ private StatValue number; /** * The volume of sales. */ private StatValue volume; public SalesStats(String name) { super(name); number = StatValueFactory.createStatValue(Long.valueOf(0), "number", Constants.getDefaultIntervals()); volume = StatValueFactory.createStatValue(Long.valueOf(0), "volume", Constants.getDefaultIntervals()); } public void addSale(int priceInCents){ number.increase(); volume.increaseByInt(priceInCents); } public long getNumber(String intervalName){ return number.getValueAsLong(intervalName); } public long getVolume(String intervalName){ return volume.getValueAsLong(intervalName); } @Override public String toStatsString(String s, TimeUnit timeUnit) { return null; } }
Let’s look inside.
private StatValue number; private StatValue volume;
Those are our value holders. Since MoSKito gathers information for different intervals, we need a special StatValue* object instead of a simple long or int.
* To know more, read about the basic MoSKito concepts.
Of course, we also need a constructor:
public SalesStats(String name) { super(name); number = StatValueFactory.createStatValue(Long.valueOf(0), "number", Constants.getDefaultIntervals()); volume = StatValueFactory.createStatValue(Long.valueOf(0), "volume", Constants.getDefaultIntervals()); }
What happens here: we are going to create a new named SalesStats object. Since we are going to count ingredients, it seems a good idea to use the name of the ingredient as the name of the SalesStats object. The inherited AbstractStats class already cares for object naming and default intervals selection, so we can skip this step. We just need to create our StatValue objects.
To create a StatValue object, we have to call the appropriate factory method with three parameters:
- pattern – type of the counted objects. We are using Long. The factory knows that to count longs it needs objects of LongValueHolder type. Other supported types are int, double and string.
- the name of the value object is something that represents what this value stands for. In our case, these are volume and number. But it can also be something like request duration, request count, error count and so on.
- the intervals that should be supported. Usually we allow the object creator to provide the list of intervals from outside. This way you can configure your own intervals. However, in this case we stick to predefined intervals.
Finally, we need to tell the stats object that the values have been changed. We do this by calling the addSale method:
public void addSale(int priceInCents){ number.increase(); volume.increaseByInt(priceInCents); }
It increases the value of both value holders. Since the number only counts the number of sales, we increase it by one on each call. The volume indicates the money we earned in cents, so we have to increase it by the price of the burger.
The rest of the object are some methods, needed to interact with other elements.
Now we have to add this to our ShopServiceImpl. To gather some stats, we need a producer. Since ShopServiceImpl is already a producer via the @Monitor annotation, we need a special object for that. But it would be annoying to build an own producer class each time (which is surely possible), so MoSKito offers an out-of-the-box producer class, called OnDemandStatsProducer, that produces stat objects on request.
So whenever we need a new stat object, it just will be produced. But to be able to produce a stat object on demand, the OnDemandStatsProducer needs a factory, able to produce this very special object type. This is how this factory looks in our case:
package de.zaunberg.burgershop.service.stats; import net.anotheria.moskito.core.dynamic.IOnDemandStatsFactory; public class SalesStatsFactory implements IOnDemandStatsFactory<SalesStats>{ @Override public SalesStats createStatsObject(String name) { return new SalesStats(name); } }
I don’t think this needs commenting.
Now back to the ShowServiceImpl. First, we have to declare and create the producer:
public class ShopServiceImpl implements ShopService { ... private OnDemandStatsProducer<SalesStats> salesProducer; xt public ShopServiceImpl(){ salesProducer = new OnDemandStatsProducer<SalesStats>("sales", "business", "sales", new SalesStatsFactory()); ProducerRegistryFactory.getProducerRegistryInstance().registerProducer(salesProducer); }
What happens here: we create a new OnDemandStatsProducer instance and register it in the ProducerRegistry. ProducerRegistry is where all producers are located and where the WebUI and other tools look them up.
So, let’s build and make an order or two! As usual, mvn clean install to build and than order at: http://localhost:8080/burgershop/order.html?choice1=brioche&choice2=dog&choice3=cockroach.
Twice.
Now let’s go to WebUI and see if it’s there!
Good news: The Producer is there and gathers the data.
Bad news: They aren’t really useful yet. This is because the WebUI doesn’t know anything about our custom stat object and therefore doesn’t know how to render it.
There are special classes used to help the WebUI, and we are going to build one. They are called Decorators.
II. Building a Decorator
Building a decorator is relatively easy, you have to tell the WebUI what data you offer, and transform a StatObject into some beans. Here’s how our decorator looks like:
package de.zaunberg.burgershop.service.stats; import net.anotheria.moskito.core.producers.IStats; import net.anotheria.moskito.core.stats.TimeUnit; import net.anotheria.moskito.webui.decorators.AbstractDecorator; import net.anotheria.moskito.webui.producers.api.DoubleValueAO; import net.anotheria.moskito.webui.producers.api.LongValueAO; import net.anotheria.moskito.webui.producers.api.StatValueAO; import java.util.ArrayList; import java.util.List; /** * TODO comment this class * * @author lrosenberg * @since 12.12.13 23:24 */ public class SalesStatsDecorator extends AbstractDecorator { /** * Captions. */ static final String CAPTIONS[] = { "Sales", "Volume", "Avg" }; /** * Short explanations. */ static final String SHORT_EXPLANATIONS[] = { "Number of sales", "Total earnings", "Avg earnings" }; /** * Explanations. */ static final String EXPLANATIONS[] = { "Total number of sales for this ingredient", "Total earnings from sales of this ingredient", "Average earnings per sale for this ingredient", }; /** * Creates a new decorator object with given name. */ protected SalesStatsDecorator(){ super("Sales", CAPTIONS, SHORT_EXPLANATIONS, EXPLANATIONS); } @Override public List<StatValueAO> getValues(IStats statsObject, String interval, TimeUnit unit) { SalesStats stats = (SalesStats)statsObject; List<StatValueAO> ret = new ArrayList<StatValueAO>(CAPTIONS.length); int i = 0; long totalSales = stats.getNumber(interval); ret.add(new LongValueAO(CAPTIONS[i++], totalSales)); ret.add(new LongValueAO(CAPTIONS[i++], stats.getVolume(interval))); ret.add(new DoubleValueAO(CAPTIONS[i++], stats.getAverageVolume(interval))); return ret; } }
As you see, there are only two interesting methods here, first the constructor, which only has a call to the parent constructor:
super("Sales", CAPTIONS, SHORT_EXPLANATIONS, EXPLANATIONS);
The parameters are:
- Name of the Decorator.
- Captions for the values in the table view.
- Short explanations for the values for the mouseover (tooltips).
- Long explanations for help.
As you see, I simply pass the previously constructed arrays with constants. The results are these:
The explanations and captions helps the WebUI to draw the producer view as well as the help page.
Now let’s take a look at the other method:
public List<StatValueAO> getValues(IStats statsObject, String interval, TimeUnit unit).
This method is called for every StatsObject and expects a list of StatValueAO in return. It provides the currently selected interval and TimeUnit, thus allowing us to select the proper data for presentation. The inner life of the method is pretty simple, we cast the incoming object to one we know, and fill in some beans:
List<StatValueAO> ret = new ArrayList<StatValueAO>(CAPTIONS.length); int i = 0; long totalSales = stats.getNumber(interval); ret.add(new LongValueAO(CAPTIONS[i++], totalSales)); ret.add(new LongValueAO(CAPTIONS[i++], stats.getVolume(interval))); ret.add(new DoubleValueAO(CAPTIONS[i++], stats.getAverageVolume(interval))); return ret;
Note: the stats.getAverageVolume(interval) is missing in the Listing of the SalesStats class above, I added it as I wrote the decorator. It would be possible to calculate this value directly in the decorator, but it’s generally a better idea to calculate values in the stats objects themselves, making them available for other tools as well.
Note: you have to register you custom decorator in SalesStats like this
static{ DecoratorRegistryFactory.getDecoratorRegistry().addDecorator(SalesStats.class, new SalesStatsDecorator()); }
Finally, let’s look at the data we’ve got. I made two orders of the dog-cockroach burger, and one lamb burger:
http://localhost:8080/burgershop/order.html?choice1=brioche&choice2=dog&choice3=cockroach
http://localhost:8080/burgershop/order.html?choice1=brioche&choice2=lamb&choice3=cockroach
Now let’s take a look at our stats:
We see that we’ve sold 3 burgers with cockroach and brioche, 2 with dog and 1 with lamb. We also see our lamb burgers’ costs average more than other burgers, but brioche and cockroach are the most earning-bringers. However, this will change when we get some more data 😉
Now let’s order some burgers! 😉
In the next step, we will be adding some thresholds and accumulators to our Burger Empire.
Visit MoSKito homepage and MoSKito Documentation for more details.
The complete MoSKito integration guide – Step 6 – MoSKito Control | anotheria devblog
[…] of all, make sure you have the burgershop project locally in a proper state (for example step 3), or catch up with us, using the following […]
Derrick
спасибо.