From a9753705f7a5d5020af6e4bcd8a0403eb7f71e84 Mon Sep 17 00:00:00 2001 From: "Nazarov, Rodion" Date: Tue, 3 Oct 2023 21:58:47 +0200 Subject: [PATCH] [Upd] Add support for the merged cells in the xls files in TabularModule. --- .../java/cz/cvut/spipes/constants/CSVW.java | 1 + .../cz/cvut/spipes/constants/KBSS_CSVW.java | 2 + .../cz/cvut/spipes/modules/TabularModule.java | 23 +++ .../cz/cvut/spipes/modules/model/Cell.java | 70 +++++++++ .../cz/cvut/spipes/modules/model/Region.java | 46 ++++++ .../spipes/modules/util/XLS2TSVConvertor.java | 30 ++++ .../spipes/modules/TabularModuleTest.java | 5 +- .../resources/merged-xls-model-output.ttl | 137 ++++++++++++++++++ .../src/test/resources/merged.xls | Bin 0 -> 26624 bytes 9 files changed, 312 insertions(+), 2 deletions(-) create mode 100644 s-pipes-modules/module-tabular/src/main/java/cz/cvut/spipes/modules/model/Cell.java create mode 100644 s-pipes-modules/module-tabular/src/main/java/cz/cvut/spipes/modules/model/Region.java create mode 100644 s-pipes-modules/module-tabular/src/test/resources/merged-xls-model-output.ttl create mode 100644 s-pipes-modules/module-tabular/src/test/resources/merged.xls diff --git a/s-pipes-modules/module-tabular/src/main/java/cz/cvut/spipes/constants/CSVW.java b/s-pipes-modules/module-tabular/src/main/java/cz/cvut/spipes/constants/CSVW.java index baf17064..6a9b06ab 100644 --- a/s-pipes-modules/module-tabular/src/main/java/cz/cvut/spipes/constants/CSVW.java +++ b/s-pipes-modules/module-tabular/src/main/java/cz/cvut/spipes/constants/CSVW.java @@ -54,6 +54,7 @@ public static Property extendedProperty(String local ) public static final String tableUri = uri + "table"; public static final String rowUri = uri + "row"; public static final String RowUri = uri + "Row"; + public static final String CellUri = uri + "Cell"; public static final String URL = uri + "url"; public static final String rowNumUri = uri + "rownum"; public static final String describesUri = uri + "describes"; diff --git a/s-pipes-modules/module-tabular/src/main/java/cz/cvut/spipes/constants/KBSS_CSVW.java b/s-pipes-modules/module-tabular/src/main/java/cz/cvut/spipes/constants/KBSS_CSVW.java index 5b3b4c17..787ffbea 100644 --- a/s-pipes-modules/module-tabular/src/main/java/cz/cvut/spipes/constants/KBSS_CSVW.java +++ b/s-pipes-modules/module-tabular/src/main/java/cz/cvut/spipes/constants/KBSS_CSVW.java @@ -22,6 +22,8 @@ protected static Property property(String local ) public static final String propertyUri = uri + "property"; + public static final String sameValueAsUri = uri + "same-value-as"; + public static final Property property = ResourceFactory.createProperty(propertyUri); /** diff --git a/s-pipes-modules/module-tabular/src/main/java/cz/cvut/spipes/modules/TabularModule.java b/s-pipes-modules/module-tabular/src/main/java/cz/cvut/spipes/modules/TabularModule.java index 6cb1a4f4..2049474a 100644 --- a/s-pipes-modules/module-tabular/src/main/java/cz/cvut/spipes/modules/TabularModule.java +++ b/s-pipes-modules/module-tabular/src/main/java/cz/cvut/spipes/modules/TabularModule.java @@ -190,6 +190,8 @@ ExecutionContext executeSelf() { tableGroup = onTableGroup(null); table = onTable(null); + StreamResource originalSourceResource = sourceResource; + switch (sourceResourceFormat) { case HTML: HTML2TSVConvertor htmlConvertor = new HTML2TSVConvertor(); @@ -332,6 +334,27 @@ ExecutionContext executeSelf() { em.getTransaction().begin(); em.persist(tableGroup); em.merge(tableSchema); + + if(sourceResourceFormat == ResourceFormat.EXCEL) { + XLS2TSVConvertor xls2TSVConvertor = new XLS2TSVConvertor(); + List regions = xls2TSVConvertor.getMergedRegions(originalSourceResource, processSpecificSheetInXLSFile); + int cellsNum = 1; + for (Region region : regions) { + int firstCellInRegionNum = cellsNum; + for(int i = region.getFirstRow();i <= region.getLastRow();i++){ + for(int j = region.getFirstColumn();j <= region.getLastColumn();j++) { + Cell cell = new Cell("http://example.org/cell"+(cellsNum)); + cell.setRowName(tableSchema.createAboutUrl(i)); + cell.setColumnName(outputColumns.get(j).getUri().toString()); + if(cellsNum != firstCellInRegionNum) + cell.setSameValueAsCell("http://example.org/cell"+(firstCellInRegionNum)); + em.merge(cell); + cellsNum++; + } + } + } + } + em.getTransaction().commit(); Model persistedModel = JopaPersistenceUtils.getDataset(em).getDefaultModel(); em.getEntityManagerFactory().close(); diff --git a/s-pipes-modules/module-tabular/src/main/java/cz/cvut/spipes/modules/model/Cell.java b/s-pipes-modules/module-tabular/src/main/java/cz/cvut/spipes/modules/model/Cell.java new file mode 100644 index 00000000..8adfcd4f --- /dev/null +++ b/s-pipes-modules/module-tabular/src/main/java/cz/cvut/spipes/modules/model/Cell.java @@ -0,0 +1,70 @@ +package cz.cvut.spipes.modules.model; + +import cz.cvut.kbss.jopa.model.annotations.OWLAnnotationProperty; +import cz.cvut.kbss.jopa.model.annotations.OWLClass; +import cz.cvut.spipes.constants.CSVW; +import cz.cvut.spipes.constants.KBSS_CSVW; +import cz.cvut.spipes.modules.util.TabularModuleUtils; + +import java.net.URI; +import java.net.URISyntaxException; + +@OWLClass(iri = CSVW.CellUri) +public class Cell extends AbstractEntity{ + public Cell() {} + + public Cell(String cellUri) { + try { + this.setUri(new URI(cellUri)); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + + @OWLAnnotationProperty(iri = CSVW.nameUri) + private String name; + + @OWLAnnotationProperty(iri = CSVW.RowUri) + private String rowName; + + @OWLAnnotationProperty(iri = CSVW.ColumnUri) + private String columnName; + + @OWLAnnotationProperty(iri = KBSS_CSVW.sameValueAsUri) + private String sameValueAsCell; + + private final transient TabularModuleUtils tabularModuleUtils = new TabularModuleUtils(); + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getSameValueAsCell() { + return sameValueAsCell; + } + + public void setSameValueAsCell(String sameValueAsCell) { + this.sameValueAsCell = sameValueAsCell; + } + + public void setRowName(String rowName) { + tabularModuleUtils.setVariable(this.rowName, rowName, value -> this.rowName = value, "rowName"); + } + + public String getRowName() { + return rowName; + } + + public void setColumnName(String columnName) { + tabularModuleUtils.setVariable(this.columnName, columnName, value -> this.columnName = value, "columnName"); + } + + public String getColumnName() { + return columnName; + } + +} diff --git a/s-pipes-modules/module-tabular/src/main/java/cz/cvut/spipes/modules/model/Region.java b/s-pipes-modules/module-tabular/src/main/java/cz/cvut/spipes/modules/model/Region.java new file mode 100644 index 00000000..099b485d --- /dev/null +++ b/s-pipes-modules/module-tabular/src/main/java/cz/cvut/spipes/modules/model/Region.java @@ -0,0 +1,46 @@ +package cz.cvut.spipes.modules.model; + +public class Region { + private int firstRow; + private int lastRow; + private int firstColumn; + private int lastColumn; + public Region(int firstRow, int firstColumn, int lastRow, int lastColumn) { + this.firstRow = firstRow; + this.firstColumn = firstColumn; + this.lastRow = lastRow; + this.lastColumn = lastColumn; + } + + public int getFirstRow() { + return firstRow; + } + + public void setFirstRow(int firstRow) { + this.firstRow = firstRow; + } + + public int getLastRow() { + return lastRow; + } + + public void setLastRow(int lastRow) { + this.lastRow = lastRow; + } + + public int getFirstColumn() { + return firstColumn; + } + + public void setFirstColumn(int firstColumn) { + this.firstColumn = firstColumn; + } + + public int getLastColumn() { + return lastColumn; + } + + public void setLastColumn(int lastColumn) { + this.lastColumn = lastColumn; + } +} diff --git a/s-pipes-modules/module-tabular/src/main/java/cz/cvut/spipes/modules/util/XLS2TSVConvertor.java b/s-pipes-modules/module-tabular/src/main/java/cz/cvut/spipes/modules/util/XLS2TSVConvertor.java index 707b848a..8514bf9d 100644 --- a/s-pipes-modules/module-tabular/src/main/java/cz/cvut/spipes/modules/util/XLS2TSVConvertor.java +++ b/s-pipes-modules/module-tabular/src/main/java/cz/cvut/spipes/modules/util/XLS2TSVConvertor.java @@ -1,6 +1,7 @@ package cz.cvut.spipes.modules.util; import cz.cvut.spipes.modules.ResourceFormat; +import cz.cvut.spipes.modules.model.Region; import cz.cvut.spipes.registry.StreamResource; import cz.cvut.spipes.registry.StringStreamResource; import org.apache.poi.hssf.usermodel.HSSFWorkbook; @@ -8,15 +9,22 @@ import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.util.CellRangeAddress; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; /** * Module for converting tabular data from XLS to TSV. Converts specific sheet of the xls file. */ public class XLS2TSVConvertor { + private static final Logger LOG = LoggerFactory.getLogger(XLS2TSVConvertor.class); + public StringStreamResource convertToTSV(StreamResource streamResource,int sheetNumber){ try { Workbook workbook = new HSSFWorkbook(new ByteArrayInputStream(streamResource.getContent())); @@ -42,6 +50,28 @@ public StringStreamResource convertToTSV(StreamResource streamResource,int sheet } } + public List getMergedRegions(StreamResource streamResource, int sheetNumber){ + Workbook workbook; + List list = new ArrayList<>(); + try { + workbook = new HSSFWorkbook(new ByteArrayInputStream(streamResource.getContent())); + Sheet sheet = workbook.getSheetAt(sheetNumber-1); + + for(int i = 0;i < sheet.getNumMergedRegions();i++){ + CellRangeAddress region = sheet.getMergedRegion(i); + list.add(new Region( + region.getFirstRow(), + region.getFirstColumn(), + region.getLastRow(), + region.getLastColumn()) + ); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + return list; + } + public int getNumberOfSheets(StreamResource streamResource){ try { return new HSSFWorkbook(new ByteArrayInputStream(streamResource.getContent())).getNumberOfSheets(); diff --git a/s-pipes-modules/module-tabular/src/test/java/cz/cvut/spipes/modules/TabularModuleTest.java b/s-pipes-modules/module-tabular/src/test/java/cz/cvut/spipes/modules/TabularModuleTest.java index 8f28170a..c00ba048 100644 --- a/s-pipes-modules/module-tabular/src/test/java/cz/cvut/spipes/modules/TabularModuleTest.java +++ b/s-pipes-modules/module-tabular/src/test/java/cz/cvut/spipes/modules/TabularModuleTest.java @@ -83,7 +83,6 @@ void executeWithSimpleTransformationXls() throws URISyntaxException, IOException } @Test - @Disabled void executeWithSimpleTransformationMergedXls() throws URISyntaxException, IOException { module.setSourceResource( StreamResourceUtils.getStreamResource( @@ -95,7 +94,9 @@ void executeWithSimpleTransformationMergedXls() throws URISyntaxException, IOExc ExecutionContext outputContext = module.executeSelf(); - assertTrue(outputContext.getDefaultModel().size() > 0); + Model expectedModel = ModelFactory.createDefaultModel().read(getFilePath("merged-xls-model-output.ttl").toString()); + + assertIsomorphic(outputContext.getDefaultModel(),expectedModel); } @Test diff --git a/s-pipes-modules/module-tabular/src/test/resources/merged-xls-model-output.ttl b/s-pipes-modules/module-tabular/src/test/resources/merged-xls-model-output.ttl new file mode 100644 index 00000000..b872f274 --- /dev/null +++ b/s-pipes-modules/module-tabular/src/test/resources/merged-xls-model-output.ttl @@ -0,0 +1,137 @@ + + a ; + + _:b0 ; + + ; + + . + +_:b0 a ; + + "aa" ; + + "http://onto.fel.cvut.cz/data/aa" ; + + "aa" ; + + . + + + + "ff" ; + + "gg" . + + + a ; + + _:b0 ; + + . + +[ a ; + + [ a ; + + "Sheet1" ; + + [ a ; + + ; + + "1"^^ ; + + + ] ; + + [ a ; + + ; + + "3"^^ ; + + + ] ; + + [ a ; + + ; + + "2"^^ ; + + + ] ; + + [ a ; + + "http://test-file#row-{_row}"^^ ; + + _:b1 , _:b0 , _:b2 ; + + ( _:b0 + _:b2 + _:b1 + ) + ] ; + + + ] +] . + + + + "merged rows" ; + + "dd" ; + + "ee" . + + + a ; + + _:b1 ; + + ; + + . + + + + "merged columns" . + + + a ; + + _:b2 ; + + ; + + . + + + a ; + + _:b0 ; + + . + +_:b2 a ; + + "bb" ; + + "http://onto.fel.cvut.cz/data/bb" ; + + "bb" ; + + . + +_:b1 a ; + + "cc" ; + + "http://onto.fel.cvut.cz/data/cc" ; + + "cc" ; + + . \ No newline at end of file diff --git a/s-pipes-modules/module-tabular/src/test/resources/merged.xls b/s-pipes-modules/module-tabular/src/test/resources/merged.xls new file mode 100644 index 0000000000000000000000000000000000000000..d0b3d5d7f02b8c559042a2bf38e7fdc849edb1ab GIT binary patch literal 26624 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 literal 0 HcmV?d00001