package com.github.windbender.resources;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import org.hibernate.Hibernate;
import org.hibernate.proxy.HibernateProxy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.drew.imaging.ImageProcessingException;
import com.drew.imaging.jpeg.JpegMetadataReader;
import com.drew.metadata.Metadata;
import com.drew.metadata.exif.ExifSubIFDDirectory;
import com.drew.metadata.exif.GpsDirectory;
import com.github.windbender.auth.Priv;
import com.github.windbender.auth.SessionAuth;
import com.github.windbender.auth.SessionCurProj;
import com.github.windbender.auth.SessionUser;
import com.github.windbender.core.HibernateDataStore;
import com.github.windbender.core.IdentificationRequest;
import com.github.windbender.core.ImageStore;
import com.github.windbender.core.NextEventRecord;
import com.github.windbender.core.RegionUtil;
import com.github.windbender.core.SessionFilteredAuthorization;
import com.github.windbender.core.SpeciesCount;
import com.github.windbender.dao.ImageRecordDAO;
import com.github.windbender.dao.ReportDAO;
import com.github.windbender.dao.SpeciesDAO;
import com.github.windbender.domain.ImageEvent;
import com.github.windbender.domain.ImageRecord;
import com.github.windbender.domain.Project;
import com.github.windbender.domain.Species;
import com.github.windbender.domain.User;
import com.github.windbender.service.TimeZoneGetter;
import com.sun.jersey.api.ConflictException;
import com.sun.jersey.multipart.BodyPart;
import com.sun.jersey.multipart.FormDataMultiPart;
import com.yammer.dropwizard.hibernate.UnitOfWork;
import com.yammer.metrics.annotation.Timed;
@Path("/images/")
@Produces("image/jpeg")
@Consumes("image/jpeg")
public class ImageResource {
Logger log = LoggerFactory.getLogger(ImageResource.class);
private HibernateDataStore ds;
private ImageStore store;
ImageRecordDAO irDAO;
private SpeciesDAO speciesDAO;
private ReportDAO reportDAO;
private TimeZoneGetter timeZoneGetter;
public ImageResource(HibernateDataStore ds, ImageStore store, ImageRecordDAO irDAO, SpeciesDAO speciesDAO, ReportDAO reportDAO,TimeZoneGetter timeZoneGetter) {
this.ds = ds;
this.store = store;
this.irDAO = irDAO;
this.speciesDAO = speciesDAO;
this.reportDAO = reportDAO;
this.timeZoneGetter = timeZoneGetter;
}
@GET
@Timed
@Path("{id}")
@UnitOfWork
public Response fetch(@SessionAuth(required={Priv.CATEGORIZE,Priv.REPORT}) SessionFilteredAuthorization auths,@SessionUser User user, @PathParam("id") String id, @QueryParam("sz") int displayWidth) {
log.info("attempting to fetch image id = " + id+" with width "+displayWidth);
try {
ImageRecord ir = this.ds.getRecordFromId(id);
ImageEvent e = ir.getEvent();
List<SpeciesCount> cd = reportDAO.findCategorizationData(e);
for(SpeciesCount sc: cd) {
if(sc.getSpecies().getName().equalsIgnoreCase("human")) {
// deliver the small version of this one.
// perhaps if you are admin you get to see the full size ?
displayWidth = 24;
}
}
InputStream is = store.getInputStreamFor(ir, id,displayWidth);
CacheControl control = new CacheControl();
control.setMaxAge(6 * 60 * 60); // 6 hours
return Response.ok(is).cacheControl(control).build();
} catch (IOException e) {
log.error("can't deliver because ", e);
throw new WebApplicationException();
}
}
@GET
@Timed
@Produces(MediaType.APPLICATION_JSON)
@UnitOfWork
@Path("nextEvent")
public NextEventRecord getNextEvent(@SessionAuth(required={Priv.CATEGORIZE}) SessionFilteredAuthorization auths,@SessionCurProj Project currentProject,@SessionUser User user, @QueryParam("lastEvent") String lastEventIdStr) {
Long lastEventId = null;
try {
lastEventId = Long.parseLong(lastEventIdStr);
} catch (NumberFormatException e) {
// stupid API ignore this exception
}
NextEventRecord ner = this.ds.makeNextEventRecord(user,currentProject,lastEventId);
if(ner.getImageEvent() != null) {
for(ImageRecord ir: ner.getImageEvent().getImageRecords()) {
ir.getDatetime();
}
ImageEvent upie = initializeAndUnproxy(ner.getImageEvent());
ner.setImageEvent(upie);
}
return ner;
}
public static <T> T initializeAndUnproxy(T entity) {
if (entity == null) {
throw new
NullPointerException("Entity passed for initialization is null");
}
Hibernate.initialize(entity);
if (entity instanceof HibernateProxy) {
entity = (T) ((HibernateProxy) entity).getHibernateLazyInitializer().getImplementation();
}
return entity;
}
@GET
@Timed
@Produces(MediaType.APPLICATION_JSON)
@UnitOfWork
@Path("species")
public List<Species> listSpecies(@SessionAuth(required={Priv.CATEGORIZE}) SessionFilteredAuthorization auths,@SessionUser User user) {
List<Species> l = this.speciesDAO.findAll();
return l;
}
@GET
@Timed
@Produces(MediaType.APPLICATION_JSON)
@UnitOfWork
@Path("topSpecies")
public List<Species> listTopSpecies(@SessionAuth(required={Priv.CATEGORIZE,Priv.REPORT}) SessionFilteredAuthorization auths,@SessionCurProj Project currentProject, @SessionUser User user, @QueryParam("includeNone") boolean includeNone,@QueryParam("includeUnknown") boolean includeUnknown,@QueryParam("count") Integer count) {
List<Species> outList = null;
if(count == null) {
count = 10;
}
if(count == -1)
count = count -2;
Integer cnt = null;
if(count >0 ) {
cnt = count;
}
List<Long> l = reportDAO.makeTopSpeciesIdList(cnt,currentProject.getId());
if(l.size() < 3) {
outList = getTopTenForProject();
} else {
outList = new ArrayList<Species>();
for(Long id : l) {
Species s = speciesDAO.findById(id);
outList.add(s);
}
}
// filter out "none"
List<Species> realOut = new ArrayList<Species>();
if(includeNone) {
Species s = speciesDAO.findByNameContains("none");
if(s != null) {
realOut.add(s);
}
}
if(includeUnknown) {
Species s = speciesDAO.findByNameContains("unknown");
if(s != null) {
realOut.add(s);
}
}
for(Species s: outList) {
if(s.getName().equals("none")) {
//
} else if(s.getName().equals("unknown")) {
//
} else {
realOut.add(s);
}
}
return realOut;
}
private List<Species> getTopTenForProject() {
return getHardWiredSpecies();
}
private List<Species> getHardWiredSpecies() {
List<Species> l = new ArrayList<Species>();
Species s = this.speciesDAO.findByNameContains("puma");
s.setC('p');
if(s != null) l.add(s);
s = this.speciesDAO.findByNameContains("mule");
s.setC('d');
if(s != null) l.add(s);
s = this.speciesDAO.findByNameContains("Striped Skunk");
if(s != null) {
s.setC('s');
l.add(s);
}
s = this.speciesDAO.findByNameContains("lynx");
s.setC('b');
if(s != null) l.add(s);
s = this.speciesDAO.findByNameContains("Wild Turkey");
if(s != null) {
s.setC('t');
l.add(s);
}
s = this.speciesDAO.findByNameContains("sapiens");
s.setC('h');
if(s != null) l.add(s);
s = this.speciesDAO.findByNameContains("familiaris");
s.setC('g');
if(s != null) l.add(s);
return l;
}
@POST
@Timed
@UnitOfWork
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Path("identification")
public Response identify(@SessionAuth(required={Priv.CATEGORIZE}) SessionFilteredAuthorization auths,@SessionCurProj Project currentProject,@SessionUser User user,IdentificationRequest idRequest) {
log.info("GOT an ID "+idRequest);
// null sh ould be the user
long id = this.ds.recordIdentification(idRequest, user,currentProject);
return Response.ok(id).build();
}
@POST
@Timed
@UnitOfWork
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Path("clearid")
public Response unid(@SessionAuth(required={Priv.CATEGORIZE}) SessionFilteredAuthorization auths,@SessionCurProj Project currentProject,@SessionUser User user,long idToClear) {
log.info("we should clear "+idToClear);
this.ds.removeId(idToClear,currentProject);
// null should be the user
//long id = this.ds.recordIdentification(idRequest, user);
return Response.ok().build();
}
@POST
@Timed
@UnitOfWork
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response add(@SessionAuth(required={Priv.UPLOAD}) SessionFilteredAuthorization auths,@SessionCurProj Project currentProject,@SessionUser User user, @Context HttpServletRequest request, FormDataMultiPart formData) {
ImageRecord newImage = null;
try {
for(BodyPart bp : formData.getBodyParts()) {
try {
MediaType mt = bp.getMediaType();
if(!"image/jpeg".equals(mt.toString())) {
continue;
}
InputStream is = bp.getEntityAs(InputStream.class);
long size = bp.getContentDisposition().getSize();
int sz = (int)size;
String filename = bp.getContentDisposition().getFileName();
BufferedInputStream bis = new BufferedInputStream(is);
if( bis.markSupported()) {
if(sz > 0) {
bis.mark(sz);
} else {
// hard 5MB limit on size of images from wildlife camera ?
bis.mark(1024*1024*5);
}
}
{
Metadata md = JpegMetadataReader.readMetadata(bis);
ExifSubIFDDirectory directory = md.getDirectory(ExifSubIFDDirectory.class);
GpsDirectory gpsDirectory = md.getDirectory(GpsDirectory.class);
//String cameraID = null; // perhaps part of the form upload ?
String cameraIDStr = request.getHeader("camera_id");
Long cameraId = null;
if(cameraIDStr != null) {
try {
cameraId = Long.parseLong(cameraIDStr);
} catch (NumberFormatException e) {
throw new ConflictException("must have camera_id in header");
}
}
String latStr = request.getHeader("pos_lat");
String lonStr = request.getHeader("pos_lon");
newImage = ImageRecord.makeImageFromExif(timeZoneGetter, directory,gpsDirectory,filename,cameraId,latStr,lonStr, user.getId());
Float distanceFromProjectCenterInMiles = RegionUtil.distanceInMilesBetweenDouble(currentProject.getCenterLat(), currentProject.getCenterLon(), newImage.getLat(), newImage.getLon());
if(distanceFromProjectCenterInMiles > currentProject.getProjectRadiusMi()) {
// this images is too far away from the project. reject reject reject
throw new ConflictException("The Lat/Lon "+newImage.getLat()+" "+newImage.getLon()+" of that images is outside of the project boundaries center:"+currentProject.getCenterLat()+" x " +currentProject.getCenterLon()+" dist:"+currentProject.getProjectRadiusMi());
}
}
ImageRecord exist = irDAO.findById(newImage.getId());
if(exist == null) {
bis.reset();
BufferedImage bi = ImageIO.read(bis);
store.saveImages(bi,newImage);
log.info("new image save done");
bis.close();
ds.addImage(newImage,currentProject);
bi.flush();
URI uri = UriBuilder.fromResource(ImageResource.class).build(newImage.getId());
log.info("the response uri will be " + uri);
return Response.created(uri).build();
} else {
bis.close();
throw new ConflictException("that image already exists");
}
} catch(Exception e) {
log.info("could not upload one because ",e);
throw e;
} finally {
log.info("and we uploaded one, for better or worse");
}
}
} catch ( ImageProcessingException | IOException e) {
throw new WebApplicationException(e);
} finally {
formData.cleanup();
}
URI uri = UriBuilder.fromResource(ImageResource.class).build();
log.info("the response uri will be " + uri);
return Response.created(uri).build();
}
}