package gov.nysenate.openleg.controller.api.bill;
import gov.nysenate.openleg.client.response.base.BaseResponse;
import gov.nysenate.openleg.client.response.base.ListViewResponse;
import gov.nysenate.openleg.client.response.base.ViewObjectResponse;
import gov.nysenate.openleg.client.response.error.ErrorCode;
import gov.nysenate.openleg.client.response.error.ViewObjectErrorResponse;
import gov.nysenate.openleg.client.view.base.ModelView;
import gov.nysenate.openleg.client.view.base.StringView;
import gov.nysenate.openleg.client.view.base.ViewObject;
import gov.nysenate.openleg.client.view.bill.*;
import gov.nysenate.openleg.controller.api.base.BaseCtrl;
import gov.nysenate.openleg.dao.base.LimitOffset;
import gov.nysenate.openleg.model.base.SessionYear;
import gov.nysenate.openleg.model.base.Version;
import gov.nysenate.openleg.model.bill.BaseBillId;
import gov.nysenate.openleg.model.bill.Bill;
import gov.nysenate.openleg.model.bill.BillAmendment;
import gov.nysenate.openleg.model.bill.BillId;
import gov.nysenate.openleg.model.search.SearchException;
import gov.nysenate.openleg.model.search.SearchResults;
import gov.nysenate.openleg.service.bill.data.BillAmendNotFoundEx;
import gov.nysenate.openleg.service.bill.data.BillDataService;
import gov.nysenate.openleg.service.bill.data.BillNotFoundEx;
import gov.nysenate.openleg.service.bill.search.BillSearchService;
import gov.nysenate.openleg.util.BillTextUtils;
import gov.nysenate.openleg.util.OutputUtils;
import gov.nysenate.openleg.util.StringDiffer;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.WebRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.util.LinkedList;
import java.util.stream.Collectors;
import static gov.nysenate.openleg.controller.api.base.BaseCtrl.BASE_API_PATH;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
/**
* Bill retrieval APIs
*/
@RestController
@RequestMapping(value = BASE_API_PATH + "/bills", method = RequestMethod.GET, produces = APPLICATION_JSON_VALUE)
public class BillGetCtrl extends BaseCtrl
{
private static final Logger logger = LoggerFactory.getLogger(BillGetCtrl.class);
@Autowired protected BillDataService billData;
@Autowired protected BillSearchService billSearch;
protected enum BillViewLevel
{
DEFAULT, // Basic bill view (models the BillView class)
INFO, // Bill info view
NO_FULLTEXT, // Basic bill view with full text stripped
ONLY_FULLTEXT, // Only the full text for a specific amendment
WITH_REFS, // Bill view with summary views for all related bills
WITH_REFS_NO_FULLTEXT; // Bill view with no full text and with summary views for all related bills
public static BillViewLevel getValue(String type) {
if (StringUtils.isNotBlank(type)) {
try {
return BillViewLevel.valueOf(type.toUpperCase());
}
catch (IllegalArgumentException ex) {
return BillViewLevel.DEFAULT;
}
}
else {
return DEFAULT;
}
}
}
/**
* Bill listing API
* ----------------
*
* Retrieve bills for session year: (GET) /api/3/bills/{session}
* Request Parameters: sort - Lucene syntax for sorting by any field from the bill response.
* full - If true, the full bill view should be returned. Otherwise just the info.
* limit - Limit the number of results.
* offset - Start results from an offset.
*
* Expected Output: List of BillInfoView or BillView
*/
@RequestMapping(value = "/{sessionYear:[\\d]{4}}")
public BaseResponse getBills(@PathVariable int sessionYear,
@RequestParam(defaultValue = "status.actionDate:desc") String sort,
@RequestParam(defaultValue = "false") boolean full,
WebRequest webRequest) throws SearchException {
LimitOffset limOff = getLimitOffset(webRequest, 50);
SearchResults<BaseBillId> results =
billSearch.searchBills(SessionYear.of(sessionYear), sort, limOff);
// The bill data is retrieved from the data service so the data is always fresh.
return ListViewResponse.of(
results.getResults().stream()
.map(r -> (full) ? new BillView(billData.getBill(r.getResult()))
: new BillInfoView(billData.getBillInfo(r.getResult())))
.collect(Collectors.toList()), results.getTotalResults(), limOff);
}
/**
* Single Bill retrieval API
* -------------------------
*
* Retrieve a single bill via printNo and session: (GET) /api/3/bills/{session}/{printNo}/
* The version on the printNo is not needed since bills are returned with all amendments.
*
* Request Parameters: view - Specify the level of detail (defaults to BillViewLevel.DEFAULT)
*
* Expected Output: BillView, DetailedBillView, or BillInfoView
*/
@RequestMapping(value = "/{sessionYear:[\\d]{4}}/{printNo}")
public BaseResponse getBill(@PathVariable int sessionYear, @PathVariable String printNo, WebRequest request) {
BaseBillId baseBillId = getBaseBillId(printNo, sessionYear, "printNo");
BillViewLevel level = BillViewLevel.getValue(request.getParameter("view"));
ViewObject viewObject;
switch (level) {
case INFO: viewObject = new BillInfoView(billData.getBillInfo(baseBillId)); break;
case WITH_REFS: viewObject = new DetailBillView(billData.getBill(baseBillId), billData); break;
case NO_FULLTEXT: viewObject = new BillView(getFullTextStrippedBill(baseBillId)); break;
case WITH_REFS_NO_FULLTEXT: viewObject = new DetailBillView(getFullTextStrippedBill(baseBillId), billData); break;
case ONLY_FULLTEXT: {
Version amdVersion = Version.DEFAULT;
if (request.getParameter("version") != null) {
amdVersion = parseVersion(request.getParameter("version"), "version");
}
Bill bill = billData.getBill(baseBillId);
viewObject = new BillFullTextView(bill.getBaseBillId(), amdVersion.getValue(), bill.getAmendment(amdVersion).getFullText());
break;
}
default: viewObject = new BillView(billData.getBill(baseBillId));
}
return new ViewObjectResponse<>(viewObject, "Data for bill " + baseBillId);
}
/**
* Returns a Bill with the full text removed.
* @param baseBillId BaseBillId
* @return Bill
*/
private Bill getFullTextStrippedBill(BaseBillId baseBillId) {
Bill strippedBill = billData.getBill(baseBillId);
strippedBill.getAmendmentList().forEach(a -> a.setFullText(""));
return strippedBill;
}
/**
* Single Bill PDF retrieval API
* -----------------------------
*
* Retrieve a single bill amendment full text: (GET) /api/3/bills/{session}/{printNo}.pdf
* The version on the printNo will dictate which full text to output.
*
* Request Parameters: None
*
* Expected Output: PDF response
*/
@RequestMapping(value = "/{sessionYear:[\\d]{4}}/{printNo}.pdf")
public ResponseEntity<byte[]> getBillPdf(@PathVariable int sessionYear, @PathVariable String printNo)
throws Exception {
BillId billId = getBillId(printNo, sessionYear, "printNo");
Bill bill = billData.getBill(BaseBillId.of(billId));
ByteArrayOutputStream pdfBytes = new ByteArrayOutputStream();
BillPdfView.writeBillPdf(bill, billId.getVersion(), pdfBytes);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.parseMediaType("application/pdf"));
return new ResponseEntity<>(pdfBytes.toByteArray(), headers, HttpStatus.OK);
}
/**
* Bill Diff API
* -------------
*
* Returns an html diff between 'version1' and 'version2' of a given bill.
*
* TODO: Handle case with default amendment. Or rather make it so that it's possible to diff any two bills.
*/
@RequestMapping(value = "/{sessionYear:[\\d]{4}}/{printNo}/diff/{version1}/{version2}")
public BaseResponse getBillDiff(@PathVariable int sessionYear, @PathVariable String printNo, @PathVariable String version1,
@PathVariable String version2) {
StringDiffer stringDiffer = new StringDiffer();
BaseBillId baseBillId = getBaseBillId(printNo, sessionYear, "printNo");
Bill bill = billData.getBill(baseBillId);
BillAmendment amend1 = bill.getAmendment(parseVersion(version1, "version1"));
BillAmendment amend2 = bill.getAmendment(parseVersion(version2, "version2"));
String fullText1 = BillTextUtils.formatBillText(bill.isResolution(), amend1.getFullText());
String fullText2 = BillTextUtils.formatBillText(bill.isResolution(), amend2.getFullText());
LinkedList<StringDiffer.Diff> diffs = stringDiffer.diff_main(fullText1, fullText2);
stringDiffer.diff_cleanupEfficiency(diffs);
stringDiffer.diff_cleanupSemantic(diffs);
stringDiffer.diff_cleanupMerge(diffs);
String prettyHtml = stringDiffer.diff_prettyHtml(diffs).replace("¶", " ");
return new ViewObjectResponse<>(
new BillDiffView(
new BaseBillIdView(baseBillId), amend1.getVersion().toString(), amend2.getVersion().toString(),
prettyHtml));
}
/** --- Exception Handlers --- */
@ExceptionHandler(BillNotFoundEx.class)
@ResponseStatus(value = HttpStatus.NOT_FOUND)
public ViewObjectErrorResponse billNotFoundHandler(BillNotFoundEx ex) {
return new ViewObjectErrorResponse(ErrorCode.BILL_NOT_FOUND, new BillIdView(ex.getBillId()));
}
@ExceptionHandler(BillAmendNotFoundEx.class)
@ResponseStatus(value = HttpStatus.NOT_FOUND)
public ViewObjectErrorResponse billAmendNotFoundHandler(BillAmendNotFoundEx ex) {
return new ViewObjectErrorResponse(ErrorCode.BILL_AMENDMENT_NOT_FOUND, new BillIdView(ex.getBillId()));
}
}