wordstudent

Word learning program
git clone git://git.wimdupont.com/wordstudent.git
Log | Files | Refs | LICENSE

commit b3751367d1c3c1567768d720f20d2a5d91637960
parent 1a2fe81a958a5aec6451f83df071dbaed8e91941
Author: Wim Dupont <wim@wimdupont.com>
Date:   Sat, 26 Aug 2023 14:39:26 +0200

updated

Diffstat:
AREADME.adoc | 11+++++++++++
DREADME.md | 8--------
Msrc/main/java/com/wimdupont/WordStudentAdvancedApplication.java | 24++++++++----------------
Msrc/main/java/com/wimdupont/client/Client.java | 7++++++-
Asrc/main/java/com/wimdupont/client/WordRepository.java | 43+++++++++++++++++++++++++++++++++++++++++++
Dsrc/main/java/com/wimdupont/model/dto/DefinitionDto.java | 14--------------
Msrc/main/java/com/wimdupont/model/dto/DictionaryDto.java | 5+----
Dsrc/main/java/com/wimdupont/model/dto/MeaningDto.java | 13-------------
Dsrc/main/java/com/wimdupont/model/dto/PhoneticDto.java | 11-----------
Dsrc/main/java/com/wimdupont/service/WordService.java | 44--------------------------------------------
Msrc/main/java/com/wimdupont/service/WordTester.java | 210++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
11 files changed, 214 insertions(+), 176 deletions(-)

