Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Switch to new SOBI processing method that uses blocks.

  • Loading branch information...
commit 81339cde622b6d487b1c50b6a72bd39b49c53a5b 1 parent c95c612
@GraylinKim GraylinKim authored
View
234 src/main/java/gov/nysenate/openleg/model/SOBIBlock.java
@@ -0,0 +1,234 @@
+package gov.nysenate.openleg.model;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * The SOBIBlock class represents a set of SOBI lines sharing the same header:
+ * <p>
+ * <pre>
+ * 2013A03006D3Amends various provisions of law relating to implementing the health and mental hygiene budget for
+ * 2013A03006D3the 2013-2014 state fiscal year.
+ * </pre>
+ * All of the different header components are parsed out and data is aggregated:
+ * <p>
+ * <ul>
+ * <li>year: 2013</li>
+ * <li>printNo: "A03006"</li>
+ * <li>amendment: "D"</li>
+ * <li>type: '3'</li>
+ * <li>data: Amends various provisions of law relating to implementing the health and mental hygiene budget for
+ * the 2013-2014 state fiscal year.</li>
+ * </ul>
+ * <p>
+ * @author GraylinKim
+ *
+ */
+public class SOBIBlock
+{
+ /**
+ * A list of sobi line types that are *single* line blocks. All other block types are multi-line.
+ */
+ public static final List<Character> oneLineBlocks = Arrays.asList('1','2','5');
+
+ /**
+ * A pattern to verify that a string is in the SOBI line format.
+ */
+ public static final Pattern blockPattern = Pattern.compile("^[0-9]{4}[A-Z][0-9]{5}[ A-Z][1-9ABCMRTV]");
+
+ /**
+ * The line number that the block starts at. Defaults to zero when using the basic constructor.
+ */
+ private Integer lineNumber = 0;
+
+ /**
+ * The file from which the block was found. Defaults to null when using the basic constructor.
+ */
+ private File file = null;
+
+ /**
+ * The full line header from the line used to construct the Block.
+ */
+ private String header = "";
+
+ /**
+ * The year indicated in the block header.
+ */
+ private Integer year = 0;
+
+ /**
+ * The printNo of the base bill. The amendment character is NOT included.
+ */
+ private String printNo = "";
+
+ /**
+ * The amendment character for the base bill. Can be empty or single letter string from "A-Z"
+ */
+ private String amendment = "";
+
+ /**
+ * The sobi line type of the block. Determines how the data is interpretted.
+ */
+ private Character type = 0;
+
+ /**
+ * An internal buffer used to accumulate block data over several lines.
+ */
+ private StringBuffer dataBuffer = new StringBuffer();
+
+ /**
+ * Construct a new block with without location information from a valid SOBI line. The line is
+ * assumed to be valid SOBI file and is NOT checked for performance reasons.
+ */
+ public SOBIBlock(String line) {
+ this.setYear(Integer.parseInt(line.substring(0,4)));
+ this.setPrintNo(line.substring(4,10));
+ this.setAmendment(line.substring(10,11).trim());
+ this.setType(line.charAt(11));
+ this.setHeader(line.substring(0,12));
+ this.setData(line.substring(12));
+ }
+
+ /**
+ * Construct a new block with location information from a valid SOBI line. Holds references to
+ * the source file and line number the block was initialized from. The line is assumed to be
+ * valid SOBI file and is NOT checked for performance reasons.
+ */
+ public SOBIBlock(File file, int lineNumber, String line)
+ {
+ this(line);
+ this.setFile(file);
+ this.setLineNumber(lineNumber);
+ }
+
+
+ /**
+ * Returns a representation of the location of the block: fileName:lineNumber.
+ */
+ public String getLocation()
+ {
+ return (this.getFile() == null ? "null" : this.getFile().getName())+":"+this.getLineNumber();
+ }
+
+ /**
+ * Extends the block data with the data from the new line.
+ */
+ public void extend(String line) {
+ this.dataBuffer.append("\n"+line.substring(12));
+ }
+
+ /**
+ * Gets the string representation of the block's data.
+ */
+ public String getData()
+ {
+ return dataBuffer.toString();
+ }
+
+ /**
+ * Replaces the block data with the input string.
+ */
+ public void setData(String data)
+ {
+ this.dataBuffer = new StringBuffer(data);
+ }
+
+ /**
+ * Returns true if the block should be extended by multiple lines. Blocks containing only DELETE should be treated as single line blocks.
+ */
+ public boolean isMultiline() {
+ return !oneLineBlocks.contains(this.getType()) && !this.getData().trim().equals("DELETE");
+ }
+
+ public boolean equals(Object obj)
+ {
+ return obj!= null && obj instanceof SOBIBlock
+ && ((SOBIBlock)obj).getHeader().equals(this.getHeader())
+ && ((SOBIBlock)obj).getData().trim().equals(this.getData().trim());
+ }
+
+ public String toString()
+ {
+ return this.getLocation()+":"+this.getHeader();
+ }
+
+
+ public int getLineNumber()
+ {
+ return lineNumber;
+ }
+
+ public void setLineNumber(int lineNumber)
+ {
+ this.lineNumber = lineNumber;
+ }
+
+
+ public File getFile()
+ {
+ return file;
+ }
+
+ public void setFile(File file)
+ {
+ this.file = file;
+ }
+
+
+ public String getHeader()
+ {
+ return header;
+ }
+
+ public void setHeader(String header)
+ {
+ this.header = header;
+ }
+
+
+ public int getYear()
+ {
+ return year;
+ }
+
+ public void setYear(int year)
+ {
+ this.year = year;
+ }
+
+
+ public String getPrintNo()
+ {
+ return printNo;
+ }
+
+ public void setPrintNo(String printNo)
+ {
+ // Integer conversion removes leading zeros in the print number.
+ this.printNo = printNo.substring(0,1)+Integer.parseInt(printNo.substring(1));
+ }
+
+
+ public String getAmendment()
+ {
+ return amendment;
+ }
+
+ public void setAmendment(String amendment)
+ {
+ this.amendment = amendment;
+ }
+
+
+ public char getType()
+ {
+ return type;
+ }
+
+ public void setType(char type)
+ {
+ this.type = type;
+ }
+}
View
300 src/main/java/gov/nysenate/openleg/processors/AbstractBillProcessor.java
@@ -0,0 +1,300 @@
+package gov.nysenate.openleg.processors;
+
+import gov.nysenate.openleg.model.Bill;
+import gov.nysenate.openleg.model.SOBIBlock;
+import gov.nysenate.openleg.util.Storage;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.regex.Matcher;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.log4j.Logger;
+
+abstract public class AbstractBillProcessor
+{
+ private final Logger logger = Logger.getLogger(AbstractBillProcessor.class);
+
+ public static class ParseError extends Exception
+ {
+ private static final long serialVersionUID = -2394555333799843588L;
+ public ParseError(String message) { super(message); }
+ }
+
+
+ /**
+ * Applies changes in the SOBI file to objects in storage.
+ *
+ * @throws IOException if File cannot be opened for reading.
+ */
+ public void process(File sobiFile, Storage storage) throws IOException
+ {
+ Date date = null;
+ try {
+ date = BillProcessor.sobiDateFormat.parse(sobiFile.getName());
+ }
+ catch (ParseException e) {
+ logger.error("Unparseable date: "+sobiFile.getName());
+ return;
+ }
+
+ // Catch exceptions on a per-block basis so that a single error won't corrupt the whole file.
+ for (SOBIBlock block : getBlocks(sobiFile)) {
+ logger.info("Processing "+block);
+ try {
+
+ if (block.getType() == '1' && block.getData().startsWith("DELETE")) {
+ // Special case here were we delete the whole bill
+ logger.info("DELETING "+block.getHeader());
+ deleteBill(block, storage);
+ }
+ else {
+ String data = block.getData().toString();
+ // Otherwise, apply the block to the bill normally
+ Bill bill = getOrCreateBill(block, storage);
+ switch (block.getType()) {
+ case '1': applyBillInfo(data, bill, date); break;
+ case '2': applyLawSection(data, bill, date); break;
+ case '3': applyTitle(data, bill, date); break;
+ case '4': applyBillEvent(data, bill, date); break;
+ case '5': applySameAs(data, bill, date); break;
+ case '6': applySponsor(data, bill, date); break;
+ case '7': applyCosponsors(data, bill, date); break;
+ case '8': applyMultisponsors(data, bill, date); break;
+ case '9': applyProgramInfo(data, bill, date); break;
+ case 'A': applyActClause(data, bill, date); break;
+ case 'B': applyLaw(data, bill, date); break;
+ case 'C': applySummary(data, bill, date); break;
+ case 'M':
+ case 'R':
+ case 'T': applyText(data, bill, date); break;
+ case 'V': applyVoteMemo(data, bill, date); break;
+ default: throw new ParseError("Invalid Line Code "+block.getType() );
+ }
+ logger.info("SAVING: "+bill.getSenateBillNo());
+ bill.addSobiReference(sobiFile.getName());
+ bill.setModified(date.getTime());
+ saveBill(bill, storage);
+ }
+ }
+ catch (ParseError e) {
+ logger.error("ParseError at "+block.getLocation(), e);
+ }
+ catch (Exception e) {
+ logger.error("Unexpected Exception at "+block.getLocation(), e);
+ }
+ }
+ }
+
+ /**
+ * Parses the given SOBI file into a list of blocks. See the Block class for more details.
+ *
+ * @param sobiFile
+ * @return
+ * @throws IOException if file cannot be opened for reading.
+ */
+ public List<SOBIBlock> getBlocks(File sobiFile) throws IOException
+ {
+ SOBIBlock block = null;
+ List<SOBIBlock> blocks = new ArrayList<SOBIBlock>();
+ List<String> lines = FileUtils.readLines(sobiFile);
+ lines.add(""); // Add a trailing line to end the last block and remove edge cases
+
+ for(int lineNum = 0; lineNum < lines.size(); lineNum++) {
+ // Replace NULL bytes with spaces to properly format lines.
+ String line = lines.get(lineNum).replace('\0', ' ');
+
+ // Source file is not assumed to be 100% SOBI so we filter out other lines
+ Matcher headerMatcher = SOBIBlock.blockPattern.matcher(line);
+
+ if (headerMatcher.find()) {
+ if (block == null) {
+ // No active block with a new matching line: create new block
+ block = new SOBIBlock(sobiFile, lineNum, line);
+ }
+ else if (block.getHeader().equals(headerMatcher.group()) && block.isMultiline()) {
+ // active multi-line block with a new matching line: extend block
+ block.extend(line);
+ }
+ else {
+ // active block does not match new line or can't be extended: create new block
+ blocks.add(block);
+ block = new SOBIBlock(sobiFile, lineNum, line);
+ }
+ }
+ else if (block != null) {
+ // Active block with non-matching line: end the current blockAny non-matching line ends the current block
+ blocks.add(block);
+ block = null;
+ }
+ else {
+ // No active block with a non-matching line, do nothing
+ }
+ }
+
+ return blocks;
+ }
+
+ /**
+ * Safely gets the bill specified by the block from storage. If the bill does not
+ * exist then it is created. When loading an amendment, if the parent bill does not
+ * exist then it is created as well.
+ *
+ * New bills will pull sponsor and amendment information up from prior versions if
+ * available.
+ */
+ public Bill getOrCreateBill(SOBIBlock block, Storage storage) throws ParseError
+ {
+ String billKey = block.getPrintNo()+block.getAmendment()+"-"+block.getYear();
+ Bill bill = storage.getBill(billKey);
+
+ if (bill != null) {
+ // We retrieved the bill successfully!
+ return bill;
+ }
+ else {
+ // We need to create a new bill with this key
+ if (block.getAmendment().isEmpty()) {
+ // We need to create an original bill, this is easy!
+ return new Bill(billKey, block.getYear());
+ }
+ else {
+ // We need to create an amendment, this is trickier
+ Bill amendment = new Bill(billKey, block.getYear());
+
+ // All new amendments are based on the original bill
+ String baseKey = block.getPrintNo()+"-"+block.getYear();
+ Bill baseBill = storage.getBill(baseKey);
+
+ if (baseBill == null) {
+ // Amendments should always have original bills already made, make it happen
+ logger.error("Bill Amendment filed without initial bill at "+block.getLocation()+" - "+block.getHeader());
+ baseBill = new Bill(baseKey, block.getYear());
+ storage.saveBill(baseBill);
+ }
+
+ // Pull sponsor information up from the base bill
+ amendment.setSponsor(baseBill.getSponsor());
+ amendment.setCoSponsors(baseBill.getCoSponsors());
+ amendment.setMultiSponsors(baseBill.getMultiSponsors());
+
+ // Pull up the list of existing versions and add yourself
+ amendment.addAmendment(baseKey);
+ amendment.addAmendments(baseBill.getAmendments());
+
+ // Broadcast yourself to all other versions and deactivate them
+ Bill activeBill = null;
+ for (String versionKey : amendment.getAmendments()) {
+ Bill billVersion = storage.getBill(versionKey);
+ if (billVersion == null) {
+ throw new ParseError("Recorded bill version not found in storage: "+versionKey);
+ }
+ else {
+ billVersion.addAmendment(billKey);
+ if(billVersion.isActive()) {
+ activeBill = billVersion;
+ billVersion.setActive(false);
+ }
+ storage.saveBill(billVersion);
+ }
+ }
+
+ if (activeBill == null) {
+ logger.error("Unable to find active bill for "+amendment.getSenateBillNo()+". BIG PROBLEM!");
+ activeBill = baseBill;
+ }
+
+ // Pull some other information up from previously active bill
+ amendment.setSummary(activeBill.getSummary());
+ amendment.setLaw(activeBill.getLaw());
+
+ // Activate yourself
+ amendment.setActive(true);
+ storage.saveBill(amendment);
+ return amendment;
+ }
+ }
+ }
+
+
+ /**
+ * Safely saves the bill to storage. This makes sure to sync sponsor data across all amendments
+ * so that different versions of the bill cannot get out of sync.
+ *
+ * @param bill
+ * @param storage
+ */
+ public void saveBill(Bill bill, Storage storage)
+ {
+ // Sponsor information needs to be synced at all times.
+ // Normally it is always sent to the base bill and broadcasted to amendments
+ // In our 2009 data set we are missing tons of base amendments and it actually
+ // needs to be broadcasted backwards to the original bill.
+ for (String versionKey : bill.getAmendments()) {
+ Bill billVersion = storage.getBill(versionKey);
+ billVersion.setSponsor(bill.getSponsor());
+ billVersion.setCoSponsors(bill.getCoSponsors());
+ billVersion.setMultiSponsors(bill.getMultiSponsors());
+ storage.saveBill(billVersion);
+ }
+ storage.saveBill(bill);
+ }
+
+ /**
+ * Safely deletes the bill specified from by the block. This removes all references to the bill
+ * from its amendments and, if the bill was currently active, re-sets the active bill to the
+ * newest amendment.
+ *
+ * @param block
+ * @param storage
+ * @throws ParseError
+ */
+ public void deleteBill(SOBIBlock block, Storage storage) throws ParseError
+ {
+ String billKey = block.getPrintNo()+block.getAmendment()+"-"+block.getYear();
+ Bill bill = storage.getBill(billKey);
+
+ if (bill == null) {
+ // If we can't find a bill they are asking us to delete, don't worry about it.
+ throw new ParseError("New bill with DELETE command only. Skipping "+block.getHeader()+block.getData().toString());
+ }
+ else {
+ List<String> amendments = bill.getAmendments();
+ if (amendments.size() > 0) {
+ // Set the previous amendment to be the active one
+ String newActiveBill = amendments.get(amendments.size()-1);
+
+ // Remove all references to the current bill/amendment.
+ for (String versionKey : bill.getAmendments()) {
+ Bill billVersion = storage.getBill(versionKey);
+ billVersion.removeAmendment(billKey);
+ if (bill.isActive() && versionKey.equals(newActiveBill)) {
+ billVersion.setActive(true);
+ }
+ storage.saveBill(billVersion);
+ }
+ }
+ storage.del(bill.getYear()+"/bill/"+billKey);
+ }
+ }
+
+
+ abstract public void applySummary(String data, Bill bill, Date date) throws ParseError;
+ abstract public void applyTitle(String data, Bill bill, Date date) throws ParseError;
+ abstract public void applySameAs(String data, Bill bill, Date date) throws ParseError;
+ abstract public void applySponsor(String data, Bill bill, Date date) throws ParseError;
+ abstract public void applyCosponsors(String data, Bill bill, Date date) throws ParseError;
+ abstract public void applyMultisponsors(String data, Bill bill, Date date) throws ParseError;
+ abstract public void applyLaw(String data, Bill bill, Date date) throws ParseError;
+ abstract public void applyLawSection(String data, Bill bill, Date date) throws ParseError;
+ abstract public void applyActClause(String data, Bill bill, Date date) throws ParseError;
+ abstract public void applyProgramInfo(String data, Bill bill, Date date) throws ParseError;
+ abstract public void applyBillInfo(String data, Bill bill, Date date) throws ParseError;
+ abstract public void applyVoteMemo(String data, Bill bill, Date date) throws ParseError;
+ abstract public void applyText(String data, Bill bill, Date date) throws ParseError;
+ abstract public void applyBillEvent(String data, Bill bill, Date date) throws ParseError;
+}
View
824 src/main/java/gov/nysenate/openleg/processors/BillProcessor.java
@@ -4,10 +4,7 @@
import gov.nysenate.openleg.model.Bill;
import gov.nysenate.openleg.model.Person;
import gov.nysenate.openleg.model.Vote;
-import gov.nysenate.openleg.util.Storage;
-import java.io.File;
-import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
@@ -18,15 +15,11 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
-import org.apache.commons.io.FileUtils;
import org.apache.log4j.Logger;
-public class BillProcessor {
-
- private final Logger logger;
- public File sobiDirectory;
-
- public static GregorianCalendar textFooterResolveDate = new GregorianCalendar(2011, 3, 23);
+public class BillProcessor extends AbstractBillProcessor
+{
+ private final Logger logger = Logger.getLogger(BillProcessor.class);
public static SimpleDateFormat eventDateFormat = new SimpleDateFormat("MM/dd/yy");
public static SimpleDateFormat voteDateFormat = new SimpleDateFormat("MM/dd/yyyy");
@@ -45,268 +38,385 @@
public static Pattern floorEventTextPattern = Pattern.compile("(REPORT CAL|THIRD READING|RULES REPORT)");
public static Pattern substituteEventTextPattern = Pattern.compile("SUBSTITUTED (FOR|BY) (.*)");
- public static class ParseError extends Exception {
- private static final long serialVersionUID = 276526760129242006L;
- public String data;
+ /**
+ * Applies the data to the bill summary. Strips out all whitespace formatting.
+ *
+ * @param data
+ * @param bill
+ * @param date
+ * @throws ParseError
+ */
+ public void applySummary(String data, Bill bill, Date date) throws ParseError
+ {
+ // The DELETE code for the summary goes through the law block (B)
+ // Combine the lines with a space and handle special character issues..
+ // I don't have any examples of these special characters right now, here is some legacy code:
+ // data = data.replace("›","S").replaceAll("\\x27(\\W|\\s)", "&apos;$1");
+ bill.setSummary(data.replace("\n", " ").trim());
+ }
+
+ /**
+ * Applies the data to the bill title. Strips out all whitespace formatting.
+ *
+ * @param data
+ * @param bill
+ * @param date
+ * @throws ParseError
+ */
+ public void applyTitle(String data, Bill bill, Date date) throws ParseError
+ {
+ // No DELETE code for titles, they just get replaced
+ // Combine the lines with a space and handle special character issues..
+ // I don't have any examples of these special characters right now, here is some legacy code:
+ // data = data.replace("›","S").replaceAll("\\x27(\\W|\\s)", "&apos;$1");
+ bill.setTitle(data.replace("\n", " ").trim());
+ }
- public ParseError(String message, String data) {
- super(message);
- this.data = data;
+ /**
+ * Applies data to the bill Same-as.
+ *
+ * @param data
+ * @param bill
+ * @param date
+ * @throws ParseError
+ */
+ public void applySameAs(String data, Bill bill, Date date) throws ParseError
+ {
+ // Guarenteed to be only a single line block.
+ if (data.trim().equals("No same as") || data.trim().equals("DELETE")) {
+ bill.setSameAs("");
}
-
- @Override
- public String getMessage() {
- return super.getMessage()+": "+data;
+ else {
+ // Why do we do this, don't have an example of this issue, here is some legacy code:
+ // line = line.replace("/", ",").replaceAll("[ \\-.;]", "");
+ Matcher sameAsMatcher = sameAsPattern.matcher(data);
+ if (sameAsMatcher.find()) {
+ bill.setSameAs(sameAsMatcher.group(2).replace("-","").replace(" ",""));
+ } else {
+ logger.error("sameAsPattern not matched: "+data);
+ }
}
}
- public BillProcessor() {
- this.logger = Logger.getLogger(this.getClass());
- }
+ /**
+ * Applies data to bill sponsor. Fully replaces existing sponsor information.
+ *
+ * A delete in these field removes all sponsor information.
+ *
+ * @param data
+ * @param bill
+ * @param date
+ * @throws ParseError
+ */
+ public void applySponsor(String data, Bill bill, Date date) throws ParseError
+ {
+ // Apply the lines in order given as each represents its own "block"
+ for(String line : data.split("\n")) {
+ if (line.trim().equals("DELETE")) {
+ bill.setSponsor(null);
+ bill.setCoSponsors(new ArrayList<Person>());
+ bill.setMultiSponsors(new ArrayList<Person>());
- public Bill loadBill(Storage storage, String billId, String billYear, String billAmendment, String oldBlock, StringBuffer blockData, String fileName, int lineNum) throws ParseError {
- // This really shouldn't be this complicated, but it is
- // TODO: Because we don't get 2009 data in order there are probably
- // some data issues created by the logic below.
-
- // Get the original bill
- String oldkey;
- String amdkey;
- String bucket = billYear+"/bill/";
- String key = billId+"-"+billYear;
- Bill bill = (Bill)storage.get(bucket+key, Bill.class);
-
- // If the bill wasn't found, make a new one
- if (bill == null) {
- bill = new Bill();
- bill.setYear(Integer.parseInt(billYear));
- bill.setSenateBillNo(key);
- if (!billAmendment.isEmpty()) {
- logger.error("Bill Amendment filed without initial bill in file "+fileName+":"+lineNum+" - "+oldBlock);
+ } else {
+ bill.setSponsor(new Person(line.trim()));
}
}
+ }
- if (!billAmendment.isEmpty()) {
- amdkey = billId+billAmendment+"-"+billYear;
- Bill amd = (Bill)storage.get(bucket+amdkey, Bill.class);
-
- if (amd == null) {
- if(blockData.toString().startsWith("DELETE")) {
- //If it is just a delete block just skip making the amendment
- //this happens quite frequently for some reason.
- throw new ParseError("New amendment with DELETE command only. Skipping", oldBlock+blockData);
- }
+ /**
+ * Applies data to bill co-sponsors. Fully replaces existing information.
+ *
+ * Delete code is sent through the sponsor block.
+ *
+ * @param data
+ * @param bill
+ * @param date
+ * @throws ParseError
+ */
+ public void applyCosponsors(String data, Bill bill, Date date) throws ParseError
+ {
+ ArrayList<Person> coSponsors = new ArrayList<Person>();
+ if (!data.trim().isEmpty()) {
+ for(String coSponsor : data.replace("\n", " ").split(",")) {
+ coSponsors.add(new Person(coSponsor.trim()));
+ }
+ }
+ bill.setCoSponsors(coSponsors);
+ }
- // get the previous version and copy it by reading from file
- if(bill.amendments.isEmpty()) {
- // In case this is an amendment introduced without an original (mostly 2009 data)
- // we need to make sure to set the base bill we just made for flushing below.
- oldkey = bill.getSenateBillNo();
- storage.set(bucket+oldkey, bill);
+ /**
+ * Applies data to bill multi-sponsors. Fully replaces existing information.
+ *
+ * Delete code is sent through the sponsor block.
+ *
+ * @param data
+ * @param bill
+ * @param date
+ * @throws ParseError
+ */
+ public void applyMultisponsors(String data, Bill bill, Date date) throws ParseError
+ {
+ ArrayList<Person> multiSponsors = new ArrayList<Person>();
+ if (!data.trim().isEmpty()) {
+ for(String multiSponsor : data.replace("\n", " ").split(",")) {
+ multiSponsors.add(new Person(multiSponsor.trim()));
+ }
+ }
+ bill.setMultiSponsors(multiSponsors);
+ }
- } else {
- oldkey = bill.amendments.get(bill.amendments.size()-1);
- }
- // Flush to make sure we load the most recent copy of the bill
- storage.flush();
+ /**
+ * Applies data to bill law. Fully replaces existing information.
+ *
+ * DELETE code here also deletes the bill summary.
+ *
+ * @param data
+ * @param bill
+ * @param date
+ * @throws ParseError
+ */
+ public void applyLaw(String data, Bill bill, Date date) throws ParseError
+ {
+ // This is theoretically not safe because a law line *could* start with DELETE but
+ // in practice this doesn't ever happen.
+ // We can't do an exact match because B can be multiline
+ if (data.trim().startsWith("DELETE")) {
+ bill.setLaw("");
+ bill.setSummary("");
- // now that we're synced with the file system, load from file for a new copy
- amd = (Bill)storage.get(bucket+oldkey , Bill.class, false);
- if (amd==null) {
- throw new ParseError("Recorded amendment not found in storage", bucket+oldkey);
- }
+ } else {
+ // We'll definitely need to clean this data up more than a little bit, these encoding issues are terrible!
+ // data = data.replaceAll("\\xBD", ""); // I dont' think we still need this
+ bill.setLaw(data.replace("\n", " ").replace("õ", "S").replace("ô","P").replace("�","S").replace((char)65533+"", "S").trim());
+ }
+ }
- // Set the amendment to the correct bill number
- amd.setSenateBillNo(amdkey);
+ /**
+ * Applies data to law section. Fully replaces existing data.
+ *
+ * Delete code is sent through the law block.
+ *
+ * @param data
+ * @param bill
+ * @param date
+ * @throws ParseError
+ */
+ public void applyLawSection(String data, Bill bill, Date date) throws ParseError
+ {
+ bill.setLawSection(data.trim());
+ }
- // Update the all amendment lists and mark existing ones as inactive
- for(String tempKey : bill.amendments) {
- storage.flush();
- Bill temp = (Bill)storage.get(bucket+tempKey, Bill.class);
- temp.setActive(false);
- temp.amendments.add(amdkey);
- storage.set(bucket+tempKey, temp);
- }
+ /**
+ * Applies data to the ACT TO clause. Fully replaces existing data.
+ *
+ * DELETE code removes existing ACT TO clause.
+ *
+ * @param data
+ * @param bill
+ * @param date
+ * @throws ParseError
+ */
+ public void applyActClause(String data, Bill bill, Date date) throws ParseError
+ {
+ if (data.trim().equals("DELETE")) {
+ bill.setActClause("");
+ } else {
+ bill.setActClause(data.replace("\n", " ").trim());
+ }
+ }
- // Update the base amendment list and mark it as inactive
- bill.amendments.add(amdkey);
- bill.setActive(false);
- storage.set(bucket+key, bill);
- }
+ /**
+ * Applies data to bill Program. Fully replaces existing information
+ *
+ * 029 Governor Program
+ *
+ * Currently implemented.
+ *
+ * @param data
+ * @param bill
+ * @param date
+ * @throws ParseError
+ */
+ public void applyProgramInfo(String data, Bill bill, Date date) throws ParseError
+ {
+ // This information currently isn't used for anything
+ //if (!data.equals(""))
+ // throw new ParseError("Program info not implemented", data);
+ }
- bill = amd;
- key = amdkey;
- } else if(!bill.amendments.isEmpty()) {
- amdkey = bill.amendments.get(bill.amendments.size()-1);
- Bill amd = (Bill)storage.get(bucket+amdkey, Bill.class);
- if (amd==null) {
- throw new ParseError("Recorded amendment not found on filesystem",amdkey);
+ /**
+ * Apply information from the Bill Info block. Fully replaces existing information.
+ *
+ * Currently only uses sponsor and previous version (which has known issues)
+ *
+ * A DELETE code sent with this block removes the whole bill.
+ *
+ * @param data
+ * @param bill
+ * @param date
+ * @throws ParseError
+ */
+ public void applyBillInfo(String data, Bill bill, Date date) throws ParseError
+ {
+ // The DELETE code here applies to the whole bill and is handled in ``process``
+ // Guarenteed to be only a 1 line block
+
+ // The null bytes screw with the regex, replace them.
+ Matcher billData = billDataPattern.matcher(data);
+ if (billData.find()) {
+ //TODO: Find a possible use for this information
+ String sponsor = billData.group(1).trim();
+ //String reprint = billData.group(2);
+ //String blurb = billData.group(3);
+ String oldbill = billData.group(4).trim().replaceAll("[0-9`-]$", "");
+ //String lbdnum = billData.group(5);
+
+ if (!sponsor.isEmpty() && bill.getSponsor() == null) {
+ bill.setSponsor(new Person(sponsor));
}
-
- bill = amd;
- key = amdkey;
+ bill.addPreviousVersion(oldbill);
+ } else {
+ throw new ParseError("billDataPattern not matched by "+data);
}
-
- return bill;
}
- public void saveBill(Storage storage, Bill bill) throws ParseError {
- // Sync certain information across versions.
- // This is a hack around the horrible 2009 data set we were given
- // TODO: We should only need to do this for certain line codes
- String bucket = bill.getYear()+"/bill/";
- if (bill.getSponsor()!=null) {
- // Update the base bill if we are not the base bill
- String baseBill = bill.getSenateBillNo().replaceAll("([A-Z][0-9]+).?-([0-9]{4})","$1-$2");
- List<String> amendments = bill.amendments;
- if (!baseBill.equals(bill.getSenateBillNo())) {
- Bill base = (Bill)storage.get(bucket+baseBill, Bill.class);
- base.setSponsor(bill.getSponsor());
- storage.set(bucket+baseBill, base);
- amendments = base.amendments;
+ public void applyVoteMemo(String data, Bill bill, Date date) throws ParseError
+ {
+ // TODO: Parse out sequence number once LBDC includes it, #6531
+ // Example of a double vote entry: SOBI.D110119.T140802.TXT:390
+ Vote vote = null;
+ for(String line : data.split("\n")) {
+ Matcher voteHeader = voteHeaderPattern.matcher(line);
+ if (voteHeader.find()) {
+ //Start over if we hit a header, sometimes we get back to back entries
+ try {
+ vote = new Vote(bill, voteDateFormat.parse(voteHeader.group(2)), Vote.VOTE_TYPE_FLOOR, "1");
+ }
+ catch (ParseException e) {
+ throw new ParseError("voteDateFormat not matched: "+line);
+ }
}
- // Update amendments (starting from the base
- for(String amendment : amendments ) {
- if (!amendment.equals(bill.getSenateBillNo())) {
- Bill amd = (Bill)storage.get(bucket+amendment, Bill.class);
- amd.setSponsor(bill.getSponsor());
- storage.set(bucket+amendment, amd);
+ else if (vote!=null){
+ //Otherwise, build the existing vote
+ Matcher voteLine = votePattern.matcher(line);
+ while(voteLine.find()) {
+ String type = voteLine.group(1).trim();
+ Person voter = new Person(voteLine.group(2).trim());
+
+ if (type.equals("Aye")) {
+ vote.addAye(voter);
+ }
+ else if (type.equals("Nay")) {
+ vote.addNay(voter);
+ }
+ else if (type.equals("Abs")) {
+ vote.addAbsent(voter);
+ }
+ else if (type.equals("Abd")) {
+ vote.addAbstain(voter);
+ }
+ else if (type.equals("Exc")) {
+ vote.addExcused(voter);
+ }
+ else {
+ throw new ParseError("Unknown vote type found: "+line);
+ }
}
+
+ } else {
+ throw new ParseError("Hit vote data without a header: "+data);
}
}
- storage.set(bucket+bill.getSenateBillNo(), bill);
- }
- public void process(File sobiFile, Storage storage) throws IOException {
-
- // Initialize file variables
- String oldBlock = "";
- String newBlock = "";
- StringBuffer blockData = new StringBuffer();
- String billYear = "";
- String billId = "";
- String billAmendment = "";
- String lineCode = "";
- String fileName = sobiFile.getName();
-
- Date date;
- try {
- date = BillProcessor.sobiDateFormat.parse(fileName);
- } catch (ParseException e) {
- logger.error("Unparseable date: "+fileName);
- return;
- }
+ //Misnomer, will actually update the vote if it already exists
+ bill.addVote(vote);
+ }
- // Loop through the lines in the file
- logger.info("Reading file: "+fileName);
- List<String> lines = FileUtils.readLines(sobiFile);
- lines.add(""); // Add a line to remove last line edge case
- for(int lineNum=0; lineNum<lines.size(); lineNum++) {
- String line = lines.get(lineNum);
- // Check to see if the current line is in the SOBI format
- Matcher sobiHeader = sobiHeaderPattern.matcher(line);
+ public void applyText(String data, Bill bill, Date date) throws ParseError
+ {
+ // BillText, ResolutionText, and MemoText can be handled the same way
+ // Deleted with a *DELETE* line.
+ String type = "";
+ StringBuffer text = null;
- // Supply a default newBlock identifier for non-matching lines
- newBlock = sobiHeader.find() ? sobiHeader.group(1) : "";
+ for (String line : data.split("\n")) {
+ Matcher header = textPattern.matcher(line);
+ if (line.startsWith("00000") && header.find()) {
+ //TODO: If house == C then bills can be used for SAME AS verification
+ // e.g. 2013S02278 T00000.SO DOC C 2279/2392
+ // and 2013S02277 5Same as Uni. A 2394
+ //String house = header.group(1);
+ //String bills = header.group(2).trim();
+ //String year = header.group(5);
+ String action = header.group(3).trim();
+ type = header.group(4).trim();
- // If we previously had a block and the new block is different
- // commit the old block before starting a new one.
- if (!oldBlock.equals("") && !newBlock.equals(oldBlock)) {
- try {
- Bill bill = loadBill(storage, billId, billYear, billAmendment, oldBlock, blockData, fileName, lineNum);
-
- // commit block
- try {
- switch (lineCode.charAt(0)) {
- case '1': applyBillData(blockData.toString(), bill, date); break;
- case '2': applyLawSection(blockData.toString(), bill, date); break;
- case '3': applyTitle(blockData.toString(), bill, date); break;
- case '4': applyBillEvent(blockData.toString(), bill, date); break;
- case '5': applySameAs(blockData.toString(), bill, date); break;
- case '6': applySponsor(blockData.toString(), bill, date); break;
- case '7': applyCosponsors(blockData.toString(), bill, date); break;
- case '8': applyMultisponsors(blockData.toString(), bill, date); break;
- case '9': applyProgramInfo(blockData.toString(), bill, date); break;
- case 'A': applyActClause(blockData.toString(), bill, date); break;
- case 'B': applyLaw(blockData.toString(), bill, date); break;
- case 'C': applySummary(blockData.toString(), bill, date); break;
- case 'M':
- case 'R':
- case 'T': applyText(blockData.toString(), bill, date); break;
- case 'V': applyVoteMemo(blockData.toString(), bill, date); break;
- default: throw new ParseError("Invalid Line Code", lineCode);
- }
+ if (!type.equals("BTXT") && !type.equals("RESO TEXT") && !type.equals("MTXT"))
+ throw new ParseError("Unknown text type found: "+type);
- bill.addSobiReference(sobiFile.getName());
- bill.setModified(date.getTime());
- saveBill(storage, bill);
- } catch (ParseError e) {
- throw e;
- } catch (Exception e) {
- logger.error("Unexpected Exception", e);
- throw new ParseError(e.getMessage(), blockData.toString());
+ if (action.equals("*DELETE*")) {
+ if (type.equals("BTXT") || type.equals("RESO TEXT")) {
+ bill.setFulltext("");
+ } else if (type.equals("MTXT")) {
+ bill.setMemo("");
}
- } catch (ParseError e) {
- logger.error("ParseError at "+fileName+":"+lineNum, e);
+ } else if (action.equals("*END*")) {
+ if (text != null) {
+ if (type.equals("BTXT") || type.equals("RESO TEXT")) {
+ bill.setFulltext(text.toString());
+ } else if (type.equals("MTXT")) {
+ bill.setMemo(text.toString());
+ }
+ text = null;
+ } else {
+ throw new ParseError("Text END Found before a body: "+line);
+ }
+ } else if (action.equals("")) {
+ if (text == null) {
+ //First header for this text segment so initialize
+ text = new StringBuffer();
+ text.ensureCapacity(data.length());
+ } else {
+ //Every 100th line is a repeated header for some reason
+ }
+ } else {
+ throw new ParseError("Unknown text action found: "+action);
}
+ } else if (text != null) {
+ // Remove the leading numbers
+ text.append(line.substring(5)+"\n");
- // cleanup
- blockData = new StringBuffer();
+ } else {
+ throw new ParseError("Text Body found before header: "+line);
}
+ }
- // Move our identifier forward
- oldBlock = newBlock;
-
- // Skip lines that did not match the SOBI format
- if (newBlock.equals("")) continue;
-
+ if (text != null) {
+ // This is a known issue that was resolved on 03/23/2011
+ if (new GregorianCalendar(2011, 3, 23).after(date)) {
+ throw new ParseError("Finished text data without a footer");
- if (blockData.length()==0) {
- // If we're starting from a blank slate, initialize block values
- billYear = sobiHeader.group(2);
- billId = sobiHeader.group(3).replaceAll("(?<=[A-Z])0*", "");
- billAmendment = sobiHeader.group(4).trim();
- lineCode = sobiHeader.group(5);
- blockData.append(sobiHeader.group(6));
} else {
- // Otherwise, build the data string, carry the new lines
- blockData.append("\r\n"+sobiHeader.group(6));
- }
-
- // Special case for the type 1 deletes!
- if (lineCode.equals("1") && sobiHeader.group(6).startsWith("DELETE")) {
- String bucket = billYear+"/bill/";
- String key = billId+billAmendment+"-"+String.valueOf(billYear);
- storage.del(bucket+key);
-
- if (!billAmendment.isEmpty()) {
- // We need to remove all references to this amendment
- String oldKey = billId+"-"+billYear;
- Bill oldBill = (Bill)storage.get(bucket+oldKey, Bill.class);
- oldBill.amendments.remove(key);
- storage.set(bucket+oldKey, oldBill);
- for (String ammendment : oldBill.amendments) {
- Bill bill = (Bill)storage.get(bucket+ammendment, Bill.class);
- bill.amendments.remove(key);
- storage.set(bucket+ammendment, bill);
- }
+ // Commit what we have and move on
+ if (type.equals("BTXT") || type.equals("RESO TEXT")) {
+ bill.setFulltext(text.toString());
+ } else if (type.equals("MTXT")) {
+ bill.setMemo(text.toString());
}
-
- blockData = new StringBuffer();
- oldBlock = "";
}
}
}
- public void applyBillEvent(String data, Bill bill, Date date) throws ParseError {
+ public void applyBillEvent(String data, Bill bill, Date date) throws ParseError
+ {
// currently we don't want to keep track of assembly committees
Boolean trackCommittees = !bill.getSenateBillNo().startsWith("A");
@@ -316,7 +426,7 @@ public void applyBillEvent(String data, Bill bill, Date date) throws ParseError
String currentCommittee = "";
List<String> pastCommittees = new ArrayList<String>();
- for (String line : data.split("\r\n")) {
+ for (String line : data.split("\n")) {
Matcher billEvent = billEventPattern.matcher(line);
if (billEvent.find()) {
try {
@@ -367,10 +477,10 @@ public void applyBillEvent(String data, Bill bill, Date date) throws ParseError
}
} catch (ParseException e) {
- throw new ParseError("eventDateFormat parse failure", billEvent.group(1));
+ throw new ParseError("eventDateFormat parse failure: "+billEvent.group(1));
}
} else {
- throw new ParseError("billEventPattern not matched", line);
+ throw new ParseError("billEventPattern not matched: "+line);
}
}
@@ -380,264 +490,4 @@ public void applyBillEvent(String data, Bill bill, Date date) throws ParseError
bill.setPastCommittees(pastCommittees);
bill.setStricken(stricken);
}
-
- public void applyText(String data, Bill bill, Date date) throws ParseError {
- // BillText and MemoText can be handled the same way
- String type = "";
- StringBuffer text = null;
-
- for (String line : data.split("\r\n")) {
- Matcher header = textPattern.matcher(line);
- if (line.startsWith("00000") && header.find()) {
- //TODO: This information should be used
- //TODO: If house == C then bills can be used for SAME AS
- //String house = header.group(1);
- //String bills = header.group(2).trim();
- String action = header.group(3).trim();
- type = header.group(4).trim();
- //String year = header.group(5);
-
- if (!type.equals("BTXT") && !type.equals("RESO TEXT") && !type.equals("MTXT"))
- throw new ParseError("Unknown text type found", type);
-
- if (action.equals("*DELETE*")) {
- if (type.equals("BTXT") || type.equals("RESO TEXT")) {
- bill.setFulltext("");
- } else if (type.equals("MTXT")) {
- bill.setMemo("");
- }
-
- } else if (action.equals("*END*")) {
- if (text != null) {
- if (type.equals("BTXT") || type.equals("RESO TEXT")) {
- bill.setFulltext(text.toString());
- } else if (type.equals("MTXT")) {
- bill.setMemo(text.toString());
- }
- text = null;
- } else {
- throw new ParseError("Text END Found before a body", line);
- }
- } else if (action.equals("")) {
- if (text == null) {
- //First header for this text segment so initialize
- text = new StringBuffer();
- text.ensureCapacity(data.length());
- } else {
- //Every 100th line is a repeated header for some reason
- }
- } else {
- throw new ParseError("Unknown text action found", action);
- }
- } else if (text != null) {
- // Remove the leading numbers
- text.append(line.substring(5)+"\n");
-
- } else {
- throw new ParseError("Text Body found before header", line);
- }
- }
-
- if (text != null) {
- // This is a known issue that was resolved on 03/23/2011
- if (textFooterResolveDate.after(date)) {
- throw new ParseError("Finished text data without a footer", "");
-
- // Commit what we have and move on
- } else {
- if (type.equals("BTXT") || type.equals("RESO TEXT")) {
- bill.setFulltext(text.toString());
- } else if (type.equals("MTXT")) {
- bill.setMemo(text.toString());
- }
- }
- }
- }
-
- public void applyProgramInfo(String data, Bill bill, Date date) throws ParseError {
- // This information currently isn't used for anything
- //if (!data.equals(""))
- // throw new ParseError("Program info not implemented", data);
- }
-
- public void applyVoteMemo(String data, Bill bill, Date date) throws ParseError {
- // Example of a double vote entry: SOBI.D110119.T140802.TXT:390
- Vote vote = null;
- for(String line : data.split("\r\n")) {
-
- //Start over if we hit a header, sometimes we get back to back entries
- Matcher voteHeader = voteHeaderPattern.matcher(line);
- if (voteHeader.find()) {
- try {
- // TODO: Parse out sequence number once LBDC includes it.
- vote = new Vote(bill, voteDateFormat.parse(voteHeader.group(2)), Vote.VOTE_TYPE_FLOOR, "1");
- } catch (ParseException e) {
- throw new ParseError("voteDateFormat not matched", line);
- }
-
- //Otherwise, build the existing vote
- } else if (vote!=null){
- Matcher voteLine = votePattern.matcher(line);
- while(voteLine.find()) {
- String type = voteLine.group(1).trim();
- Person voter = new Person(voteLine.group(2).trim());
-
- if (type.equals("Aye")) {
- vote.addAye(voter);
- } else if (type.equals("Nay")) {
- vote.addNay(voter);
- } else if (type.equals("Abs")) {
- vote.addAbsent(voter);
- } else if (type.equals("Abd")) {
- vote.addAbstain(voter);
- } else if (type.equals("Exc")) {
- vote.addExcused(voter);
- } else {
- throw new ParseError("Unknown vote type found", line);
- }
- }
-
- } else {
- throw new ParseError("Hit vote data without a header", data);
- }
- }
-
- //Misnomer, will actually update the vote if it already exists
- if (vote!=null)
- bill.addVote(vote);
- }
-
- public void applyActClause(String data, Bill bill, Date date) throws ParseError {
- if (data.trim().startsWith("DELETE")) {
- bill.setActClause("");
- } else {
- bill.setActClause(data.replace("\r\n", " ").trim());
- }
- }
-
- public void applyLawSection(String data, Bill bill, Date date) throws ParseError {
- // No DELETE command for law section
- if (data.contains("\r\n"))
- throw new ParseError("LawSection (2) blocks should only ever be 1 line long",data);
-
- bill.setLawSection(data.trim());
- }
-
- public void applyBillData(String data, Bill bill, Date date) throws ParseError{
- // Get rid of the nulls first and foremost
- data = data.replace('\0', ' ');
-
- // Sequential lines should be executed in order
- for(String line : data.split("\r\n")) {
-
- Matcher billData = billDataPattern.matcher(line);
- if (billData.find()) {
- //TODO: Find a possible use for this information
- String sponsor = billData.group(1).trim();
- //String reprint = billData.group(2);
- //String blurb = billData.group(3);
- String oldbill = billData.group(4).trim().replaceAll("[0-9`-]$", "");
- //String lbdnum = billData.group(5);
-
- if (!sponsor.isEmpty()) {
- bill.setSponsor(new Person(sponsor));
- }
- bill.addPreviousVersion(oldbill);
- } else {
- throw new ParseError("billDataPattern not matched", line);
- }
- }
- }
-
- public void applyLaw(String data, Bill bill, Date date) throws ParseError {
- if (data.trim().startsWith("DELETE")) {
- // The Law delete code should also remove the summary information
- bill.setLaw("");
- bill.setSummary("");
-
- } else {
- // We'll definitely need to clean this data up more than a little bit..
- // data = data.replaceAll("\\xBD", "");
- bill.setLaw(data.replace("\r\n", " ").replace("õ", "S").replace("ô","P").replace("�","S").replace((char)65533+"", "S").trim());
- }
- }
-
- public void applyCosponsors(String data, Bill bill, Date date) throws ParseError {
- // No DELETE code for coSponsors, sent through sponsor
- // instead which deletes the whole package
-
- // New values are always replacements of existing sets...
- ArrayList<Person> coSponsors = new ArrayList<Person>();
- if (!data.isEmpty()) {
- for(String coSponsor : data.replace("\r\n", " ").split(",")) {
- coSponsors.add(new Person(coSponsor.trim()));
- }
- }
- bill.setCoSponsors(coSponsors);
- }
-
- public void applyMultisponsors(String data, Bill bill, Date date) throws ParseError {
- // No DELETE code for multisponsors, sent through sponsor
- // instead which deletes the whole package
-
- // New values are always replacements of existing sets...
-
- ArrayList<Person> multiSponsors = new ArrayList<Person>();
- if (!data.isEmpty()) {
- for(String multiSponsor : data.replace("\r\n", " ").split(",")) {
- multiSponsors.add(new Person(multiSponsor.trim()));
- }
- }
- bill.setMultiSponsors(multiSponsors);
- }
-
- public void applySponsor(String data, Bill bill, Date date) throws ParseError{
- // Apply the lines in order given as each represents its own "block"
- for(String line : data.split("\r\n")) {
- if (line.startsWith("DELETE")) {
- // When we receive a delete for sponsor, remove ALL sponsor information
- bill.setSponsor(null);
- bill.setCoSponsors(new ArrayList<Person>());
- bill.setMultiSponsors(new ArrayList<Person>());
-
- } else {
- bill.setSponsor(new Person(line.trim()));
- }
- }
- }
-
- public void applySummary(String data, Bill bill, Date date) throws ParseError{
- // The DELETE code for the summary goes through the law block (B)
- // Combine the lines with a space and handle special character issues..
- // Again, I don't have any examples of these special characters right now
- // data = data.replace("›","S").replaceAll("\\x27(\\W|\\s)", "&apos;$1");
- bill.setSummary(data.replace("\r\n", " ").trim());
- }
-
- public void applyTitle(String data, Bill bill, Date date) throws ParseError {
- // No DELETE code for titles
- // Combine the lines with a space and handle special character issues..
- // Again, I don't have any examples of these special characters right now
- // data = data.replace("›","S").replaceAll("\\x27(\\W|\\s)", "&apos;$1");
- bill.setTitle(data.replace("\r\n", " ").trim());
- }
-
- public void applySameAs(String data, Bill bill, Date date) throws ParseError {
- // Like BillData lines, apply them in order because of DELETE
- for(String line : data.split("\r\n")) {
- if (line.trim().equals("No same as") || line.trim().startsWith("DELETE")) {
- bill.setSameAs("");
-
- } else {
- // Why do we do this, don't have an example of this issue..
- // line = line.replace("/", ",").replaceAll("[ \\-.;]", "");
- Matcher sameAs = sameAsPattern.matcher(line);
- if (sameAs.find()) {
- bill.setSameAs(sameAs.group(2).replace("-","").replace(" ",""));
- } else {
- logger.error("sameAsPattern not matched: "+data);
- }
- }
- }
- }
-}
+}
Please sign in to comment.
Something went wrong with that request. Please try again.