/*
* Copyright 2010 kk-electronic a/s.
*
* This file is part of KKPortal.
*
* KKPortal is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* KKPortal is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with KKPortal. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.kk_electronic.kkportal.timereg.model;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.inject.Inject;
import com.kk_electronic.kkportal.core.util.Range;
import com.kk_electronic.kkportal.timereg.ui.TimeView;
/**
* @author Jes Andersen
*
*/
public class TimeRegistry {
//Range that is ready and received from the server
Range<Long> fetched = new Range<Long>();
//Range that we ave requested from the server
//If fetched.equals(requested) we do not have any outstanding requests
Range<Long> requested = new Range<Long>();
//The actual data
//For simplicity we use use a plain list
private final List<TimeEntry> entries = new LinkedList<TimeEntry>();
//List of displays interested in updates to the data
private List<TimeView> displays = new LinkedList<TimeView>();
private final TimeService timeService;
//Constructor takes a TimeService so we can actually fetch items from the backend
@Inject
public TimeRegistry(TimeService timeService) {
this.timeService = timeService;
}
//Checkin should add a new TimeEntry
public void checkin() {
if(!canCheckin()){
return;
}
//Without arguments creates the current local time
Date now = new Date();
//getTime is in milliseconds and TimeEntry uses seconds
final TimeEntry entry = new TimeEntry(now.getTime()/1000, null, null);
//This will call the service, fill out the id, and update displays
addNewEntry(entry);
}
//Checking is possible if we have fetched something from the server and the last
//entry is checked out
public boolean canCheckin() {
if (!fetched.isBounded()){
return false;
}
//Refuse to do a double checkin
TimeEntry e = getLast();
if(e != null && e.getCheckout() == null){
return false;
}
return true;
}
//Checkout is possible if the last entry is not checked out
public boolean canCheckout() {
TimeEntry e = getLast();
if(e != null && e.getCheckout() == null){
return true;
}
return false;
}
//Checkout should either update a entry if there is one to close or create a new one
public void checkout() {
if(!canCheckout()){
return;
}
Date now = new Date();
TimeEntry entry = getLast();
if (entry != null && entry.getCheckoutDate() == null) {
entry.setCheckoutDate(now);
timeService.update(entry, ignore);
updatedisplays();
} else {
addNewEntry(new TimeEntry(null, now.getTime()/1000, null));
}
}
//This is used by checkout to find an potential update entry
public TimeEntry getLast() {
if (entries.size() == 0)
return null;
return entries.get(entries.size() - 1);
}
//Add a new View to be called when the data changes
public void addDisplay(TimeView timeView) {
displays.add(timeView);
timeView.addRangeUpdatedHandler(rangeHandler);
Range<Long> viewrange = timeView.getRange();
if (viewrange == null || !viewrange.isBounded()) {
return;
}
if (fetched.contains(viewrange)) {
timeView.update(filterEntries(timeView.getRange()));
} else {
ensureRange(viewrange);
}
}
//Called when a TimeView updates the time range
private RangeUpdatedEvent.Handler rangeHandler = new RangeUpdatedEvent.Handler() {
@Override
public void onRangeUpdate(RangeUpdatedEvent event) {
if(fetched.contains(event.getRange())){
updatedisplays();
} else {
ensureRange(event.getRange());
}
}
};
//Function to update all displays added with addDisplay();
private void updatedisplays() {
//For each display we update it with a list fitting it's range
for (TimeView display : displays) {
display.update(filterEntries(display.getRange()));
}
}
//Filter the entries and return a list of entries that overlaps visible for a range
//User by updatedisplays() to send the appropriate data
private List<TimeEntry> filterEntries(Range<Long> range){
List<TimeEntry> result = new ArrayList<TimeEntry>();
for(TimeEntry entry:entries){
//If the entry ends before our range, we skip it
if(entry.getCheckout() != null && entry.getCheckout() < range.begin){
continue;
}
//If the entry begins after our range, we skip it
if(entry.getCheckin() != null && entry.getCheckin() > range.end){
continue;
}
//When we don't for sure it not there we add it
result.add(entry);
}
//TODO: Sort
return result;
}
AsyncCallback<Object> ignore = new AsyncCallback<Object>(){
@Override
public void onFailure(Throwable caught) {
GWT.log("TimeRegistry - Failed to update",caught);
}
@Override
public void onSuccess(Object result) {
GWT.log("TimeRegistry - Updated");
}
};
//Call the service to add a new entry and update the id when the call returns
private void addNewEntry(final TimeEntry entry) {
entries.add(entry);
timeService.add(entry, new AsyncCallback<Integer>() {
@Override
public void onSuccess(Integer result) {
entry.setId(result);
seen.add(result);
}
@Override
public void onFailure(Throwable caught) {
entries.remove(entry);
}
});
updatedisplays();
}
//Called to ensure a given range is in the data.
//In case of missing data callsrequestRangeFromService()
private void ensureRange(Range<Long> viewrange) {
//If the range is inside what is already requested we don't do anything
if (requested.contains(viewrange)) {
return;
} else {
//The requested range is expanded with the new range
if (requested.isBounded()){
requested.begin = Math.min(requested.begin, viewrange.begin);
requested.end = Math.max(requested.end, viewrange.end);
} else {
//if requested is null is copy the range
requested = viewrange.clone();
}
//if we have some data we need to construct the two new ranges we need
if (fetched.isBounded()) {
Range<Long> upperrange = requested.clone();
upperrange.begin = fetched.end;
Range<Long> lowerrange = requested.clone();
lowerrange.end = fetched.begin;
requestRangeFromService(lowerrange);
requestRangeFromService(upperrange);
} else {
//If we do not have any entries simply request the entire range
viewrange.clone();
requestRangeFromService(viewrange);
}
}
}
//Makes a call the the service to fetch new data
private void requestRangeFromService(final Range<Long> range) {
//Do a sanity check to prevent asking the server for senseless ranges
if (range.begin >= range.end){
return;
}
timeService.get(range.begin, range.end,
new AsyncCallback<List<TimeEntry>>() {
@Override
public void onSuccess(List<TimeEntry> result) {
//Add the new entries and update the fetched range
update(result,range);
GWT.log("TimeRegistry - Got entries:" + result.size());
}
@Override
public void onFailure(Throwable caught) {
GWT.log("TimeRegistry - Failed fetching entries",
caught);
}
});
}
//Set of seen id's
private Set<Integer> seen = new HashSet<Integer>();
//Adds a list of new data entries to the internal data structure
//It has a guard to protect against entries getting added multiple times.
protected void update(List<TimeEntry> result, Range<Long> range) {
fetched.extend(range);
for(TimeEntry entry: result){
//This prevents entries that spans from the requested into fetches
//from getting added multiple times
if(seen.add(entry.getId())){
//This part is only called if entry id was not seen before
entries.add(entry);
}
}
//Since we have changed the entries data, we update
updatedisplays();
}
}