2020-05-15 13:47:54 +02:00
package org.hso.ecommerce.controller.cronjob ;
2020-05-25 17:08:15 +02:00
import org.hso.ecommerce.action.booking.CreateBookingAction ;
import org.hso.ecommerce.action.cronjob.ReadSupplierDataAction ;
import org.hso.ecommerce.action.cronjob.ReadSupplierDataAction.ArticleIdentifier ;
import org.hso.ecommerce.action.cronjob.ReorderAction ;
import org.hso.ecommerce.action.cronjob.UpdateOffersAction ;
import org.hso.ecommerce.entities.booking.Booking ;
import org.hso.ecommerce.entities.booking.BookingAccountEntry ;
import org.hso.ecommerce.entities.booking.BookingReason ;
2020-05-15 13:47:54 +02:00
import org.hso.ecommerce.entities.cron.BackgroundJob ;
2020-05-25 17:08:15 +02:00
import org.hso.ecommerce.entities.shop.Article ;
import org.hso.ecommerce.entities.supplier.ArticleOffer ;
import org.hso.ecommerce.entities.supplier.Supplier ;
import org.hso.ecommerce.entities.supplier.SupplierOrder ;
import org.hso.ecommerce.repos.booking.BookingAccountEntryRepository ;
import org.hso.ecommerce.repos.booking.BookingRepository ;
2020-05-15 13:47:54 +02:00
import org.hso.ecommerce.repos.cronjob.BackgroundJobRepository ;
2020-06-15 17:47:55 +02:00
import org.hso.ecommerce.repos.dashboard.DashboardSummaryRepository ;
2020-05-25 17:08:15 +02:00
import org.hso.ecommerce.repos.shop.ArticleRepository ;
2020-06-01 20:24:59 +02:00
import org.hso.ecommerce.repos.shop.CustomerOrderRepository ;
2020-05-25 17:08:15 +02:00
import org.hso.ecommerce.repos.supplier.ArticleOfferRepository ;
import org.hso.ecommerce.repos.supplier.SupplierOrderRepository ;
import org.hso.ecommerce.repos.supplier.SupplierRepository ;
2020-06-15 17:47:55 +02:00
import org.hso.ecommerce.repos.user.UserRepository ;
import org.hso.ecommerce.repos.warehouse.SlotRepository ;
2020-05-25 17:08:15 +02:00
import org.hso.ecommerce.repos.warehouse.WarehouseBookingPositionSlotEntryRepository ;
2020-05-15 13:47:54 +02:00
import org.slf4j.Logger ;
import org.slf4j.LoggerFactory ;
import org.springframework.beans.factory.annotation.Autowired ;
import org.springframework.stereotype.Component ;
2020-06-15 16:23:42 +02:00
import org.springframework.ui.Model ;
import org.springframework.web.bind.annotation.GetMapping ;
import org.springframework.web.bind.annotation.PathVariable ;
import org.springframework.web.bind.annotation.PostMapping ;
import org.springframework.web.bind.annotation.RequestMapping ;
2020-05-15 13:47:54 +02:00
2020-06-15 17:47:55 +02:00
import javax.annotation.PostConstruct ;
import java.sql.Timestamp ;
import java.util.* ;
import java.util.Map.Entry ;
2020-05-25 17:08:15 +02:00
interface ICronjob {
/ * *
* Calculate the earliest cronjob execution time that happens after the given reference time .
*
* @param reference Position in time to start searching . The implementor is allowed to modify the reference time .
* @return A new Calendar instance ( or the same ) containing the time for next execution .
* /
Calendar nextExecution ( Calendar reference ) ;
/ * *
* Calculate the latest cronjob execution time that happens before or exactly at the given refernce time .
*
* @param reference Position in time to start searching . The implementor is allowed to modify the reference time .
* @return A new Calendar instance ( or the same ) containing the time of the last execution .
* /
Calendar previousExecution ( Calendar reference ) ;
/ * *
* Execute this cronjob .
*
* @param time The point in time this execution was scheduled . In case of a missed cronjob , the actual time of
* this call might be much later .
* @param controller Back - reference that allows to use repositories .
* /
void executeAt ( Calendar time , CronjobController controller ) ;
2020-06-15 16:23:42 +02:00
/ * *
* Get a name for this cronjob , that can be presented to the user in the frontend .
*
* @return A german name of this cronjob .
* /
String getDisplayName ( ) ;
2020-05-25 17:08:15 +02:00
}
@Component
class Reorder implements ICronjob {
2020-06-15 16:23:42 +02:00
@Override
public String getDisplayName ( ) {
return " Nachbestellung " ;
}
2020-05-25 17:08:15 +02:00
@Override
public Calendar nextExecution ( Calendar reference ) {
if ( reference . get ( Calendar . HOUR_OF_DAY ) > = 8 ) {
reference . add ( Calendar . DAY_OF_MONTH , 1 ) ;
}
reference . set ( Calendar . HOUR_OF_DAY , 8 ) ;
reference . set ( Calendar . MINUTE , 0 ) ;
reference . set ( Calendar . SECOND , 0 ) ;
reference . set ( Calendar . MILLISECOND , 0 ) ;
return reference ;
}
@Override
public Calendar previousExecution ( Calendar reference ) {
if ( reference . get ( Calendar . HOUR_OF_DAY ) < 8 ) {
reference . add ( Calendar . DAY_OF_MONTH , - 1 ) ;
}
reference . set ( Calendar . HOUR_OF_DAY , 8 ) ;
reference . set ( Calendar . MINUTE , 0 ) ;
reference . set ( Calendar . SECOND , 0 ) ;
reference . set ( Calendar . MILLISECOND , 0 ) ;
return reference ;
}
/ * *
* Calculates the amount of ordered articles by customers for the given article type in the time between begin and
* end .
*
* @param article The article to search orders for .
* @param begin The start time for the search ( included )
* @param end The end time for the search ( excluded )
* @return The number of articles that were ordered by customers in the given range .
* /
private Integer getOrderedAmounts ( Article article , Calendar begin , Calendar end , CronjobController controller ) {
return controller . customerOrderRepository . countOrdersOfArticleInTimespan (
article . id ,
new Timestamp ( begin . getTimeInMillis ( ) ) , new Timestamp ( end . getTimeInMillis ( ) ) ) ;
}
/ * *
* Calculates the amount of ordered articles by customers for the given article type in the three days before the
* given reference time . The return - array contains 3 fields : Index 0 : Orders 72 to 48 hours ago ; Index 1 : Orders 48
* to 24 hours ago ; Index 2 : Orders 24 to 0 hours ago .
*
* @param article The article for which the customer orders are checked .
* @param time The reference time to use for calculation of the last orders .
* @return A 3 - element array containing the orders of the last three days .
* /
private Integer [ ] getOrderedAmounts ( Article article , Calendar time , CronjobController controller ) {
Calendar oneDayBefore = ( Calendar ) time . clone ( ) ;
oneDayBefore . add ( Calendar . DAY_OF_MONTH , - 1 ) ;
Calendar twoDaysBefore = ( Calendar ) time . clone ( ) ;
twoDaysBefore . add ( Calendar . DAY_OF_MONTH , - 2 ) ;
Calendar threeDaysBefore = ( Calendar ) time . clone ( ) ;
threeDaysBefore . add ( Calendar . DAY_OF_MONTH , - 3 ) ;
return new Integer [ ] { //
getOrderedAmounts ( article , threeDaysBefore , twoDaysBefore , controller ) , //
getOrderedAmounts ( article , twoDaysBefore , oneDayBefore , controller ) , //
getOrderedAmounts ( article , oneDayBefore , time , controller ) , //
} ;
}
private HashMap < ArticleIdentifier , ArticleOffer > mapArticleOffers ( List < ArticleOffer > articleOffers ) {
HashMap < ArticleIdentifier , ArticleOffer > map = new HashMap < > ( ) ;
for ( ArticleOffer articleOffer : articleOffers ) {
ArticleIdentifier identifier = new ArticleIdentifier ( articleOffer . manufacturer , articleOffer . articleNumber ) ;
map . put ( identifier , articleOffer ) ;
}
return map ;
}
@Override
public void executeAt ( Calendar time , CronjobController controller ) {
List < Supplier > suppliers = controller . supplierRepository . findAll ( ) ;
ReadSupplierDataAction . Result supplierData = new ReadSupplierDataAction ( suppliers ) . finish ( ) ;
// Save the new offers in the database
List < ArticleOffer > allOffers = controller . articleOfferRepository . findAll ( ) ;
allOffers = new UpdateOffersAction ( allOffers , supplierData . cheapestOffer ) . finish ( ) ;
controller . articleOfferRepository . saveAll ( allOffers ) ;
HashMap < ArticleIdentifier , ArticleOffer > mappedOffers = mapArticleOffers ( allOffers ) ;
// Reorder
List < Article > allArticles = controller . articleRepository . findAll ( ) ;
for ( Article article : allArticles ) {
Integer [ ] orderedAmounts = getOrderedAmounts ( article , time , controller ) ;
Integer undeliveredReorders = controller . supplierOrderRepository
. countUndeliveredReorders ( article . related . articleNumber ) ;
2020-06-20 23:24:06 +02:00
int amountInStock = controller . warehouseBookingPositionSlotEntryRepository
. getByArticle ( article . id )
. stream ( )
. mapToInt ( e - > e . newSumSlot )
. sum ( ) ;
2020-05-25 17:08:15 +02:00
ReorderAction action = new ReorderAction ( article , orderedAmounts ,
undeliveredReorders ,
2020-05-28 10:58:11 +02:00
amountInStock ,
2020-05-25 17:08:15 +02:00
supplierData . cheapestOffer , mappedOffers ) ;
SupplierOrder order = action . finish ( ) ;
if ( order ! = null ) {
controller . supplierOrderRepository . save ( order ) ;
// Create bookings for this order
int netPrice = order . totalPriceNet ;
int vatPercent = order . ordered . vatPercent ;
int vatAmount = netPrice * vatPercent / 100 ;
int grossPrice = netPrice + vatAmount ;
// Obligation towards the supplier
BookingAccountEntry mainAccount = controller . bookingAccountEntryRepository . getByMain ( )
. orElseGet ( BookingAccountEntry : : newMain ) ;
BookingAccountEntry supplierAccount = controller . bookingAccountEntryRepository
. getBySupplier ( order . supplier . id )
. orElseGet ( ( ) - > BookingAccountEntry . newSupplier ( order . supplier ) ) ;
BookingReason obligationReason = new BookingReason ( order ) ;
Booking obligationBooking = new CreateBookingAction ( mainAccount ,
supplierAccount ,
obligationReason ,
grossPrice ) . finish ( ) ;
controller . bookingRepository . save ( obligationBooking ) ;
// Input Tax
BookingAccountEntry vatAccount = controller . bookingAccountEntryRepository . getByVat ( )
. orElseGet ( BookingAccountEntry : : newVat ) ;
mainAccount = controller . bookingAccountEntryRepository . getByMain ( ) . get ( ) ;
BookingReason inputTaxReason = new BookingReason ( order ) ;
Booking inputTaxBooking = new CreateBookingAction ( vatAccount , mainAccount , inputTaxReason , vatAmount )
. finish ( ) ;
controller . bookingRepository . save ( inputTaxBooking ) ;
}
}
}
}
2020-05-15 13:47:54 +02:00
class ScheduledCronjob {
public final Calendar executionTime ;
public final ICronjob cronjob ;
public final BackgroundJob model ;
public ScheduledCronjob ( Calendar executionTime , ICronjob cronjob , BackgroundJob model ) {
this . executionTime = executionTime ;
this . cronjob = cronjob ;
this . model = model ;
}
}
@Component
2020-06-15 16:23:42 +02:00
@RequestMapping ( " intern/cronjobs " )
2020-05-15 13:47:54 +02:00
class CronjobController {
private static final Logger log = LoggerFactory . getLogger ( CronjobController . class ) ;
private static final Map < String , ICronjob > cronjobs = getCronjobs ( ) ;
@Autowired
private final BackgroundJobRepository cronjobRepository = null ;
2020-06-15 17:47:55 +02:00
@Autowired
final SlotRepository slotRepository = null ;
2020-05-25 17:08:15 +02:00
@Autowired
final ArticleRepository articleRepository = null ;
@Autowired
final ArticleOfferRepository articleOfferRepository = null ;
@Autowired
2020-06-01 20:24:59 +02:00
final CustomerOrderRepository customerOrderRepository = null ;
2020-05-25 17:08:15 +02:00
@Autowired
final BookingRepository bookingRepository = null ;
@Autowired
final BookingAccountEntryRepository bookingAccountEntryRepository = null ;
@Autowired
final WarehouseBookingPositionSlotEntryRepository warehouseBookingPositionSlotEntryRepository = null ;
2020-06-15 17:47:55 +02:00
@Autowired
final DashboardSummaryRepository dashboardSummaryRepository = null ;
2020-05-25 17:08:15 +02:00
@Autowired
final SupplierRepository supplierRepository = null ;
@Autowired
final SupplierOrderRepository supplierOrderRepository = null ;
2020-06-15 17:47:55 +02:00
@Autowired
final UserRepository userRepository = null ;
2020-05-15 13:47:54 +02:00
private static Map < String , ICronjob > getCronjobs ( ) {
HashMap < String , ICronjob > map = new HashMap < > ( ) ;
// Register all existing cronjobs
map . put ( BackgroundJob . JOB_REORDER , new Reorder ( ) ) ;
2020-06-15 17:47:55 +02:00
map . put ( BackgroundJob . JOB_DASHBOARD , new DashboardCronjob ( ) ) ;
2020-05-15 13:47:54 +02:00
return Collections . unmodifiableMap ( map ) ;
}
private ScheduledCronjob getNextCronjob ( ) {
Calendar currentTime = new GregorianCalendar ( ) ;
2020-05-25 17:08:15 +02:00
Iterable < BackgroundJob > jobs = cronjobRepository . findAll ( ) ;
2020-05-15 13:47:54 +02:00
HashMap < String , BackgroundJob > alreadyExecuted = new HashMap < > ( ) ;
for ( BackgroundJob job : jobs ) {
alreadyExecuted . put ( job . jobName , job ) ;
}
ScheduledCronjob earliestJob = null ;
for ( Entry < String , ICronjob > entry : cronjobs . entrySet ( ) ) {
ScheduledCronjob resultingJob ;
BackgroundJob dbEntry = alreadyExecuted . get ( entry . getKey ( ) ) ;
if ( dbEntry ! = null ) {
Calendar previousExecution = new GregorianCalendar ( ) ;
previousExecution . setTimeInMillis ( dbEntry . lastExecution . getTime ( ) ) ;
Calendar followingSchedule = entry . getValue ( ) . nextExecution ( ( Calendar ) previousExecution . clone ( ) ) ;
Calendar lastSchedule = entry . getValue ( ) . previousExecution ( ( Calendar ) currentTime . clone ( ) ) ;
if ( lastSchedule . getTimeInMillis ( ) > followingSchedule . getTimeInMillis ( ) ) {
// This happens, if more than one execution was missed.
// In this case, run the job only once.
followingSchedule = lastSchedule ;
}
resultingJob = new ScheduledCronjob ( followingSchedule , entry . getValue ( ) , dbEntry ) ;
} else {
// This cronjob has never been executed before.
Calendar lastScheduleTime = entry . getValue ( ) . previousExecution ( ( Calendar ) currentTime . clone ( ) ) ;
BackgroundJob model = new BackgroundJob ( ) ;
model . jobName = entry . getKey ( ) ;
resultingJob = new ScheduledCronjob ( lastScheduleTime , entry . getValue ( ) , model ) ;
}
// Look for the job with earliest executionTime - it will run next
if ( earliestJob = = null
| | resultingJob . executionTime . getTimeInMillis ( ) < earliestJob . executionTime . getTimeInMillis ( ) ) {
earliestJob = resultingJob ;
}
}
return earliestJob ;
}
private void runCronjobExecutionLoop ( ) {
Thread . currentThread ( ) . setName ( " Cronjob " ) ;
try {
while ( true ) {
ScheduledCronjob nextJob = getNextCronjob ( ) ;
if ( nextJob = = null ) {
// In case there are no cronjobs
return ;
}
long waitingTime = nextJob . executionTime . getTimeInMillis ( ) - System . currentTimeMillis ( ) ;
if ( waitingTime > 0 ) {
Thread . sleep ( waitingTime ) ;
}
2020-05-25 17:08:15 +02:00
try {
nextJob . cronjob . executeAt ( nextJob . executionTime , this ) ;
} catch ( Throwable t ) {
log . error ( " Failed to execute cronjob " + nextJob . cronjob . getClass ( ) + " : " ) ;
t . printStackTrace ( ) ;
}
2020-05-15 13:47:54 +02:00
nextJob . model . lastExecution = new Timestamp ( nextJob . executionTime . getTimeInMillis ( ) ) ;
cronjobRepository . save ( nextJob . model ) ;
}
} catch ( InterruptedException e ) {
log . error ( " The cronjob execution thread has been interrupted " ) ;
}
}
@PostConstruct
public void onPostConstruct ( ) {
new Thread ( this : : runCronjobExecutionLoop ) . start ( ) ;
}
2020-06-15 16:23:42 +02:00
class ManualCronjob {
public final String identifier ;
public final String visibleName ;
public ManualCronjob ( String identifier , String visibleName ) {
this . identifier = identifier ;
this . visibleName = visibleName ;
}
}
private List < ManualCronjob > listAllCronjobs ( ) {
ArrayList < ManualCronjob > entries = new ArrayList < > ( ) ;
for ( Entry < String , ICronjob > job : cronjobs . entrySet ( ) ) {
entries . add ( new ManualCronjob ( job . getKey ( ) , job . getValue ( ) . getDisplayName ( ) ) ) ;
}
return entries ;
}
@GetMapping ( " / " )
public String cronjobOverview ( Model model ) {
model . addAttribute ( " jobs " , listAllCronjobs ( ) ) ;
return " intern/cronjobs/index " ;
}
@PostMapping ( " /run/{identifier} " )
public String runCronjob ( Model model , @PathVariable ( " identifier " ) String identifier ) {
ICronjob jobToExecute = cronjobs . get ( identifier ) ;
if ( jobToExecute ! = null ) {
jobToExecute . executeAt ( new GregorianCalendar ( ) , this ) ;
model . addAttribute ( " info " ,
String . format ( " Der Cronjob \" %s \" wurde ausgeführt. " , jobToExecute . getDisplayName ( ) ) ) ;
} else {
model . addAttribute ( " error " , " Der Cronjob konnte nicht gefunden werden. " ) ;
}
model . addAttribute ( " jobs " , listAllCronjobs ( ) ) ;
return " intern/cronjobs/index " ;
}
2020-05-15 13:47:54 +02:00
}