Commit 96bae2b8 authored by Maxym Borodenko's avatar Maxym Borodenko

Added new API endpoints for downloading MCPD of selected accessions;

Updated generating methods;
parent fb418b1b
...@@ -486,6 +486,51 @@ public class AccessionController { ...@@ -486,6 +486,51 @@ public class AccessionController {
} }
} }
@PreAuthorize("isAuthenticated()")
@RequestMapping(value = "/download-selected", method = RequestMethod.POST, params = { "mcpd" })
public void downloadMcpdByUuids(@RequestParam(value="uuids", required = true) Set<UUID> uuids, HttpServletResponse response) throws IOException {
// Create JSON filter
AccessionFilter filter = new AccessionFilter();
filter.uuid().addAll(uuids);
final long countFiltered = accessionService.countAccessions(filter);
if (countFiltered > DOWNLOAD_LIMIT) {
throw new InvalidApiUsageException("Refusing to export more than " + DOWNLOAD_LIMIT + " entries");
}
// Write MCPD to the stream.
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.addHeader("Content-Disposition", String.format("attachment; filename=\"genesys-accessions-selected-%1s.xlsx\"", System.currentTimeMillis()));
final OutputStream outputStream = response.getOutputStream();
try {
downloadService.writeXlsxMCPD(filter, outputStream, null, null);
response.flushBuffer();
} catch (EOFException e) {
LOG.warn("Download was aborted: {}", e.getMessage());
}
}
@RequestMapping(value = "/download-selected", method = RequestMethod.POST, params = { "dwca" })
public void downloadDwcaByUuids(@RequestParam(value="uuids", required = true) Set<UUID> uuids, HttpServletResponse response) throws Exception {
// Create JSON filter
AccessionFilter filter = new AccessionFilter();
filter.uuid().addAll(uuids);
final long countFiltered = accessionService.countAccessions(filter);
if (countFiltered > DOWNLOAD_LIMIT) {
throw new InvalidApiUsageException("Refusing to export more than " + DOWNLOAD_LIMIT + " entries");
}
// Write Darwin Core Archive to the stream.
response.setContentType("application/zip");
response.addHeader("Content-Disposition", String.format("attachment; filename=\"genesys-accessions-selected-%1$s.zip\"", System.currentTimeMillis()));
final OutputStream outputStream = response.getOutputStream();
genesysService.writeAccessions(filter, outputStream, null, null);
response.flushBuffer();
}
@PreAuthorize("isAuthenticated()") @PreAuthorize("isAuthenticated()")
@RequestMapping(value = "/download", method = RequestMethod.POST, params = { "mcpd" }) @RequestMapping(value = "/download", method = RequestMethod.POST, params = { "mcpd" })
public void downloadMcpd(@RequestParam(value = "f", required = false, defaultValue = "") String filterCode, @RequestParam(value="filter", required = false) AccessionFilter filter, HttpServletResponse response) throws IOException { public void downloadMcpd(@RequestParam(value = "f", required = false, defaultValue = "") String filterCode, @RequestParam(value="filter", required = false) AccessionFilter filter, HttpServletResponse response) throws IOException {
...@@ -505,7 +550,7 @@ public class AccessionController { ...@@ -505,7 +550,7 @@ public class AccessionController {
final OutputStream outputStream = response.getOutputStream(); final OutputStream outputStream = response.getOutputStream();
try { try {
downloadService.writeXlsxMCPD(filterInfo.filter, outputStream); downloadService.writeXlsxMCPD(filterInfo.filter, outputStream, filterInfo.filterCode, "/a/" + filterInfo.filterCode);
response.flushBuffer(); response.flushBuffer();
} catch (EOFException e) { } catch (EOFException e) {
LOG.warn("Download was aborted: {}", e.getMessage()); LOG.warn("Download was aborted: {}", e.getMessage());
...@@ -532,7 +577,7 @@ public class AccessionController { ...@@ -532,7 +577,7 @@ public class AccessionController {
final OutputStream outputStream = response.getOutputStream(); final OutputStream outputStream = response.getOutputStream();
try { try {
downloadService.writeXlsxPDCI(filterInfo.filter, outputStream); downloadService.writeXlsxPDCI(filterInfo.filter, outputStream, filterInfo.filterCode, "/a/" + filterInfo.filterCode);
response.flushBuffer(); response.flushBuffer();
} catch (EOFException e) { } catch (EOFException e) {
LOG.warn("Download was aborted: {}", e.getMessage()); LOG.warn("Download was aborted: {}", e.getMessage());
...@@ -555,7 +600,7 @@ public class AccessionController { ...@@ -555,7 +600,7 @@ public class AccessionController {
response.addHeader("Content-Disposition", String.format("attachment; filename=\"genesys-accessions-%1$s.zip\"", filterInfo.filterCode)); response.addHeader("Content-Disposition", String.format("attachment; filename=\"genesys-accessions-%1$s.zip\"", filterInfo.filterCode));
final OutputStream outputStream = response.getOutputStream(); final OutputStream outputStream = response.getOutputStream();
genesysService.writeAccessions(filterInfo.filter, outputStream); genesysService.writeAccessions(filterInfo.filter, outputStream, filterInfo.filterCode, "/a/" + filterInfo.filterCode);
response.flushBuffer(); response.flushBuffer();
} }
......
...@@ -200,7 +200,7 @@ public class InstituteController { ...@@ -200,7 +200,7 @@ public class InstituteController {
response.addHeader("Content-Disposition", String.format("attachment; filename=\"genesys-accessions-%1$s.zip\"", faoInstitute.getCode())); response.addHeader("Content-Disposition", String.format("attachment; filename=\"genesys-accessions-%1$s.zip\"", faoInstitute.getCode()));
final OutputStream outputStream = response.getOutputStream(); final OutputStream outputStream = response.getOutputStream();
genesysService.writeAccessions(filter, outputStream); genesysService.writeAccessions(filter, outputStream, null, "/wiews/" + faoInstitute.getCode());
response.flushBuffer(); response.flushBuffer();
} }
...@@ -224,7 +224,7 @@ public class InstituteController { ...@@ -224,7 +224,7 @@ public class InstituteController {
final OutputStream outputStream = response.getOutputStream(); final OutputStream outputStream = response.getOutputStream();
try { try {
downloadService.writeXlsxPDCI(filter, outputStream); downloadService.writeXlsxPDCI(filter, outputStream, null, "/wiews/" + faoInstitute.getCode());
response.flushBuffer(); response.flushBuffer();
} catch (EOFException e) { } catch (EOFException e) {
LOG.warn("Download was aborted", e); LOG.warn("Download was aborted", e);
...@@ -251,7 +251,7 @@ public class InstituteController { ...@@ -251,7 +251,7 @@ public class InstituteController {
final OutputStream outputStream = response.getOutputStream(); final OutputStream outputStream = response.getOutputStream();
try { try {
downloadService.writeXlsxMCPD(filter, outputStream); downloadService.writeXlsxMCPD(filter, outputStream, null, "/wiews/" + faoInstitute.getCode());
response.flushBuffer(); response.flushBuffer();
} catch (EOFException e) { } catch (EOFException e) {
LOG.warn("Download was aborted", e); LOG.warn("Download was aborted", e);
......
...@@ -39,15 +39,15 @@ import org.genesys2.server.service.filter.AccessionFilter; ...@@ -39,15 +39,15 @@ import org.genesys2.server.service.filter.AccessionFilter;
*/ */
public interface DownloadService { public interface DownloadService {
void writeXlsxMCPD(AccessionFilter accessionFilter, OutputStream outputStream) throws IOException; void writeXlsxMCPD(AccessionFilter accessionFilter, OutputStream outputStream, String shortFilter, String dataSource) throws IOException;
void writeXlsxMCPD(Predicate predicate, OutputStream outputStream, String filters, String dataSource) throws IOException; void writeXlsxMCPD(Predicate predicate, OutputStream outputStream, String shortFilter, String dataSource) throws IOException;
void writeXlsxMCPD(JPQLQuery<Long> queryAccessionId, OutputStream outputStream, String filters, String dataSource) throws IOException; void writeXlsxMCPD(JPQLQuery<Long> queryAccessionId, OutputStream outputStream, String shortFilter, String dataSource) throws IOException;
void writeXlsxMCPD(IDownloadAction action, OutputStream outputStream, String filters, String dataSource) throws IOException; void writeXlsxMCPD(IDownloadAction action, OutputStream outputStream, String shortFilter, String dataSource) throws IOException;
void writeXlsxPDCI(AccessionFilter accessionFilter, OutputStream outputStream) throws IOException; void writeXlsxPDCI(AccessionFilter accessionFilter, OutputStream outputStream, String shortFilter, String dataSource) throws IOException;
void writeXlsxDescriptor(List<Descriptor> descriptors, OutputStream outputStream) throws IOException; void writeXlsxDescriptor(List<Descriptor> descriptors, OutputStream outputStream) throws IOException;
......
...@@ -152,7 +152,7 @@ public interface GenesysService { ...@@ -152,7 +152,7 @@ public interface GenesysService {
long countDatasets(FaoInstitute faoInstitute); long countDatasets(FaoInstitute faoInstitute);
void writeAccessions(AccessionFilter filter, OutputStream outputStream) throws Exception; void writeAccessions(AccessionFilter filter, OutputStream outputStream, String shortFilter, String dataSource) throws Exception;
List<AccessionGeo> listAccessionsGeo(Set<Long> copy); List<AccessionGeo> listAccessionsGeo(Set<Long> copy);
......
...@@ -21,7 +21,6 @@ import java.io.OutputStream; ...@@ -21,7 +21,6 @@ import java.io.OutputStream;
import java.io.OutputStreamWriter; import java.io.OutputStreamWriter;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.net.URLEncoder;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
import java.text.MessageFormat; import java.text.MessageFormat;
...@@ -168,9 +167,6 @@ public class DownloadServiceImpl implements DownloadService { ...@@ -168,9 +167,6 @@ public class DownloadServiceImpl implements DownloadService {
@Autowired @Autowired
private TraitService traitService; private TraitService traitService;
@Value("${base.url}")
private String baseUrl;
@Value("${frontend.url}") @Value("${frontend.url}")
private String frontendUrl; private String frontendUrl;
...@@ -197,9 +193,7 @@ public class DownloadServiceImpl implements DownloadService { ...@@ -197,9 +193,7 @@ public class DownloadServiceImpl implements DownloadService {
} }
@Override @Override
public void writeXlsxMCPD(AccessionFilter filter, OutputStream outputStream) throws IOException { public void writeXlsxMCPD(AccessionFilter filter, OutputStream outputStream, String shortFilter, String dataSource) throws IOException {
final String dataSource = baseUrl + "/explore?filter=" + filter.toString();
if (filter.isFulltextQuery()) { if (filter.isFulltextQuery()) {
writeXlsxMCPD((action) -> { writeXlsxMCPD((action) -> {
try { try {
...@@ -208,14 +202,14 @@ public class DownloadServiceImpl implements DownloadService { ...@@ -208,14 +202,14 @@ public class DownloadServiceImpl implements DownloadService {
} catch (Exception e) { } catch (Exception e) {
LOG.warn("Error generating: {}", e.getMessage()); LOG.warn("Error generating: {}", e.getMessage());
} }
}, outputStream, filter.toString(), dataSource); }, outputStream, shortFilter, dataSource);
} else { } else {
writeXlsxMCPD(filter.buildPredicate(), outputStream, filter.toString(), dataSource); writeXlsxMCPD(filter.buildPredicate(), outputStream, shortFilter, dataSource);
} }
} }
@Override @Override
public void writeXlsxMCPD(Predicate predicate, OutputStream outputStream, String filters, String dataSource) throws IOException { public void writeXlsxMCPD(Predicate predicate, OutputStream outputStream, String shortFilter, String dataSource) throws IOException {
PathBuilder<Accession> builder = new PathBuilderFactory().create(Accession.class); PathBuilder<Accession> builder = new PathBuilderFactory().create(Accession.class);
Querydsl querydsl = new Querydsl(entityManager, builder); Querydsl querydsl = new Querydsl(entityManager, builder);
JPQLQuery<Long> query = querydsl.createQuery(QAccession.accession) JPQLQuery<Long> query = querydsl.createQuery(QAccession.accession)
...@@ -227,11 +221,11 @@ public class DownloadServiceImpl implements DownloadService { ...@@ -227,11 +221,11 @@ public class DownloadServiceImpl implements DownloadService {
// apply filter // apply filter
query.where(predicate); query.where(predicate);
writeXlsxMCPD(query, outputStream, filters, dataSource); writeXlsxMCPD(query, outputStream, shortFilter, dataSource);
} }
@Override @Override
public void writeXlsxMCPD(JPQLQuery<Long> queryAccessionId, OutputStream outputStream, String filters, String dataSource) throws IOException { public void writeXlsxMCPD(JPQLQuery<Long> queryAccessionId, OutputStream outputStream, String shortFilter, String dataSource) throws IOException {
writeXlsxMCPD((action) -> { writeXlsxMCPD((action) -> {
try { try {
accessionProcessor.process(queryAccessionId, action, null); accessionProcessor.process(queryAccessionId, action, null);
...@@ -239,11 +233,11 @@ public class DownloadServiceImpl implements DownloadService { ...@@ -239,11 +233,11 @@ public class DownloadServiceImpl implements DownloadService {
} catch (Exception e) { } catch (Exception e) {
LOG.warn("Error generating: {}", e.getMessage()); LOG.warn("Error generating: {}", e.getMessage());
} }
}, outputStream, filters, dataSource); }, outputStream, shortFilter, dataSource);
} }
@Override @Override
public void writeXlsxMCPD(IDownloadAction action, OutputStream outputStream, String filters, String dataSource) throws IOException { public void writeXlsxMCPD(IDownloadAction action, OutputStream outputStream, String shortFilter, String dataSource) throws IOException {
XSSFWorkbook template = new XSSFWorkbook(getClass().getResourceAsStream("/template/download/MCPD.xlsx")); XSSFWorkbook template = new XSSFWorkbook(getClass().getResourceAsStream("/template/download/MCPD.xlsx"));
POIXMLProperties props = template.getProperties(); POIXMLProperties props = template.getProperties();
...@@ -251,7 +245,9 @@ public class DownloadServiceImpl implements DownloadService { ...@@ -251,7 +245,9 @@ public class DownloadServiceImpl implements DownloadService {
coreProp.setCreated(new Nullable<Date>(new Date())); coreProp.setCreated(new Nullable<Date>(new Date()));
POIXMLProperties.CustomProperties custProp = props.getCustomProperties(); POIXMLProperties.CustomProperties custProp = props.getCustomProperties();
custProp.addProperty("Filter", filters); if (StringUtils.isNotBlank(shortFilter)) {
custProp.addProperty("Filter", shortFilter);
}
custProp.addProperty("Genesys URL", frontendUrl); custProp.addProperty("Genesys URL", frontendUrl);
// keep 50 rows in memory, exceeding rows will be flushed to disk // keep 50 rows in memory, exceeding rows will be flushed to disk
...@@ -262,23 +258,30 @@ public class DownloadServiceImpl implements DownloadService { ...@@ -262,23 +258,30 @@ public class DownloadServiceImpl implements DownloadService {
Sheet legal = wb.getXSSFSheet("Legal information"); Sheet legal = wb.getXSSFSheet("Legal information");
Row r; Row r;
Cell c; Cell c;
r = legal.createRow(0); int row = 0;
r = legal.createRow(row++);
r.createCell(0).setCellValue("Server URL"); r.createCell(0).setCellValue("Server URL");
r.createCell(1).setCellValue(frontendUrl); r.createCell(1).setCellValue(frontendUrl);
r = legal.createRow(1);
r.createCell(0).setCellValue("Filters");
r.createCell(1).setCellValue(filters);
r = legal.createRow(2);
r.createCell(0).setCellValue("Data source");
c = r.createCell(1);
c.setCellValue(dataSource);
r = legal.createRow(3); if (StringUtils.isNotBlank(shortFilter)) {
r = legal.createRow(row++);
r.createCell(0).setCellValue("Filters");
r.createCell(1).setCellValue(shortFilter);
}
if (StringUtils.isNotBlank(dataSource)) {
r = legal.createRow(row++);
r.createCell(0).setCellValue("Data source");
c = r.createCell(1);
c.setCellValue(frontendUrl + dataSource);
}
r = legal.createRow(row++);
r.createCell(0).setCellValue("Date"); r.createCell(0).setCellValue("Date");
c = r.createCell(1); c = r.createCell(1);
c.setCellStyle(dateStyle); c.setCellStyle(dateStyle);
c.setCellValue(new Date()); c.setCellValue(new Date());
r = legal.createRow(4); r = legal.createRow(row);
r.createCell(0).setCellValue("Attribution"); r.createCell(0).setCellValue("Attribution");
r.createCell(1).setCellValue(frontendUrl + "/content/terms"); r.createCell(1).setCellValue(frontendUrl + "/content/terms");
...@@ -787,7 +790,7 @@ public class DownloadServiceImpl implements DownloadService { ...@@ -787,7 +790,7 @@ public class DownloadServiceImpl implements DownloadService {
} }
@Override @Override
public void writeXlsxPDCI(final AccessionFilter filter, final OutputStream outputStream) throws IOException { public void writeXlsxPDCI(final AccessionFilter filter, final OutputStream outputStream, String shortFilter, String dataSource) throws IOException {
XSSFWorkbook template = new XSSFWorkbook(getClass().getResourceAsStream("/template/download/PDCI.xlsx")); XSSFWorkbook template = new XSSFWorkbook(getClass().getResourceAsStream("/template/download/PDCI.xlsx"));
...@@ -801,22 +804,29 @@ public class DownloadServiceImpl implements DownloadService { ...@@ -801,22 +804,29 @@ public class DownloadServiceImpl implements DownloadService {
Sheet legal = wb.getXSSFSheet("Legal information"); Sheet legal = wb.getXSSFSheet("Legal information");
Row r; Row r;
Cell c; Cell c;
r = legal.createRow(0); int row = 0;
r = legal.createRow(row++);
r.createCell(0).setCellValue("Server URL"); r.createCell(0).setCellValue("Server URL");
updateCellUrl(r, 1, frontendUrl); updateCellUrl(r, 1, frontendUrl);
r = legal.createRow(1);
r.createCell(0).setCellValue("Filters"); if (StringUtils.isNotBlank(shortFilter)) {
r.createCell(1).setCellValue(filter.toString()); r = legal.createRow(row++);
r = legal.createRow(2); r.createCell(0).setCellValue("Filters");
r.createCell(0).setCellValue("Data source"); r.createCell(1).setCellValue(shortFilter);
updateCellUrl(r, 1, baseUrl + "/explore?filter=" + URLEncoder.encode(filter.toString(), "UTF-8")); }
r = legal.createRow(3); if (StringUtils.isNotBlank(dataSource)) {
r = legal.createRow(row++);
r.createCell(0).setCellValue("Data source");
updateCellUrl(r, 1, frontendUrl + dataSource);
}
r = legal.createRow(row++);
r.createCell(0).setCellValue("Date"); r.createCell(0).setCellValue("Date");
c = r.createCell(1); c = r.createCell(1);
c.setCellStyle(dateStyle); c.setCellStyle(dateStyle);
c.setCellValue(new Date()); c.setCellValue(new Date());
r = legal.createRow(4); r = legal.createRow(row);
r.createCell(0).setCellValue("Attribution"); r.createCell(0).setCellValue("Attribution");
updateCellUrl(r, 1, frontendUrl + "/content/terms"); updateCellUrl(r, 1, frontendUrl + "/content/terms");
......
...@@ -41,6 +41,7 @@ import java.util.zip.ZipOutputStream; ...@@ -41,6 +41,7 @@ import java.util.zip.ZipOutputStream;
import javax.persistence.EntityManager; import javax.persistence.EntityManager;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.genesys.blocks.security.SecurityContextUtil; import org.genesys.blocks.security.SecurityContextUtil;
import org.genesys.blocks.security.model.AclSid; import org.genesys.blocks.security.model.AclSid;
import org.genesys.blocks.security.service.CustomAclService; import org.genesys.blocks.security.service.CustomAclService;
...@@ -105,6 +106,7 @@ import org.slf4j.Logger; ...@@ -105,6 +106,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
...@@ -131,6 +133,9 @@ public class GenesysServiceImpl implements GenesysService, DatasetService { ...@@ -131,6 +133,9 @@ public class GenesysServiceImpl implements GenesysService, DatasetService {
public static final Logger LOG = LoggerFactory.getLogger(GenesysServiceImpl.class); public static final Logger LOG = LoggerFactory.getLogger(GenesysServiceImpl.class);
@Value("${frontend.url}")
private String frontendUrl;
@Autowired @Autowired
private EntityManager entityManager; private EntityManager entityManager;
...@@ -1072,19 +1077,23 @@ public class GenesysServiceImpl implements GenesysService, DatasetService { ...@@ -1072,19 +1077,23 @@ public class GenesysServiceImpl implements GenesysService, DatasetService {
@Override @Override
// TODO FIXME Need proper term URLs // TODO FIXME Need proper term URLs
public void writeAccessions(final AccessionFilter filter, final OutputStream outputStream) throws Exception { public void writeAccessions(final AccessionFilter filter, final OutputStream outputStream, String shortFilter, String dataSource) throws Exception {
// UTF8 is used for encoding entry names // UTF8 is used for encoding entry names
final ZipOutputStream zos = new ZipOutputStream(outputStream); final ZipOutputStream zos = new ZipOutputStream(outputStream);
zos.setComment("Genesys Accessions filter=" + filter); final StringBuilder commentBuilder = new StringBuilder("Genesys Accessions");
if (StringUtils.isNotBlank(shortFilter)) {
commentBuilder.append(" ").append("filter=").append(shortFilter);
}
zos.setComment(commentBuilder.toString());
zos.flush(); zos.flush();
// Filter information // Filter information
final ZipEntry readmeEntry = new ZipEntry("README.txt"); final ZipEntry readmeEntry = new ZipEntry("README.txt");
readmeEntry.setComment("Extra iformation"); readmeEntry.setComment("Extra information");
readmeEntry.setTime(System.currentTimeMillis()); readmeEntry.setTime(System.currentTimeMillis());
zos.putNextEntry(readmeEntry); zos.putNextEntry(readmeEntry);
writeREADME(filter, zos); writeREADME(zos, shortFilter, dataSource);
zos.closeEntry(); zos.closeEntry();
zos.flush(); zos.flush();
...@@ -1206,19 +1215,24 @@ public class GenesysServiceImpl implements GenesysService, DatasetService { ...@@ -1206,19 +1215,24 @@ public class GenesysServiceImpl implements GenesysService, DatasetService {
outputStream.flush(); outputStream.flush();
} }
private void writeREADME(AccessionFilter filter, ZipOutputStream zos) throws IOException { private void writeREADME(ZipOutputStream zos, String shortFilter, String dataSource) throws IOException {
final BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(zos)); final BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(zos));
bw.write("Date:\t"); bw.write("Date:\t");
bw.write(new Date().toString()); bw.write(new Date().toString());
bw.write("\n"); bw.write("\n");
bw.write("Filter:\t"); if (StringUtils.isNotBlank(shortFilter)) {
bw.write(filter.toString()); bw.write("Filter:\t");
bw.write("\n"); bw.write(shortFilter);
bw.write("\n");
}
bw.write("URL:\thttps://www.genesys-pgr.org/explore?filter="); if (StringUtils.isNotBlank(dataSource)) {
bw.write(filter.toString()); bw.write("URL:\t");
bw.write("\n"); bw.write(frontendUrl);
bw.write(dataSource);
bw.write("\n");
}
bw.write("Attribution:\t"); bw.write("Attribution:\t");
bw.write("https://www.genesys-pgr.org/content/terms"); bw.write("https://www.genesys-pgr.org/content/terms");
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment