This is a quick prototype to pull a CSV out of a Document blob stored in Salesforce and render it in a table via Visualforce. It borrows heavily from the CSV parser by Marty Chang linked at the end of the post.
Parts are a bit rough around the edges. E.g. It is missing standard error checking.
CsvReader.apex
/**
* Slightly modified CSV parser
* Originally by MARTY Y. CHANG:
* http://frombelvideres4thfloor.blogspot.com.es/2010/10/ietf-rfc-4180-compliant-csv-reader-for.html
*/
public with sharing class CsvReader {
/**
* Comma String as defined by IETF RFC 4180.
*/
public static final String ParserCOMMA = String.fromCharArray(new List<Integer> { 44 });
/**
* Carriage return String as defined by Salesforce documentation.
*
* Force.com IDE Library >
* Apex Developer's Guide >
* Language Constructs >
* Data Types >
* Primitive Data Types
*/
public static final String ParserCR = '\r';
/**
* Double-quote String as defined by Salesforce documentation.
*
* Force.com IDE Library >
* Apex Developer's Guide >
* Language Constructs >
* Data Types >
* Primitive Data Types
*/
public static final String ParserDQUOTE = '\"';
/**
* Line feed String as defined by Salesforce documentation.
*
* Force.com IDE Library >
* Apex Developer's Guide >
* Language Constructs >
* Data Types >
* Primitive Data Types
*/
public static final String ParserLF = '\n';
/**
* Carriage return String followed by a line feed String.
*/
public static final String ParserCRLF = ParserCR + ParserLF;
/**
* Line feed String followed by a carriage return String.
*/
public static final String ParserLFCR = ParserLF + ParserCR;
/**
* Escaped double-quotes per IETF RFC 4180.
*/
public static final String ParserDQUOTEDQUOTE = ParserDQUOTE + ParserDQUOTE;
/**
* Returns a List containing Lists of Strings that represents
* the values contained in an IETF RFC 4180-compliant CSV file.
*
* Each element in the outer list represents a row in the CSV file.
* Each element in the inner list is the value in the field specified
* by the row-column combination.
*
* @param file the CSV file to read
* @return the List<List<String>> containing values read from the
* CSV file
*/
public static List<List<String>> readIETFRFC4180CSVFile(Blob file) {
String fileString = file.toString();
if (!fileString.endsWith(ParserCRLF)) {
fileString = fileString + ParserCRLF;
}
List<List<String>> fileValues = new List<List<String>>();
List<String> rowValues = new List<String>();
CSVValue csvValue = new CSVValue();
Boolean eod = false; // Whether end of CSV data is reached
while (!eod) {
System.debug(fileString);
csvValue = readIETFRFC4180CSVValue(fileString);
rowValues.add(csvValue.value);
if (csvValue.delimiter == ParserCRLF) {
fileValues.add(rowValues);
System.debug(rowValues);
if (fileValues.size() > 0) {
System.assertEquals(fileValues.get(0).size(),
rowValues.size());
}
rowValues = new List<String>();
}
if (csvValue.biteSize() == fileString.length()) {
eod = true;
}
else {
fileString = fileString.substring(csvValue.biteSize());
}
}
return fileValues;
}
/**
* Returns the first String value read from a String representation of
* data contained in an IETF RFC 4180-compliant CSV file.
*
* The data is assumed to be terminated with a CRLF.
*
* @param data the textual CSV data in one long string
* @return the first CSV value read from <code>data</code>.
* null is returned if no value is discerned.
*/
public static CSVValue readIETFRFC4180CSVValue(String data) {
System.assert(data.endsWith(ParserCRLF));
CSVValue csvValue = new CSVValue();
if (data.startsWith(ParserDQUOTE)) {
csvValue.enclosed = true;
Integer searchIndex = 1; // starting index to search
Integer dquoteIndex = -1; // index of DQUOTE
Integer dquotesIndex = -1; // index of DQUOTEDQUOTE
Boolean closerFound = false;
while (!closerFound) {
dquoteIndex = data.indexOf(ParserDQUOTE, searchIndex);
dquotesIndex = data.indexOf(ParserDQUOTEDQUOTE,
searchIndex);
System.assert(dquoteIndex != -1);
if (dquoteIndex == dquotesIndex) {
searchIndex = dquotesIndex
+ ParserDQUOTEDQUOTE.length();
}
else {
closerFound = true;
}
}
csvValue.value = data.substring(
ParserDQUOTE.length(), dquoteIndex)
.replaceAll(ParserDQUOTEDQUOTE, ParserDQUOTE);
Integer commaIndex = data.indexOf(ParserCOMMA, dquoteIndex);
Integer crlfIndex = data.indexOf(ParserCRLF, dquoteIndex);
if (commaIndex != -1 && commaIndex < crlfIndex) {
csvValue.delimiter = ParserCOMMA;
}
else {
csvValue.delimiter = ParserCRLF;
}
}
else {
csvValue.enclosed = false;
Integer commaIndex = data.indexOf(ParserCOMMA);
Integer crlfIndex = data.indexOf(ParserCRLF);
if (commaIndex != -1 && commaIndex < crlfIndex) {
csvValue.value = data.substring(0, commaIndex);
csvValue.delimiter = ParserCOMMA;
}
else {
csvValue.value = data.substring(0, crlfIndex);
csvValue.delimiter = ParserCRLF;
}
}
System.debug('Returning: ' + csvValue);
return csvValue;
}
/**
* CSVValue is a class structure containing information about a CSV
* value that was read from a CSV file, including such information as
* whether the value was encapsulated in double-quotes.
*/
private class CSVValue {
/**
* The field value that was read from the CSV file.
*/
public String value;
/**
* Whether the value was surrounded by double-quotes.
*/
public Boolean enclosed;
/**
* The comma or CRLF delimiter that identified the end of the CSV value.
*/
public String delimiter;
/**
* Default constructor, setting all members to null.
*/
public CSVValue() {
this(null, null, null);
}
/**
* Constructor.
*
* @param value the field value
* @param enclosed whether the value was surrounded by double-quotes
* @param delimiter the delimiter that identified the end
* of the CSV value
*/
public CSVValue(String value, Boolean enclosed, String delimiter) {
this.value = value;
this.enclosed = enclosed;
this.delimiter = delimiter;
}
/**
* Returns the number of characters to remove from the data
* String which produced the CSVValue in order to reach the next
* value in the data String.
*/
public Integer biteSize() {
Integer biteSize = value
.replaceAll(ParserDQUOTE, ParserDQUOTEDQUOTE).length()
+ delimiter.length();
if (enclosed) {
biteSize += ParserDQUOTE.length() * 2;
}
System.debug('biteSize: ' + biteSize);
return biteSize;
}
/**
* Returns whether a CSVValue has the same <code>value</code> and
* <code>enclosed</code> as another CSVValue.
*/
public Boolean equals(CSVValue compCSVValue) {
return this.value.equals(compCSVValue.value)
&& this.enclosed == compCSVValue.enclosed
&& this.delimiter == compCSVValue.delimiter;
}
/**
* Asserts that two <code>CSVValue</code> instances have the same
* <code>value</code> and <code>enclosed</code>.
*/
public void assertEquals(CSVValue compCSVValue) {
System.assertEquals(value, compCSVValue.value);
System.assertEquals(enclosed, compCSVValue.enclosed);
System.assertEquals(delimiter, compCSVValue.delimiter);
}
}
/**
* Test some use cases for reading IETF RFC 4180-compliant CSV values.
*/
/* TODO: Move to a separate class
public static testMethod void readIETFRFC4180CSVValueTest() {
String data = null; // Placeholder for data to use in testing.
System.debug(data = ParserCRLF);
new CSVValue('', false, ParserCRLF)
.assertEquals(readIETFRFC4180CSVValue(data));
System.debug(data = '""' + ParserCRLF);
new CSVValue('', true, ParserCRLF)
.assertEquals(readIETFRFC4180CSVValue(data));
System.debug(data = '"",asdf' + ParserCRLF);
new CSVValue('', true, ParserCOMMA)
.assertEquals(readIETFRFC4180CSVValue(data));
System.debug(data = ',asdf' + ParserCRLF);
new CSVValue('', false, ParserCOMMA)
.assertEquals(readIETFRFC4180CSVValue(data));
System.debug(data = '"' + ParserCRLF + '",blah' + ParserCRLF);
new CSVValue(ParserCRLF, true, ParserCOMMA)
.assertEquals(readIETFRFC4180CSVValue(data));
System.debug(data = '"""marty""","""chang"""' + ParserCRLF);
new CSVValue('"marty"', true, ParserCOMMA)
.assertEquals(readIETFRFC4180CSVValue(data));
System.debug(data = '"com""pli""cate' + ParserCRLF + 'd"'
+ ParserCRLF);
new CSVValue('com"pli"cate' + ParserCRLF + 'd', true, ParserCRLF)
.assertEquals(readIETFRFC4180CSVValue(data));
System.debug(data = 'asdf' + ParserCRLF);
new CSVValue('asdf', false, ParserCRLF)
.assertEquals(readIETFRFC4180CSVValue(data));
}
*/
}
ViewCsvController.apex
public with sharing class ViewCsvController {
public List<string> header_row { get; set; }
public Map<integer, List<string>> row_map { get; set; }
public ViewCsvController() {
// Sample data that can be used if there isn't a document available.
/*
header_row = new List<string>{'Column 1', 'Column 2'};
row_map = new Map<integer, List<string>>();
row_map.put(1, new List<string>{'AAA', 'BBB'});
row_map.put(2, new List<string>{'CCC', 'DDD'});
*/
Id documentId = ApexPages.currentPage().getParameters().get('docId');
if(documentId == null) {
documentId = '015400000000001';
}
List<Document> docs = [Select Id, Body from Document where Id = :documentId];
// TODO: Check that the list is not empty.
List<List<String>> fileValues = CsvReader.readIETFRFC4180CSVFile(docs[0].Body);
header_row = fileValues[0];
row_map = new Map<integer, List<string>>();
for(integer i = 1; i < fileValues.size(); i++) {
row_map.put(i, fileValues[i]);
}
}
}
ViewCsv.page
<apex:page showHeader="true" sidebar="true" controller="ViewCsvController">
<table border="1">
<tr>
<apex:repeat value="{!header_row}" var="h_col">
<td><b>{!h_col}</b></td>
</apex:repeat>
</tr>
<apex:repeat value="{!row_map}" var="idx">
<tr>
<apex:repeat value="{!row_map[idx]}" var="r_col">
<td>{!r_col}</td>
</apex:repeat>
</tr>
</apex:repeat>
</table>
</apex:page>
See also:
- Code Samples: Parse a CSV with APEX
- MARTY Y. CHANG: IETF RFC 4180-compliant CSV Reader for Salesforce
- Display CSV data on a Visualforce page