diff --git a/README.adoc b/README.adoc @@ -0,0 +1,11 @@ += WordStudentAdvanced + +Utilizes public https://couchdb.apache.org/[CouchDB] database. + +== Setup + +. Create a csv file with words to learn; separated by new lines and/or semicolons +. Create application.properties file under src/main/resources with following properties: +.. 'csv.dir': path of the csv file +.. 'couchdb.connection-url': couchDb url (example: http://localhost:5984/wordtester) +.. 'dictionary.client.connection-url': api url (https://api.dictionaryapi.dev/api/v2/entries/en/) diff --git a/README.md b/README.md @@ -1,8 +0,0 @@ -# WordStudentAdvanced - -## Setup - -1. Create application.properties file under src/main/resources -2. Create a csv file -3. Add the path of the csv file to application.properties with property name 'csv.dir' -4. Add words to the csv file separated by semicolon (;) diff --git a/src/main/java/com/wimdupont/WordStudentAdvancedApplication.java b/src/main/java/com/wimdupont/WordStudentAdvancedApplication.java @@ -1,29 +1,21 @@ package com.wimdupont; import com.wimdupont.client.DictionaryApi; +import com.wimdupont.client.WordRepository; import com.wimdupont.service.WordFetcher; -import com.wimdupont.service.WordService; import com.wimdupont.service.WordTester; -public class WordStudentAdvancedApplication { +import java.util.List; - private static final WordService WORD_SERVICE = new WordService(); - private static final WordFetcher WORD_FETCHER = new WordFetcher(); - private static final DictionaryApi DICTIONARY_API = new DictionaryApi(); +public class WordStudentAdvancedApplication { public static void main(String[] args) { + List<String> words = new WordFetcher().fetch(); + System.out.printf("Words in csv file: %s%n", words.size()); - new WordTester(WORD_FETCHER, WORD_SERVICE).process(); -// var words = WORD_FETCHER.fetch(); -// -// words.forEach(word -> { -// var result = DICTIONARY_API.getDictionary(word); -// result.ifPresent(WORD_SERVICE::save); -// }); -// var result = new WordService().findByWord("acumen"); -// -// System.out.println(result.get()); -// System.exit(0); + var wordTester = new WordTester(new WordRepository(), new DictionaryApi()); + wordTester.saveNew(words); + wordTester.startTest(words); } } diff --git a/src/main/java/com/wimdupont/client/Client.java b/src/main/java/com/wimdupont/client/Client.java @@ -8,6 +8,7 @@ import java.io.OutputStream; import java.io.OutputStreamWriter; import java.net.HttpURLConnection; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.Optional; @@ -16,6 +17,10 @@ public class Client { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private static final Map<String, String> POST_HEADER_MAP = Map.of("Content-type", "application/json"); + public static ObjectMapper getObjectMapper(){ + return OBJECT_MAPPER; + } + public static <T> Optional<T> get(String url, Class<T> responseClazz) { try { HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); @@ -38,7 +43,7 @@ public class Client { POST_HEADER_MAP.forEach(connection::setRequestProperty); connection.setDoOutput(true); OutputStream outStream = connection.getOutputStream(); - OutputStreamWriter outStreamWriter = new OutputStreamWriter(outStream, "UTF-8"); + OutputStreamWriter outStreamWriter = new OutputStreamWriter(outStream, StandardCharsets.UTF_8); outStreamWriter.write(body); outStreamWriter.flush(); outStreamWriter.close(); diff --git a/src/main/java/com/wimdupont/client/WordRepository.java b/src/main/java/com/wimdupont/client/WordRepository.java @@ -0,0 +1,43 @@ +package com.wimdupont.client; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.wimdupont.config.ApplicationProperties; +import com.wimdupont.model.db.WordCouchDocument; +import com.wimdupont.model.db.WordSelectorResponse; +import com.wimdupont.model.dto.WordDto; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + + +public class WordRepository { + + private final ApplicationProperties applicationProperties = ApplicationProperties.getInstance(); + + public List<String> findAllWords() { + return Client.get(applicationProperties.getCouchdbUrl() + "/_all_docs?include_docs=true", WordCouchDocument.class) + .map(wordCouchDocument -> wordCouchDocument.rows().stream().map(row -> row.doc().word()).toList()) + .orElseGet(ArrayList::new); + } + + public Optional<WordDto> findByWord(String word) { + var url = applicationProperties.getCouchdbUrl() + "/_find"; + var requestString = String.format("{ \"selector\": { \"word\": { \"$eq\": \"%s\" } } }", word); + return Client.post(url, WordSelectorResponse.class, requestString).flatMap(f -> f.docs().stream().findAny()); + } + + public void save(WordDto wordDto) { + if (findByWord(wordDto.word()).isEmpty()) { + var url = applicationProperties.getCouchdbUrl(); + try { + Client.post(url, WordSelectorResponse.class, new ObjectMapper().writeValueAsString(wordDto)); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } else { + System.out.printf("Word %s already saved", wordDto.word()); + } + } +} diff --git a/src/main/java/com/wimdupont/model/dto/DefinitionDto.java b/src/main/java/com/wimdupont/model/dto/DefinitionDto.java @@ -1,14 +0,0 @@ -package com.wimdupont.model.dto; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -import java.util.List; - -@JsonIgnoreProperties(ignoreUnknown = true) -public record DefinitionDto( - String definition, - String example, - List<String> synonyms, - List<String> antonyms -) { -} diff --git a/src/main/java/com/wimdupont/model/dto/DictionaryDto.java b/src/main/java/com/wimdupont/model/dto/DictionaryDto.java @@ -8,9 +8,6 @@ import java.util.List; @JsonIgnoreProperties(ignoreUnknown = true) public record DictionaryDto( String word, - String phonetic, - List<PhoneticDto> phonetics, - String origin, - List<MeaningDto> meanings + List<Object> meanings ) { } diff --git a/src/main/java/com/wimdupont/model/dto/MeaningDto.java b/src/main/java/com/wimdupont/model/dto/MeaningDto.java @@ -1,13 +0,0 @@ -package com.wimdupont.model.dto; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -import java.util.List; - -@JsonIgnoreProperties(ignoreUnknown = true) -public record MeaningDto( - String partOfSpeech, - List<DefinitionDto> definitions -) { - -} diff --git a/src/main/java/com/wimdupont/model/dto/PhoneticDto.java b/src/main/java/com/wimdupont/model/dto/PhoneticDto.java @@ -1,11 +0,0 @@ -package com.wimdupont.model.dto; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -@JsonIgnoreProperties(ignoreUnknown = true) -public record PhoneticDto( - String text, - String audio -) { -} - diff --git a/src/main/java/com/wimdupont/service/WordService.java b/src/main/java/com/wimdupont/service/WordService.java @@ -1,44 +0,0 @@ -package com.wimdupont.service; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.wimdupont.client.Client; -import com.wimdupont.config.ApplicationProperties; -import com.wimdupont.model.db.WordCouchDocument; -import com.wimdupont.model.db.WordSelectorResponse; -import com.wimdupont.model.dto.WordDto; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - - -public class WordService { - - private final ApplicationProperties applicationProperties = ApplicationProperties.getInstance(); - - public List<String> findAllWords() { - return Client.get(applicationProperties.getCouchdbUrl() + "/_all_docs?include_docs=true", WordCouchDocument.class) - .map(wordCouchDocument -> wordCouchDocument.rows().stream().map(row -> row.doc().word()).toList()) - .orElseGet(ArrayList::new); - } - - public Optional<WordDto> findByWord(String word) { - var url = applicationProperties.getCouchdbUrl() + "/_find"; - var requestString = String.format("{ \"selector\": { \"word\": { \"$eq\": \"%s\" } } }", word); - return Client.post(url, WordSelectorResponse.class, requestString).flatMap(f -> f.docs().stream().findAny()); - } - - public void save(WordDto wordDto) { - if (findByWord(wordDto.word()).isEmpty()) { - var url = applicationProperties.getCouchdbUrl(); - try { - Client.post(url, WordSelectorResponse.class, new ObjectMapper().writeValueAsString(wordDto)); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } else { - System.out.printf("Word %s already saved", wordDto.word()); - } - } -} diff --git a/src/main/java/com/wimdupont/service/WordTester.java b/src/main/java/com/wimdupont/service/WordTester.java @@ -1,110 +1,190 @@ package com.wimdupont.service; -import com.wimdupont.model.dto.DictionaryDto; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.wimdupont.client.Client; +import com.wimdupont.client.DictionaryApi; +import com.wimdupont.client.WordRepository; import com.wimdupont.model.dto.WordDto; +import javax.swing.AbstractAction; +import javax.swing.ActionMap; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.InputMap; import javax.swing.JButton; +import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; +import javax.swing.JScrollPane; import javax.swing.JTextArea; +import javax.swing.KeyStroke; import java.awt.BorderLayout; -import java.awt.GridBagLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.event.ActionEvent; import java.util.Collections; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; public class WordTester { - /* - - navigate with hjkl, arrows, spacebar,... - - close frame - - add database options: remove word, fully clean database,... - - - - - */ - - - private final WordFetcher wordFetcher; - private final WordService wordService; - - public WordTester(WordFetcher wordFetcher, - WordService wordService) { - this.wordFetcher = wordFetcher; - this.wordService = wordService; - } - public void process() { - List<String> words = wordFetcher.fetch(); - System.out.printf("Words in csv file: %s%n", words.size()); + private final WordRepository wordRepository; + private final DictionaryApi dictionaryApi; + private final InputMap inputMap; + private static final String PREVIOUS = "Previous"; + private static final String SHOW = "Show"; + private static final String NEXT = "Next"; + + public WordTester(WordRepository wordRepository, + DictionaryApi dictionaryApi) { + this.wordRepository = wordRepository; + this.dictionaryApi = dictionaryApi; + this.inputMap = initiateInputMap(); + } + public void startTest(List<String> words) { Collections.shuffle(words); - panelItUp(words); - System.out.println("========================"); - System.out.println("All words have been tested."); + showUI(words); } - private JTextArea createShowPanel() { - var dictionaryPanel = new JTextArea(); - dictionaryPanel.setLineWrap(true); - dictionaryPanel.setEditable(false); - return dictionaryPanel; + public void saveNew(List<String> words) { + List<String> savedWordList = wordRepository.findAllWords(); + System.out.printf("Words in database: %s%n", savedWordList.size()); + AtomicInteger count = new AtomicInteger(0); + words.stream() + .filter(csvWord -> !savedWordList.contains(csvWord)) + .forEach(newWord -> { + var response = dictionaryApi.getDictionary(newWord); + if (response.isEmpty()) { + System.out.printf("No results found for '%s', no data saved%n", newWord); + count.get(); + } else { + wordRepository.save(response.get()); + System.out.printf("Saved new word %s%n", response.get()); + count.getAndIncrement(); + } + }); + + if (count.get() > 0) + System.out.printf("Saved %s words%n", count.get()); + } + + private InputMap initiateInputMap() { + var inputMap = new InputMap(); + inputMap.put(KeyStroke.getKeyStroke("LEFT"), PREVIOUS); + inputMap.put(KeyStroke.getKeyStroke("H"), PREVIOUS); + inputMap.put(KeyStroke.getKeyStroke("L"), NEXT); + inputMap.put(KeyStroke.getKeyStroke("RIGHT"), NEXT); + inputMap.put(KeyStroke.getKeyStroke("DOWN"), SHOW); + inputMap.put(KeyStroke.getKeyStroke("SPACE"), SHOW); + inputMap.put(KeyStroke.getKeyStroke("J"), SHOW); + return inputMap; } - private void panelItUp(List<String> words) { + private void showUI(List<String> words) { AtomicInteger index = new AtomicInteger(0); JFrame jFrame = new JFrame(); jFrame.setLayout(new BorderLayout()); - final JPanel jPanel = new JPanel(); - jPanel.add(new JLabel("A Panel")); - jFrame.add(jPanel, BorderLayout.CENTER); - var wordPanel = new JPanel(new GridBagLayout()); + var wordPanel = new JPanel(); + wordPanel.setFocusable(false); + wordPanel.setLayout(new BoxLayout(wordPanel, BoxLayout.Y_AXIS)); var wordField = new JLabel(words.get(index.get())); + wordPanel.add(Box.createRigidArea(new Dimension(20, 20))); wordPanel.add(wordField); - var showPanel = createShowPanel(); + wordField.setFont(new Font(Font.SERIF, Font.BOLD, 30)); + wordPanel.add(Box.createRigidArea(new Dimension(20, 20))); + var showPanel = createShowPanel(index, wordField, words); + wordPanel.add(new JScrollPane(showPanel)); + jFrame.add(wordPanel, BorderLayout.CENTER); + jFrame.add(createButtonPanel(showPanel), BorderLayout.SOUTH); + jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + jFrame.pack(); + jFrame.setVisible(true); + } + + private JTextArea createShowPanel(AtomicInteger index, JLabel wordField, List<String> words) { + var dictionaryPanel = new JTextArea(); + dictionaryPanel.setLineWrap(true); + dictionaryPanel.setEditable(false); + dictionaryPanel.setInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT, inputMap); + dictionaryPanel.setBackground(new Color(0, 0, 51)); + dictionaryPanel.setForeground(new Color(150, 250, 250)); + dictionaryPanel.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 15)); + var actionMap = new ActionMap(); + actionMap.put(PREVIOUS, toActionListener(previous(index, wordField, words, dictionaryPanel))); + actionMap.put(SHOW, toActionListener(show(index, words, dictionaryPanel))); + actionMap.put(NEXT, toActionListener(next(index, wordField, words, dictionaryPanel))); + dictionaryPanel.setActionMap(actionMap); + return dictionaryPanel; + } + + private JPanel createButtonPanel(JTextArea showPanel) { + JButton previousButton = new JButton(PREVIOUS); + previousButton.addActionListener(showPanel.getActionMap().get(PREVIOUS)); + previousButton.setFocusable(false); + JButton showButton = new JButton(SHOW); + showButton.setFocusable(false); + showButton.addActionListener(showPanel.getActionMap().get(SHOW)); + JButton nextButton = new JButton(NEXT); + nextButton.setFocusable(false); + nextButton.addActionListener(showPanel.getActionMap().get(NEXT)); + + var btnPanel = new JPanel(); + btnPanel.add(previousButton); + btnPanel.add(showButton); + btnPanel.add(nextButton); - wordPanel.add(showPanel); + return btnPanel; + } - JButton previousButton = new JButton("Previous"); - previousButton.addActionListener(e -> { + private Runnable previous(AtomicInteger index, JLabel wordField, List<String> words, JTextArea showPanel) { + return () -> { if (index.get() > 0) { wordField.setText(words.get(index.decrementAndGet())); + showPanel.setText(null); } - }); - JButton showButton = new JButton("Show"); - showButton.addActionListener(e -> { - var word = wordService.findByWord(words.get(index.get())); - word.ifPresent(wordDto -> showPanel.setText(toMeaning(wordDto))); - //TODO else grab and save + }; + } - }); + private Runnable show(AtomicInteger index, List<String> words, JTextArea showPanel) { + return () -> { + var word = wordRepository.findByWord(words.get(index.get())); + word.ifPresent(wordDto -> showPanel.setText(toMeaning(wordDto))); + showPanel.setVisible(true); + }; + } - JButton nextButton = new JButton("Next"); - nextButton.addActionListener(e -> { + private Runnable next(AtomicInteger index, JLabel wordField, List<String> words, JTextArea showPanel) { + return () -> { if (index.get() < words.size()) { wordField.setText(words.get(index.incrementAndGet())); + showPanel.setText(null); } - }); - var btnPanel = new JPanel(); - btnPanel.add(previousButton); - btnPanel.add(showButton); - btnPanel.add(nextButton); + }; + } + + private AbstractAction toActionListener(Runnable runnable) { + return new AbstractAction() { + + @Override + public void actionPerformed(ActionEvent actionEvent) { + runnable.run(); + } + }; - jFrame.add(wordPanel, BorderLayout.CENTER); - jFrame.add(btnPanel, BorderLayout.SOUTH); - jFrame.pack(); - jFrame.setVisible(true); } private String toMeaning(WordDto wordDto) { - return """ - Word: %s - Meaning: %s - """ - .formatted(wordDto.word() + System.lineSeparator(), - wordDto.dictionaryResults().stream() - .map(DictionaryDto::meanings) - .toList()); + try { + return Client.getObjectMapper() + .writerWithDefaultPrettyPrinter() + .writeValueAsString(wordDto.dictionaryResults()); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } } }