diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..536565e --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +### Java template +*.class + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.ear + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# Created by .ignore support plugin (hsz.mobi) + +# Custom +.idea/ +target/ +*.iml diff --git a/README.md b/README.md index 042a677..a8c66eb 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,25 @@ own domain model. It also demonstrates a very simple add-on usage with Vaadin If you are new to Maven and want to try this application, check out this [tiny Maven tutorial](https://vaadin.com/blog/-/blogs/the-maven-essentials-for-the-impatient-developer). -To get this running execute "mvn wildfly:run" to launch this locally on Wildfly -server or deploy to a Java EE 7+ server like Wildfly or Glassfish. With Liberty you'll need to -present a data source (other modern servers provide "development datasource" when +## Run the example + +As this application is based on JEE it relies on a JEE capable application server. +The Maven project setup includes a Wildfly Maven application server for demonstration purposes. + +To run the project, you can use + +``` +mvn wildfly:run +``` +to launch this locally on Wildfly server. Afterwards, the application is available +in your local browser at + +[http://localhost:8080/jpa-addressbook-1.0-SNAPSHOT](http://localhost:8080/jpa-addressbook-1.0-SNAPSHOT) + + +Alternatively, you can deploy to a Java EE 7+ server like Wildfly or Glassfish. +With Liberty you'll need to present a data source +(other modern servers provide "development datasource" when no jta-datasource is present in persistence.xml). This is a suitable basis for small to medium sized apps. For larger applications, diff --git a/phonebook_entries.csv b/phonebook_entries.csv new file mode 100644 index 0000000..7a44d14 --- /dev/null +++ b/phonebook_entries.csv @@ -0,0 +1,3 @@ +name,number,email +Martin Senne,+49 234 823 9178,martin@vaadin.com +Marc Manager,+356 253 346 221,marc@coolcompany.com diff --git a/phonebook_incorrect.csv b/phonebook_incorrect.csv new file mode 100644 index 0000000..d5ea611 --- /dev/null +++ b/phonebook_incorrect.csv @@ -0,0 +1,4 @@ +a, b +1, 2 +3, 4 +5, 6 diff --git a/pom.xml b/pom.xml index 5fddf3f..ab64e69 100644 --- a/pom.xml +++ b/pom.xml @@ -16,6 +16,13 @@ + + + com.opencsv + opencsv + 3.3 + + org.apache.deltaspike.core deltaspike-core-api diff --git a/src/main/java/org/example/CSVImportView.java b/src/main/java/org/example/CSVImportView.java new file mode 100644 index 0000000..4d3e3fe --- /dev/null +++ b/src/main/java/org/example/CSVImportView.java @@ -0,0 +1,189 @@ +package org.example; + +import com.vaadin.cdi.CDIView; +import com.vaadin.cdi.UIScoped; +import com.vaadin.data.Container; +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.navigator.View; +import com.vaadin.navigator.ViewChangeListener; +import com.vaadin.ui.*; +import org.example.backend.PhoneBookEntry; +import org.example.backend.PhoneBookService; +import org.example.csv.CSVReadUtil; +import org.example.ui.upload.FileBasedUploadReceptor; +import org.vaadin.cdiviewmenu.ViewMenuItem; +import org.vaadin.viritin.label.Header; +import org.vaadin.viritin.layouts.MVerticalLayout; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.io.*; +import java.util.*; +import java.util.function.Consumer; + +@UIScoped +@CDIView("csv_imports") +@ViewMenuItem(order = 2) +public class CSVImportView extends MVerticalLayout implements View { + + @Inject + PhoneBookService service; + + Upload upload = new Upload(); + Grid grid; + Button cancelButton; + Button saveButton; + Header header; + + @PostConstruct + void init() { + + header = new Header("CSV Import"); + + /** + * External handler, after upload was successful. + */ + Consumer consumeReader = reader -> { + System.out.println(reader); + try { + IndexedContainer csvContainer = CSVReadUtil.buildContainerFromCSV(new FileReader(reader.getFile())); + // this is intentional, as attaching new datasource to existing grid seems cause problems in this Vaadin version + grid = new Grid(); + grid.setWidth(100, Unit.PERCENTAGE); + grid.setContainerDataSource(csvContainer); + boolean columnNamesValid = checkEntries(grid.getContainerDataSource()); + if ( !columnNamesValid ) { + Notification.show("Save is not possible. Schema ( column names ) of CSV not correct.", Notification.Type.WARNING_MESSAGE); + } + displayUploadedData( columnNamesValid ); + + } catch (IOException e) { + e.printStackTrace(); + } + }; + + FileBasedUploadReceptor fileBasedUploadReceptor = new FileBasedUploadReceptor(consumeReader); + upload.setReceiver(fileBasedUploadReceptor); + upload.addSucceededListener(fileBasedUploadReceptor); + upload.setImmediate(true); + upload.setButtonCaption("Upload CSV."); + + saveButton = new Button("Save"); + saveButton.addClickListener(clickEvent -> { + Container.Indexed container = grid.getContainerDataSource(); + List entries = createEntries(container); + saveEntries( entries ); + Notification.show( entries.size() + " entries have been imported."); + displayUpload(); + }); + + cancelButton = new Button("Cancel"); + cancelButton.addClickListener(clickEvent -> { + displayUpload(); + } + ); + + displayUpload(); + } + + // ========================================================== + // ===== View related methods + // ========================================================== + public void displayUpload() { + removeAllComponents(); + + Label label = new Label("Please upload your CSV, that contains columns 'name', 'number' and 'email'"); + + addComponent(header); + addComponent(label); + addComponent(upload); + } + + public void displayUploadedData( boolean activateSave ) { + removeAllComponents(); + + saveButton.setEnabled( activateSave ); + + HorizontalLayout hl = new HorizontalLayout(); + hl.addComponent(cancelButton); + hl.addComponent(saveButton); + + HorizontalLayout bottom = new HorizontalLayout(); + bottom.setWidth(100, Unit.PERCENTAGE); + bottom.addComponent(hl); + bottom.setComponentAlignment(hl, Alignment.MIDDLE_RIGHT); + + addComponent(header); + addComponent(grid); + setExpandRatio(grid, 1); + addComponent(bottom); + } + + @Override + public void enter(ViewChangeListener.ViewChangeEvent event) { + // nothing to do on enter + } + + // ========================================================== + // ===== Entry related methods + // ========================================================== + // TODO: To be moved to separate class, but left here to keep everything "close together". + + private String NAME = "name"; + private String NUMBER = "number"; + private String EMAIL = "email"; + + /** + * Check if all property ids NAME, NUMBER and EMAIL are present in given container (as read from CSV). + * @param containerDataSource + * @return true if so + */ + private boolean checkEntries(Container.Indexed containerDataSource) { + Collection pids = (Collection)containerDataSource.getContainerPropertyIds(); + + Set actualSchema = new HashSet<>(pids); + Set expectedSchema = new HashSet<>(Arrays.asList(NAME, NUMBER, EMAIL)); + + Set intersect = (new HashSet<>(actualSchema)); + intersect.retainAll(expectedSchema); + + if (actualSchema.size() == 3) { + return true; // everything is fine + } else { + return false; + } + } + + /** + * Create entries based on container content. + * + * @param container use for construction of {@link PhoneBookEntry}s. + * @return list of entries + */ + private List createEntries(Container.Indexed container ) { + + List entries = new ArrayList<>(); + int n = container.size(); + + // TODO: Introduce real mapping functionality + for ( Object itemId : container.getItemIds() ) { + String name = (String) container.getContainerProperty(itemId, NAME).getValue(); + String number = (String) container.getContainerProperty(itemId, NUMBER).getValue(); + String email = (String) container.getContainerProperty(itemId, EMAIL).getValue(); + PhoneBookEntry entry = new PhoneBookEntry(name, number, email); + entries.add(entry); + } + return entries; + } + + /** + * Persist given {@link PhoneBookEntry}s. + * @param entries are the entries to persist. + */ + private int saveEntries( List entries ) { + for (PhoneBookEntry entry : entries) { + service.save(entry); + } + return entries.size(); + } +} diff --git a/src/main/java/org/example/GroupsView.java b/src/main/java/org/example/GroupsView.java index b834ab6..f800152 100644 --- a/src/main/java/org/example/GroupsView.java +++ b/src/main/java/org/example/GroupsView.java @@ -13,6 +13,7 @@ import javax.inject.Inject; import org.example.backend.PhoneBookGroup; import org.example.backend.PhoneBookService; +import org.vaadin.cdiviewmenu.ViewMenuItem; import org.vaadin.viritin.button.MButton; import org.vaadin.viritin.fields.MTable; import org.vaadin.viritin.fields.MTextField; @@ -23,6 +24,7 @@ @UIScoped @CDIView("groups") +@ViewMenuItem(order = 1) public class GroupsView extends CssLayout implements View { @Inject diff --git a/src/main/java/org/example/csv/CSVReadUtil.java b/src/main/java/org/example/csv/CSVReadUtil.java new file mode 100644 index 0000000..7abaa65 --- /dev/null +++ b/src/main/java/org/example/csv/CSVReadUtil.java @@ -0,0 +1,160 @@ +package org.example.csv; + +import com.opencsv.CSVReader; +import com.vaadin.data.Item; +import com.vaadin.data.util.IndexedContainer; + +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + + +public class CSVReadUtil { + + /** + * Uses http://opencsv.sourceforge.net/ to read the entire contents of a CSV + * file, and creates an IndexedContainer from it + * + * @param reader + * @return + * @throws IOException + */ + public static IndexedContainer buildContainerFromCSV(Reader reader) throws IOException { + IndexedContainer container = new IndexedContainer(); + CSVReader csvReader = new CSVReader(reader); + String[] columnHeaders = null; + String[] record; + while ((record = csvReader.readNext()) != null) { + // Headerline + if (columnHeaders == null) { + columnHeaders = record; + addItemProperties(container, columnHeaders); + } else { + addItem(container, columnHeaders, record); + } + } + return container; + } + + public static class CSV { + private int columns; + private int rows; + + List header; + List> content; + + public CSV(List content ) { + this( generateHeader(content), content); + } + + public CSV(String[] header, List content ) { + this.header = Arrays.asList(header); + + this.content = new ArrayList<>(content.size()); + content.forEach( array -> this.content.add( Arrays.asList(array))); + } + + private static String[] generateHeader(List content) { + Optional optCols = checkEqualNumberOfColumns(content); + String[] header; + if (optCols.isPresent()) { + int n = optCols.get(); + header = new String[n]; + for (int i = 0; i < n; i++) { + header[i] = "Column " + Integer.toString(i); + } + } else { + throw new IllegalArgumentException(("Column size is not identical for each row.")); + } + return header; + } + + private static Optional checkEqualNumberOfColumns(List data) { + if (data.size() == 0) { + return Optional.empty(); + } + + int expected = data.get(0).length; + return data.stream().filter( p -> p.length != expected ).count() > 0 + ? Optional.of(expected) : Optional.empty(); + } + + public List getColumnNames() { + return header; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + + sb.append( fixedLengthFormat( header ) ); + content.forEach(row -> sb.append( fixedLengthFormat( row ) )); + + return sb.toString(); + + } + + public static String fixedLengthFormat(List ss) { + return ss.stream() + .map(s -> turnIntoFixedSize(s, 20)) + .collect(Collectors.joining("|", "", "")); + } + + public static String turnIntoFixedSize(String input, int s) { + int n = input.length(); + + return (n <= s) ? input + createNSpaces( n-s ) : input.substring(0, s - 1) + "…"; + } + + public static String createNSpaces( int n ) { + + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < n; i++) { + sb.append(" "); + } + return sb.toString(); + } + + } + + + /** + * Set's up the item property ids for the container. Each is a String (of course, + * you can create whatever data type you like, but I guess you need to parse the whole file + * to work it out) + * + * @param container The container to set + * @param columnHeaders The column headers, i.e. the first row from the CSV file + */ + private static void addItemProperties(IndexedContainer container, String[] columnHeaders) { + for (String propertyName : columnHeaders) { + container.addContainerProperty(propertyName, String.class, null); + } + } + + /** + * Adds an item to the given container, assuming each field maps to it's corresponding property id. + * Again, note that I am assuming that the field is a string. + * + * @param container + * @param propertyIds + * @param fields + */ + private static void addItem(IndexedContainer container, String[] propertyIds, String[] fields) { + if (propertyIds.length != fields.length) { + throw new IllegalArgumentException("Hmmm - Different number of columns to fields in the record"); + } + Object itemId = container.addItem(); + Item item = container.getItem(itemId); + for (int i = 0; i < fields.length; i++) { + String propertyId = propertyIds[i]; + String field = fields[i]; + item.getItemProperty(propertyId).setValue(field); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/example/ui/upload/FileBasedUploadReceptor.java b/src/main/java/org/example/ui/upload/FileBasedUploadReceptor.java new file mode 100644 index 0000000..f7dc680 --- /dev/null +++ b/src/main/java/org/example/ui/upload/FileBasedUploadReceptor.java @@ -0,0 +1,72 @@ +package org.example.ui.upload; + +import com.vaadin.ui.Upload; + +import java.io.*; +import java.util.function.Consumer; + +/** + * Purpose of this class: A helper class to allow storage of upload (via com.vaadin.ui.Upload) in a file. + */ +public class FileBasedUploadReceptor implements Upload.Receiver, Upload.SucceededListener { + + private File tempFile; + + private Consumer fileConsumer; + + public static class FileAndInfo { + private File file; + private String filename; + private String mimetype; + + public FileAndInfo(File file, String filename, String mimetype) { + this.file = file; + this.filename = filename; + this.mimetype = mimetype; + } + + public File getFile() { + return file; + } + + public String getFilename() { + return filename; + } + + public String getMimetype() { + return mimetype; + } + } + + /** + * Constructor. + * @param fileConsumer is a (consuming) function of type ( Reader -> () ) that is called, when upload was successful. + */ + public FileBasedUploadReceptor(Consumer fileConsumer) { + this.fileConsumer = fileConsumer; + + try { + tempFile = File.createTempFile("temp", ".csv"); + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException("Can't create temporary file."); + } + } + + @Override + public OutputStream receiveUpload(String s, String s1) { + try { + return new FileOutputStream(tempFile); + } catch (FileNotFoundException e) { + e.printStackTrace(); + throw new RuntimeException("Temporary file not found."); + } + } + + @Override + public void uploadSucceeded(Upload.SucceededEvent event) { + System.out.println("Got file '" + event.getFilename() + + "' and mimetype '" + event.getMIMEType() + "'."); + fileConsumer.accept( new FileAndInfo(tempFile, event.getFilename(), event.getMIMEType()) ); + } +} \ No newline at end of file diff --git a/src/main/resources/about.md b/src/main/resources/about.md index 20afa71..0e75c3e 100644 --- a/src/main/resources/about.md +++ b/src/main/resources/about.md @@ -10,3 +10,22 @@ own domain model. It also demonstrates a very simple add-on usage with Vaadin The source code for this example is available [from github](https://github.com/mstahv/jpa-addressbook). + +## Views +Currently, four different views are avaiable. + +### Main + +Main view for *viewing* phone entries and *adding* new entries. + +### Groups + +Management of group to whom phone entries can be assigned. + +### CSVImport +View that allows for importing phone entries from a CSV file +An example CSV file is given at [/phonebook_example_entries.csv](/phonebook_example_entries.csv) + +### About +This about dialogue. +