diff --git a/CHANGELOG.md b/CHANGELOG.md index 51958628e8c..9ee47d452e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Note that this project **does not** adhere to [Semantic Versioning](http://semve - We added confirmation dialog when user wants to close a library where any empty entires are detected. [#8096](https://github.com/JabRef/jabref/issues/8096) - We added import support for CFF files. [#7945](https://github.com/JabRef/jabref/issues/7945) +- We added support to use Lucence search syntax also for the searchin the library or in dynamic groups. - We added the option to copy the DOI of an entry directly from the context menu copy submenu. [#7826](https://github.com/JabRef/jabref/issues/7826) - We added a fulltext search feature. [#2838](https://github.com/JabRef/jabref/pull/2838) - We improved the deduction of bib-entries from imported fulltext pdfs. [#7947](https://github.com/JabRef/jabref/pull/7947) diff --git a/docs/sweng/GanttProjectplan.gan b/docs/sweng/GanttProjectplan.gan new file mode 100644 index 00000000000..ae77e47b357 --- /dev/null +++ b/docs/sweng/GanttProjectplan.gan @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 8 Constraints +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/sweng/diagrams.pu b/docs/sweng/diagrams.pu new file mode 100644 index 00000000000..f27df339284 --- /dev/null +++ b/docs/sweng/diagrams.pu @@ -0,0 +1,273 @@ +' +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +@startuml +mainframe **Class Diagram: "Better Search" Implementation ** +' package LEGEND { +' class Existing as " Existing Classes " #White +' hide Existing circle +' hide Existing methods +' hide Existing members + +' class New as " New Classes " +' hide New circle +' hide New methods +' hide New members + +' ' Workaround to prevent PlantUML from positioning the legend blocks randomly. +' Existing -[hidden]- New +' } + +' ======================================================= +' Grouping +' ======================================================= +together { + class DropDownMenu + class SearchFieldSynchronizer +} + +' ======================================================= +' Relations +' ======================================================= +CustomTextField - GlobalSearchbar +SearchItem *- itemType +RecentSearch <- SearchFieldSynchronizer +GlobalSearchbar -- Highlighting +GlobalSearchbar <-- Autocomplete +DropDownMenu <-- RecentSearch + +SearchFieldSynchronizer -- LuceneBasedSearchRule +SearchItem "0...*" -> "1 " SearchFieldSynchronizer +JFXChipView -- SearchFieldSynchronizer +DropDownMenu --> SearchFieldSynchronizer +CustomTextField <-- TextInputControl +Autocomplete <-- TextInputControl +TextInputControl <-- SearchFieldSynchronizer +RecentSearch "1 " <-- "0...*" SearchItem + +' ======================================================= +' Classes +' ======================================================= +class GlobalSearchbar #SkyBlue { + searchField: CustomTextField + updateSearchResultsForQuery() + ' initSearchModifierButtons() + ' initSearchModifierButton() + ' focus() + performSearch() + ' validRegex() + ' informUserAboutInvalidSearchQuery() + updateResults() + ' setSearchFieldHintTooltip() + ' updateHintVisibility() + ' registerEventListener() + ' onSuggestionChosen() +} + +class Highlighting #TECHNOLOGY { + +} + +class RecentSearch #PeachPuff { + pastSearchItemList: ArrayList + ListView authors + ListView titles + show() + hide() + add() + checkSearchItemExists() + +} + +class DropDownMenu #PeachPuff { + authorBtn: Button + journalBtn: Button + titleBtn: Button + yearBtn: Button + andBtn: Button + orBtn: Button + andMinusBtn: Button + orMinusBtn: Button + yearRange: RangeSlider + stringadder: Textfield + isPrevAttribute: boolean + checkAndAddSpace() + isPrevAttribute() +} + +class LuceneBasedSearchRule #TECHNOLOGY { + validateSearchStrings() +} + +class Autocomplete #TECHNOLOGY { + AutoCompletionTextFieldBinding String + AutoCompletePopup autoCompletionPopup + bindAutoCompletion() +} + +class SearchFieldSynchronizer #PeachPuff { + searchItemList: ArrayList + searchString: String + addSearchitem() + isValid() + returnLatest() + synchronize() + searchStringBuilder() + updateSearchItemList() + searchItemListToString() + textFieldToList() + syntaxHighlighting() + bracketsBalanced() + isValisLucene() + deleteAllEntries() +} + +class SearchItem #PeachPuff { + itemType: enum + item: String + getType() + get() + setType() + set() +} + +enum itemType #PeachPuff { + logicalOperator + attribute + bracket + query +} + +class CustomTextField #TECHNOLOGY { + get() + get(int start, int end) + length() + addListener() +} + +class TextInputControl #TECHNOLOGY { + getText() + insertText() + deleteText() + replaceText() + positionCaret() + getCarretPosition() + getLength() +} + +class JFXChipView #TECHNOLOGY { + chips + StringConverter() +} +@enduml + +' +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +' Julian: Recent Search +@startuml recent-search +mainframe **Sequence Diagram: Recent Search ** +actor User +User -> GlobalSearchBar: performSearch() +GlobalSearchBar -> RecentSearch: addQueryToRecentSearches +User -> RecentSearch: clickOnRecentSearchEntry +RecentSearch -> GlobalSearchBar: performSearch() +@enduml + +' +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +' Mark: User Search Input +@startuml user-search-input +mainframe **Sequence Diagram: User Search Input ** +actor User +User -> DropDownMenu: Button Input +box **SearchFieldSynchronizer** +DropDownMenu -> SearchFieldSynchronizer: ActionEvent +entity searchItemList +SearchFieldSynchronizer -> searchItemList: addSearchItemToList() +searchItemList --> SearchFieldSynchronizer: SearchStringBuilder() +end box +box **CustomTextField** #White +entity searchField +SearchFieldSynchronizer -> searchField: replaceText() +end box + +User -> searchField: Text Input +searchField --> SearchFieldSynchronizer: EventListener +SearchFieldSynchronizer -> searchItemList: updateList() +searchItemList --> SearchFieldSynchronizer: SearchStringBuilder() +SearchFieldSynchronizer -> searchField: replaceText() +@enduml + +' +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +' Albert: Autocomplete +@startuml +mainframe **Sequence Diagram: Autocomplete ** +actor User +User -> AutoCompletePopup: bindAutoCompletion() +AutoCompletePopup -> AutoCompletionTextInputBinding: convertSuggestionsToStrings +AutoCompletionTextInputBinding -> GlobalSearchBar: provideSuggestionsInGlobalSearch +GlobalSearchBar -> AutoCompletePopup: bindAutoCompletion() +@enduml + +' +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +' Dani: ChipView +@startuml +mainframe Sequence Diagram: ChipView +actor User +User->GlobalSearchBar: performSearch() +GlobalSearchBar->ChipView: addChipViewToSearchField() +User->ChipView: clickOnChipInSearchField +ChipView->GlobalSearchBar:deleteChosenSearchkey() +@enduml + +' +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +' Mark: Logical Operators +@startuml logic-operators +mainframe **Activity Diagram: Logic Operators ** +start +switch ( Button Press ) +case ( ) + :Attribute; + if (leftSideExists?) then + if (isLeftAttribute?) then + if (isLeftAttributeWithoutSearchString?) then + #pink:do nothing; + kill + endif + #palegreen:Add OR; + endif + endif + #palegreen:Add attribute; + stop +case ( ) + switch ( Logic Operator ) + case ( ) + :1; + if (leftSideExists?) then + if (isLeftAttribute?) then + if (isLeftAttributeWithSearchString?) then + #palegreen:Add logic operator; + kill + endif + endif + endif + #pink:do nothing; + stop + case ( ) + :-1; + if (!MinusOneExists?) then + if (leftSideExists?) then + if (isLeftAttribute?) then + if (isLeftAttributeWithSearchString?) then + #palegreen:Add right bracket; + #palegreen:Add logic operator; + #palegreen:Add left bracket; + :positionCarret to start; + #palegreen:Add left bracket; + :positionCarret to end; + #palegreen:Add right bracket; + :positionCarret to end - 1; + kill + endif + endif + endif + endif + #pink:do nothing; + stop +@enduml diff --git a/docs/sweng/diagrams/autocomplete.png b/docs/sweng/diagrams/autocomplete.png new file mode 100644 index 00000000000..e9c7a68c40f Binary files /dev/null and b/docs/sweng/diagrams/autocomplete.png differ diff --git a/docs/sweng/diagrams/chip-view.png b/docs/sweng/diagrams/chip-view.png new file mode 100644 index 00000000000..22757991b4c Binary files /dev/null and b/docs/sweng/diagrams/chip-view.png differ diff --git a/docs/sweng/diagrams/class-diagram-withLegend.png b/docs/sweng/diagrams/class-diagram-withLegend.png new file mode 100644 index 00000000000..8b10c0b90bf Binary files /dev/null and b/docs/sweng/diagrams/class-diagram-withLegend.png differ diff --git a/docs/sweng/diagrams/class-diagram.png b/docs/sweng/diagrams/class-diagram.png new file mode 100644 index 00000000000..1d8f3b67685 Binary files /dev/null and b/docs/sweng/diagrams/class-diagram.png differ diff --git a/docs/sweng/diagrams/class-diagram2.0-withLegend.png b/docs/sweng/diagrams/class-diagram2.0-withLegend.png new file mode 100644 index 00000000000..d63e58be665 Binary files /dev/null and b/docs/sweng/diagrams/class-diagram2.0-withLegend.png differ diff --git a/docs/sweng/diagrams/class-diagram2.0.png b/docs/sweng/diagrams/class-diagram2.0.png new file mode 100644 index 00000000000..ea9e8b43494 Binary files /dev/null and b/docs/sweng/diagrams/class-diagram2.0.png differ diff --git a/docs/sweng/diagrams/logic-operators.png b/docs/sweng/diagrams/logic-operators.png new file mode 100644 index 00000000000..63e5a6885f1 Binary files /dev/null and b/docs/sweng/diagrams/logic-operators.png differ diff --git a/docs/sweng/diagrams/recent-search.png b/docs/sweng/diagrams/recent-search.png new file mode 100644 index 00000000000..47ec36055d8 Binary files /dev/null and b/docs/sweng/diagrams/recent-search.png differ diff --git a/docs/sweng/diagrams/user-search-input.png b/docs/sweng/diagrams/user-search-input.png new file mode 100644 index 00000000000..b1e3019dfd4 Binary files /dev/null and b/docs/sweng/diagrams/user-search-input.png differ diff --git a/docs/sweng/functional-specification-document.md b/docs/sweng/functional-specification-document.md new file mode 100644 index 00000000000..4ffe3175af8 --- /dev/null +++ b/docs/sweng/functional-specification-document.md @@ -0,0 +1,171 @@ +# Functional Specification Document "Better Search" Feature for JabRef + +## 1. Introduction + +### 1.1 Purpose + +- The open-sourced citation and reference management software JabRef is in need of a more functional global search and a user-friendly interface thereof. +- The users of JabRef should be able to conveniently search in their citations library and perform complex search querys, also via GUI. +- The main purpose of this feature is to add a UI for the global search bar. + +### 1.2 Definitions + +- Bracketing: Lucene allows for bracketing of multiple fields +- Chipview: A box design for search terms which allows easier separation +- Chips: A box in the ChipView. May contain search words, Boolean operators, ... +- Complex search query: JabRef specific search query +- Dropdown menu: Graphical menu which shows up below the item which triggers the menu +- GUI: Graphical user interface +- JabRef: Citation and reference management software +- Lucene: Open-source search engine library. [More details](http://www.lucenetutorial.com/lucene-query-syntax.html) +- Query: Is a request for data or information from a database table or combination of tables +- Search field: The textfield from the global search bar. +- Search user interface: Main graphical user interface for the developed extension +- Search key: Every search string typed into the global search bar which isn't a field. Search strings are separated by spaces in general but can be extended to more than one word by enclosing the search term within "". + +### 1.3 Applications & Goals + +- "Better Search" is a feature implementation for the software JabRef. +- Target group of the JabRef software are researchers, PhD candidates, university members, students and the scientific community in general. +- An internet connection is not needed for this feature. +- Search queries in global search should have the same syntax as the already implemented web search. +- Search queries need to be performed reliably and without any bugs or errors. +- Improve the view of the search bar. +- Improve the use of the search bar. +- Improve the search function of the search bar. +- Improve the autocomplete function of the search bar. + +### 1.4 References +* /R00/: + +### 1.5 Overview + +- The search user interface is activated upon click on the search term in the search box. +- Autocomplete is implemented into the global search bar and shows suggestions for fields and string searches in the search user interface. +- A chipview design for the fields in the global search bar shall show the search key in separated boxes. +- Search strings in the search field are already parsed with Lucene. + + +## 2. General description + +### 2.1 Embedding + +- The search category fields from the dropdown menu can be chosen via mouseclick and added with a search key, they will be used in the global search bar. +- The search field uses Lucene query syntax to run the search. +- The search field uses the internal autocompletion which proposes search key results from the local database. + + +### 2.2 Functional requirements + +| Features | | | | +| :---: | :---: | :---: | :---: | +| MUST: | Search User Interface | Chipview Design | Autocompletion | +| SHOULD: | Highlighting | Recent Search | + +- Lucene syntax +- Search user interface +- Chipview design +- Autocompletion +- Highlighting +- Recent search + +### 2.3 User profile + +- All JabRef users with basic know-how in field-based search. +- Target group of the JabRef software are researchers, PhD candidates, university members, students and the scientific community in general. +- For all supported operating systems (Linux, OS, Windows) + +### 2.4 Limitations + +- Lucene has to be used for global search bar. +- The speed of query evaluation depends on the current JabRef implementation and won't be altered. + +### 2.5 Assumptions & Dependencies + +- Search bar exists and is running properly. + + +## 3. Specific requirements + +- /F00/ The global search bar functionality must be extended with new features. + - /F01/ (/F10/) The global search bar must implement a search user interface. + - /F02/ (/F20/) The global search bar must must allow for a Lucene syntax. + - /F03/ (/F30/) The global search bar must implement a chipview design for the fields and search keys. + - /F05/ (/F60/) The global search bar must implement a new button to the right which allows for a dropdown menu with recent searches. + +- /F10/ The search user interface must be implemented as dropdown menu. + - /F11/ The search user interface must popup below the search bar after clicking it. + - /F12/ The search user interface must contain all relevant fields as clickable items. + - /F13/ The search user interface must contain logical operators like OR/AND as clickable items. + - /F14/ The search user interface must close if the user clicks somewhere other than the interface itself or the global search bar. + - /F15/ The search user interface could implement a special section for recent search queries. + - /F16/ The search user interface could implement an adjustment bar with two markers left and right to limit the search for papers within a specific period of years. + - /F17/ The search bar interface could implement complex bracketing of search terms and boosting in form of clickable items. + +- /F20/ (/F00/) The Lucene query parser must be used in the global search bar. + - /F21/ The Lucene query parser must function in the same way it was implemented in the web search bar. + - /F22/ The Lucene query parser must take the complete global search string and fetch it to the complex search query. + +- /F30/ The global search bar must appear in a chipview design. + - /F31/ (/F12/) Clicking on a field must open a new chip that allows entering a search key. + - /F32/ Chips must be closable by clicking on a cross next to the keyword. + - /F33/ (/F13/) Clicking on a field must open a new chip for AND/OR. + - /F34/ The order of the chips should be changeable by drag and drop. + - /F35/ The cursor should jump over the separate chips. + +- /F40/ Autocomplete must complete missing characters/words in the interface. + - /F41/ Autocomplete completed text must be displayed in a list below the search bar. + - /F42/ Words suggested by autocomplete should be color-coded in the suggestions. + - /F43/ Autocomplete could correct typos and overwrite them. + +- /F50/ Red highlighting could be implemented for the search field if the search syntax is not valid. + +- /F60/ The recent search should be implemented as a dropdown menu. + - /F61/ A clickable item corresponding to the recent search should be inside the global search bar. + - /F62/ When selecting the clickable item it should open a dropdown menu right below the clickable item. + - /F63/ The dropdown menu should contain a list of the last used search queries. + - /F64/ Selecting a query from the list should perform a search with the selected query. + +## 4. Acceptance criteria + +- /A10/ JabRef users can search in the global search bar by using the search user interface. +- /A20/ JabRef users can use Lucene syntax in the global search bar. +- /A30/ JabRef users gets suggestions for search (autocompletion). +- /A40/ JabRef users can see their current search keys as chips in the global search bar. +- /A50/ JabRef users gets feedback via highlighted search key. +- /A60/ JabRef users can see their recent searches in a separate dropdown menu. +- /A98/ Extension passes all unit tests. +- /A99/ The extension's new source code conforms to JabRefs coding style. + +# Apendix + +### Use-cases + +### Use Case 1: +* Name: Click on global search bar +* Stakeholders: Users of JabRef +* Preconditions: JabRef must be started +* Procedure: + * Click on the global search bar +* Postconditions: Dropdown menu shows up below the search bar with suggestions for syntax use. Last seen searches are listed. +* Postconditions exception: Dropdown menu shows up below the search bar with suggestions for syntax use. No prior searches show empty "Last searches" list + +### Use Case 2: +* Name: Make a search +* Stakeholders: Users of JabRef +* Preconditions: JabRef must be started +* Procedure: + * Click on the global search bar. + * Choose the fields you would like to search in (i.e. author, title etc.). + * Click on author field (button or text) in the interface. + * Type in the author "Beat Schmutz" into the global search bar and press ENTER. + * Click on the OR field in the interface. + * Click on the title field in the interface. + * Type in "Morphological analysis" and press ENTER. + * Click on an entry match in the main library pane. +* Postconditions: + * After selection of the author field an author field shows up in the global search bar. The cursor will be positioned to the right of this field. + * After typing in the author and selecting the OR field the position of the cursor is to the right again. + * After selecting the title field the cursor position changes to the right of this field once again. Typing in the title and pressing ENTER starts the search query which will be parsed with Lucene and passed on to the complex search query. The matched entries will show up in the main library pane. After clicking on an entry Metadata will show up; "Beat Schmutz" and "Morphological analysis" will be highlighted. +* Postconditions exception: The search finds no matching entries in the library. in this case the library pane stays empty. + diff --git a/docs/sweng/legend.pu b/docs/sweng/legend.pu new file mode 100644 index 00000000000..c44b65b3988 --- /dev/null +++ b/docs/sweng/legend.pu @@ -0,0 +1,7 @@ +@startuml legend #White +legend right + |<#TECHNOLOGY> | <#White> Existing class | + |<#PeachPuff> | <#White> New class | + |<#SkyBlue> | <#White> Modified class | +endlegend +@enduml \ No newline at end of file diff --git a/docs/sweng/presentation_slides.pdf b/docs/sweng/presentation_slides.pdf new file mode 100644 index 00000000000..519787109d2 Binary files /dev/null and b/docs/sweng/presentation_slides.pdf differ diff --git a/docs/sweng/project-repo/README.md b/docs/sweng/project-repo/README.md new file mode 100644 index 00000000000..a3660a38aa6 --- /dev/null +++ b/docs/sweng/project-repo/README.md @@ -0,0 +1,3 @@ +# Jabref Software Engineering Project files + +This repo is a place where we collect all important files that are relevant to the project. diff --git a/docs/sweng/project-repo/w1/GanttProjectplan.gan b/docs/sweng/project-repo/w1/GanttProjectplan.gan new file mode 100644 index 00000000000..ae77e47b357 --- /dev/null +++ b/docs/sweng/project-repo/w1/GanttProjectplan.gan @@ -0,0 +1,193 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 8 Constraints +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/sweng/project-repo/w1/functional-specification-document.md b/docs/sweng/project-repo/w1/functional-specification-document.md new file mode 100644 index 00000000000..fab27fe82c0 --- /dev/null +++ b/docs/sweng/project-repo/w1/functional-specification-document.md @@ -0,0 +1,173 @@ +# Functional Specification Document "Better Search" Feature for JabRef + +## 1. Introduction + +### 1.1 Purpose + +- The open-sourced citation and reference management software JabRef is in need of a more functional global search and a user-friendly interface thereof. +- The users of JabRef should be able to conveniently search in their citations library and perform complex search querys, also via GUI. +- The goal of this project is to develop an implementation that improves upon the current library search capabilities and enables users of the JabRef software to very easily navigate their libraries. + +### 1.2 Definitions + +- Bracketing and Boost: Lucene allows for bracketing of multiple fields +- Chipview: A box design for search terms which allows easier separation +- Chips: boxes including a search key +- Complex search query: JabRef specific search query +- Dropdown menu: Graphical menu which shows up below the item which triggers the menu +- GUI: Graphical user interface +- JabRef: Citation and reference management software +- Lucene: Open-source search engine library +- Query: Is a request for data or information from a database table or combination of tables +- Search user interface: Main graphical user interface for the developed extension +- Search key: Every search string typed into the global search bar which isn't a field. Search strings are separated by spaces in general but can be extended to more than one word by enclosing the search term within "". + +### 1.3 Applications & Goals + +- "Better Search" is a feature implementation for the software JabRef. +- Target group of the JabRef software are researchers, PHD candidates, university members, students and the scientific community in general. +- An internet connection is not needed for this feature. +- Search queries in global search must have the same syntax as the already implemented web search. +- Search queries need to be performed reliably and without any bugs or errors. +- Improve the view of the search bar. +- Improve the use of the search bar. +- Improve the search function of the search bar. +- Improve the autocomplete function of the search bar. + +### 1.4 References +* /R00/: [https://github.com/koppor/jabref/issues/341]() + +### 1.5 Overview + +- The search user interface is a dropdown menu. +- Autocomplete is implemented into the global search bar and shows suggestions for fields and string searches in the search user interface. +- A chipview design for the fields in the global search bar shall show the search key in separated boxes. +- Search strings in the global search bar are already parsed with Lucene. + + +## 2. General description + +### 2.1 Embedding + +- The search category fields from the dropdown menu can be chosen via mouseclick and added with a search key, they will be used in the global search bar. +- The global search bar uses Lucene query syntax to run the search. +- The global search bar uses the internal autocompletion which proposes search key results from the local database. + + +### 2.2 Functional requirements + +| Features | | | | | | +| :---: | :-: | :-: | :-: | :-: | :-: | +| MUST: | Global Search Bar | Lucene Syntax | Search User Interface | Chipview Design | Autocompletion | +| SHOULD: | Highlighting | Recent Search | + +- Lucene syntax +- Search user interface +- Chipview design +- Autocompletion +- Highlighting +- Recent search + + +### 2.3 User profile + +- For all JabRefusers in all Languages, without special knowhow. +- Target group of the JabRef software are researchers, PHD candidates, university members, students and the scientific community in general. +- For all supported operating systems (Linux, OS, Windows) + +### 2.4 Limitations + +- Lucene has to be used for global search bar. +- Chipview is required in the search bar. +- Search bar is required to use autocomplete. + +### 2.5 Assumptions & Dependencies + +- Search bar exists and is running properly. + + +## 3. Specific requirements + +- /F00/ The global search bar functionality must be extended with new features. + - /F01/ (/F10/) The global search bar must implement a search user interface. + - /F02/ (/F20/) The global search bar must must allow for a Lucene syntax. + - /F03/ (/F30/) The global search bar must implement a chipview design for the fields and search keys. + - /F05/ (/F60/) The global search bar must implement a new button to the right which allows for a dropdown menu with recent searches. + +- /F10/ The search user interface must be implemented as dropdown menu. + - /F11/ The search user interface must popup below the search bar after clicking it. + - /F12/ The search user interface must contain all relevant fields as clickable items. + - /F13/ The search user interface must contain logical operators like OR/AND as clickable items. + - /F14/ The search user interface must close if the user clicks somewhere other than the interface itself or the global search bar. + - /F15/ The search user interface could implement a special section for recent search queries. + - /F16/ The search user interface could implement an adjustment bar with two markers left and right to limit the search for papers within a specific period of years. + - /F17/ The search bar interface could implement complex bracketing of search terms and boosting in form of clickable items. + +- /F20/ (/F00/) The Lucene query parser must be used in the global search bar. + - /F21/ The Lucene query parser must function in the same way it was implemented in the web search bar. + - /F22/ The Lucene query parser must take the complete global search string and fetch it to the complex search query. + +- /F30/ The global search bar must appear in a chipview design. + - /F31/ (/F12/) Clicking on a field must open a new chip that allows entering a search key. + - /F32/ Chips must be closable by clicking on a cross next to the keyword. + - /F33/ (/F13/) Clicking on a field must open a new chip for AND/OR. + - /F34/ The order of the chips should be changeable by drag and drop. + - /F35/ The cursor should jump over the separate chips. + +- /F40/ Autocomplete must complete missing characters/words in the interface. + - /F41/ Autocomplete completed text must be displayed in a list below the search bar. + - /F42/ Words suggested by autocomplete should be color-coded in the suggestions. + - /F43/ Autocomplete could correct typos and overwrite them. + +- /F50/ Highlighting the search key after a search properly should be implemented into JabRef. + - /F51/ Search keys should be highlighted red if not found in the global database. + +- /F60/ The recent search should be implemented as a dropdown menu. + - /F61/ A clickable item corresponding to the recent search should be inside the global search bar. + - /F62/ When selecting the clickable item it should open a dropdown menu right below the clickable item. + - /F63/ The dropdown menu should contain a list of the last used search queries. + - /F64/ Selecting a query from the list should perform a search with the selected query. + +## 4. Acceptance criteria + +- /A10/ JabRef users can search in the global search bar by using the search user interface. +- /A20/ JabRef users can use Lucene syntax in the global search bar. +- /A30/ JabRef users gets suggestions for search (autocompletion). +- /A40/ JabRef users can see their current search keys as chips in the global search bar. +- /A50/ JabRef users gets feedback via highlighted search key. +- /A60/ JabRef users can see their recent searches in a separate dropdown menu. +- /A98/ Extension passes all unit tests. +- /A99/ The extension's new source code conforms to JabRefs coding style. + +# Apendix + +### Use-cases + +### Use Case 1: +* Name: Click on global search bar +* Stakeholders: Users of JabRef +* Preconditions: JabRef must be started +* Procedure: + * Click on the global search bar +* Postconditions: Dropdown menu shows up below the search bar with suggestions for syntax use. Last seen searches are listed. +* Postconditions exception: Dropdown menu shows up below the search bar with suggestions for syntax use. No prior searches show empty "Last searches" list + +### Use Case 2: +* Name: Make a search +* Stakeholders: Users of JabRef +* Preconditions: JabRef must be started +* Procedure: + * Click on the global search bar. + * Choose the fields you would like to search in (i.e. author, title etc.). + * Click on author field (button or text) in the interface. + * Type in the author "Beat Schmutz" into the global search bar and press ENTER. + * Click on the OR field in the interface. + * Click on the title field in the interface. + * Type in "Morphological analysis" and press ENTER. + * Click on an entry match in the main library pane. +* Postconditions: + * After selection of the author field an author field shows up in the global search bar. The cursor will be positioned to the right of this field. + * After typing in the author and selecting the OR field the position of the cursor is to the right again. + * After selecting the title field the cursor position changes to the right of this field once again. Typing in the title and pressing ENTER starts the search query which will be parsed with Lucene and passed on to the complex search query. The matched entries will show up in the main library pane. After clicking on an entry Metadata will show up; "Beat Schmutz" and "Morphological analysis" will be highlighted. +* Postconditions exception: The search finds no matching entries in the library. in this case the library pane stays empty or a pop-up could suggest that no entries were found. + diff --git a/docs/sweng/project-repo/w1/pflichtenheft-template.md b/docs/sweng/project-repo/w1/pflichtenheft-template.md new file mode 100644 index 00000000000..6fdf996ec19 --- /dev/null +++ b/docs/sweng/project-repo/w1/pflichtenheft-template.md @@ -0,0 +1,119 @@ +--- +layout: default +title : Woche 6 +--- +# Pflichtenheft +##### (Nach Lichter & Ludwig, Software Engineering: Grundlagen, Menschen, Prozesse, Techniken) + + +## 1. Einleitung + +### 1.1 Zweck + +Beschreibt den Zweck und den Leserkreis der Spezifikation. + +### 1.2 Einsatzbereich und Ziele +Gibt an, wo die Software eingesetzt werden soll und welche wesentlichen Funktionen es haben wird. Wo sinnvoll, sollte auch definiert werden, was die Software nicht leisten wird. + +Beschreibt die mit der Software verfolgten Ziele. + +### 1.3 Definitionen + +Dokumentiert alle verwendeten Fachbegriffe und Abkürzungen. Alternativ können Sie auch ein separates Glossar nutzen. + +### 1.4 Referenzierte Dokumente + +Verzeichnet alle Dokumente, auf die in der Spezifikation verwiesen wird. + +### 1.5 Überblick + +Beschreibt, wie der Rest der Spezifikation aufgebaut ist, insbesondere, wie Kapitel 3 strukturiert ist. + +## 2. Allgemeine Beschreibung + +### 2.1 Einbettung + +Beschreibt, wie das System in seine Umgebung eingebettet ist und wie die Software mit den umgebenden Komponenten und Systemen zusammenspielt. Dazu werden die Schnittstellen, Kommunikationsprotokolle etc. definiert. + +### 2.2 Funktionen + +Skizziert die wichtigsten Funktionen + +### 2.3 Benutzerprofile + +Charakterisiert die Benutzergruppen und die Voraussetzungen die diese jeweils mitbringen (Ausbildung, Know-how, Sprache) + +### 2.4 Einschränkungen +Dokumentiert Einschränkungen, die die Freiheit der Entwicklung reduzieren (Basis-Software, Ziel-Hardware, Gesetzliche Grundlagen, ...) + +### 2.5 Annahmen und Abhängigkeiten +Nennt explizit die Annahmen und externen Voraussetzungen, von denen bei der Spezifikation ausgegangen wurde. + + +## 3. Einzelanforderungen + +Beschreibt die Anforderung i so genau, dass bei der Verwendung der Spezifikation (im Entwurf usw.) keine Rückfragen dazu notwendig sind. + +Identifizieren Sie jede Funktionale Anforderung mit einer Nummer, so dass diese Nachverfolgbar sind. Zusammengehörende Funktionale Anforderungen können durch geeignete Nummerierung angezeigt werden. + +Zur Spezifikation der Software sollen Sprachschablonen benutzt werden. + +* /F10/ Funktion 1 des Systems +* /F11/ Weitere Detaillierung Funkion 1 +* /F20/ Funktion 2 des Systems + + +Die Funktionalen Anforderungen sollen mithilfe von Use-cases erhoben werden. Die Use-cases sollen in Anhang A detailliert beschrieben werden. + +## 4. Abnahmekriterien + +Beschreiben Sie hier, wie die Anforderungen bei der Abnahme auf ihre Realisierung überprüft werden können. + +Definieren Sie hier mindestens ein Abnahmekriterium +* /A10/ Abnahmekriterium 1 +* /A20/ Abnahmekriterium 2 + + +# Anhang + +## Anhang A. Use-cases + +An dieser Stelle können detaillierte Use-cases angegeben werden +![Diagram](../../slides/images/use-case.png) + +### Use Case 1: +* Name: *Name des Use-cases* +* Akteure: *Akteur1, Akteur2, ...* +* Vorbedingungen: *Was muss vor Beginn des Ablaufs gelten* +* Standardablauf + * Schritt 1 + * Schritt 2 +* Nachbedingungen Erfolg: *Was muss nach dem Ende des erfolgreichen Ablaufs gelten* +* Nachbedingung Sonderfall: *Was gilt nach dem Ende, wenn der Ablauf fehlgeschlagen ist* + + +#### Sonderfall 1a: Ausnahme 1 +* Ablauf Sonderfall 1a + * Schritt 1 + * Schritt 2 + +#### Sonderfall 1b: Ausnahme 2 +* Ablauf Sonderfall 1b + * Schritt 1 + * Schritt 2 + + +### Use Case 2: +* Name: *Name des Use-cases* +* Akteure: *Akteur1, Akteur2, ...* +* Vorbedingungen: *Was muss vor Beginn des Ablaufs gelten* +* Standardablauf + * Schritt 1 + * Schritt 2 +* Nachbedingungen Erfolg: *Was muss nach dem Ende des erfolgreichen Ablaufs gelten* +* Nachbedingung Sonderfall: *Was gilt nach dem Ende, wenn der Ablauf fehlgeschlagen ist* + +#### Sonderfall 2a: Ausnahme 1 +* Ablauf Sonderfall 1a + * Schritt 1 + * Schritt 2 diff --git a/docs/sweng/project-repo/w1/pflichtenheft.md b/docs/sweng/project-repo/w1/pflichtenheft.md new file mode 100644 index 00000000000..80af8944172 --- /dev/null +++ b/docs/sweng/project-repo/w1/pflichtenheft.md @@ -0,0 +1,172 @@ +# Functional Specification Document "Better Search" Feature for JabRef + +## 1. Introduction + +### 1.1 Purpose + +- The open-sourced citation and reference management software JabRef is in need of a more functional global search and a user-friendly interface thereof. +- The users of JabRef should be able to conveniently search in their citations library and perform complex search querys, also via GUI. +- The goal of this project is to develop an implementation that improves upon the current library search capabilities and enables users of the JabRef software to very easily navigate their libraries. + +### 1.2 Definitions + +- JabRef: Citation and reference management software +- Lucene: Search Query Syntax +- Query: Is a request for data or information from a database table or combination of tables +- Chipview: A box design for search terms which allows easier separation +- Chips: box including a search key +- GUI: Graphical user interface +- Dropdown menu: Graphical menu which shows up below the item which triggers the menu +- Bracketing and Boost: Lucene allows for bracketing of multiple fields +- Search user interface: Main graphical user interface for the developed extension +- Complex search query: JabRef specific search query + + +### 1.3 Applications & Goals + +- "Better Search" is a feature implementation for the software JabRef. +- Target group of the JabRef software are researchers, PHD candidates, university members, students and the scientific community in general. +- An internet conneciton is not needed for this feature. +- Search queries in global search must have the same syntax as the already implemented web search. +- Search queries need to be performed reliably and without any bugs or errors. +- Improve the view of the search bar. +- Improve the use of the search bar. +- Improve the search function of the search bar. +- Improve the autocomplete function of the search bar. + +### 1.4 References +* /R00/: [https://github.com/koppor/jabref/issues/341]() + +### 1.5 Overview + +- The search user interface is a dropdown menu. +- Lucene is implemented into the global search bar. +- Autocomplete is implemented into the global search bar and shows suggestions for fields and string searches in the search user interface. +- A chipview design for the fields in the searchbar shall show the search key in separated boxes +- Search strings in the global search bar are parsed with Lucene. + + +## 2. General description + +### 2.1 Embedding + +- The searchcategoriefields from the dropdown menu can be chosen via mouseclick and added with a search key they will be used in the global searchbar. +- The globalsearchbar uses Lucene query syntax to run the search +- The global searchbar uses the internal autocompletion which propose search key results from the local database. + + +### 2.2 Functional requirements + +| Features | | | | | | +| :---: | :-: | :-: | :-: | :-: | :-: | +| MUST: | Global Search Bar | Lucene Syntax | Search User Interface | Chipview Design | Autocompletion | +| SHOULD: | Highlighting | Recent Search | + +- Lucene syntax +- Search user interface +- Chipview design +- Autocompletion +- Highlighting +- Recent search + + +### 2.3 User profile + +- For all JabRefusers in all Languages, without special knowhow. + +### 2.4 Limitations + +- Lucene has to be used for global search bar. +- Chipview is required in the search bar. +- Search bar is required to use autocomplete. + +### 2.5 Assumptions & Dependencies + +- Search bar exists and is running properly. + + +## 3. Specific requirements + +- /F00/ The global search bar functionality must be extended with new features. + - /F01/ (/F10/) The global search bar must implement a search user interface. + - /F02/ (/F20/) The global search bar must must allow for a Lucene syntax. + - /F03/ (/F30/) The global search bar must implement a chipview design for the fields and search keys. + - /F05/ (/F60/) The global search bar must implement a new button to the right which allows for a dropdown menu with recent searches. + +- /F10/ The search user interface must be implemented as dropdown menu. + - /F11/ The search user interface must popup below the search bar after clicking it. + - /F12/ The search user interface must contain all relevant fields as clickable items. + - /F13/ The search user interface must contain logical operators like OR/AND as clickable items. + - /F14/ The search user interface must close if the user clicks somewhere other than the interface itself or the global search bar. + - /F15/ The search user interface could implement a special section for recent search queries. + - /F16/ The search user interface could implement an adjustment bar with two markers left and right to limit the search for papers within a specific period of years. + - /F17/ The search bar interface could implement complex bracketing of search terms and boosting in form of clickable items. + +- /F20/ (/F00/) The Lucene query parser must be used in the global search bar. + - /F21/ The Lucene query parser must function in the same way it was implemented in the web search bar. + - /F22/ The Lucene query parser must take the complete global search string and fetch it to the complex search query. + +- /F30/ The searchbar must appear in a chipview design. +<<<<<<< HEAD + - /F31/ (/F12/) Clicking on a field must open a new chip that allows entering a search key. + - /F32/ Chips must be closable by clicking on a cross next to the keyword. + - /F33/ (/F13/) Clicking on a fiel must open a new chip for AND/OR. + - /F34/ The order of the chips should be changeable by drag and drop. + - /F35/ The cursor should jump over the seperate chips. + +- /F40/ Autocomplete completes missing characters/words in the interface. + - /F41/ JabRef users can choose from a combobox of suggested datasets + - /F42/ Autocomplete completed text is displayed in a list below the search bar and is color-coded to match the search word entered + - /F43/ Autocomplete corrects typos and overwrites them + +- /F50/ highlighting the search key. + - /F51/ highlights search keys red if not found in the global database. + +- /F60/ The recent search should be implemented as a dropdown menu. + - /F61/ A clickable item corresponding to the recent search shold be inside the searchbar. + - /F62/ When you select the clickable item it should open a dropdown menu right below the clickable item. + - /F63/ The dropdown menu should contain a list of the last used search queries. + - /F64/ Selecting a query from the list should perform a search with the selected query. + +## 4. Acceptance criteria + +- /A10/ JabRef users can search in the global searchbar by using the search user interface. +- /A20/ JabRef users can use Lucene syntax in the global search bar. +- /A30/ JabRef users gets suggestions for search (autocompletion). +- /A40/ JabRef users can see their current search keys as chips in the global search bar. +- /A50/ JabRef users gets feedback via highlighted search key. +- /A60/ JabRef users can see their recent searches in a separate dropdown menu. + + +# Apendix + +### Use-cases + +### Use Case 1: +* Name: Click on global search bar +* Stakeholders: Users of JabRef +* Preconditions: JabRef must be started +* Procedure: + * Click on the global search bar +* Postconditions: Dropdown menu shows up below the search bar with suggestions for syntax use. Last seen searches are listed. +* Postconditions exception: Dropdown menu shows up below the search bar with suggestions for syntax use. No prior searches show empty "Last searches" list + +### Use Case 2: +* Name: Make a search +* Stakeholders: Users of JabRef +* Preconditions: JabRef must be started +* Procedure: + * Click on the global search bar. + * Choose the fields you would like to search in (i.e. author, title etc.). + * Click on author field (button or text) in the interface. + * Type in the author "Beat Schmutz" into the global search bar and press ENTER. + * Click on the OR field in the interface. + * Click on the title field in the interface. + * Type in "Morphological analysis" and press ENTER. + * Click on an entry match in the main library pane. +* Postconditions: + * After selection of the author field an author field shows up in the global search bar. The cursor will be positioned to the right of this field. + * After typing in the author and selecting the OR field the position of the cursor is to the right again. + * After selecting the title field the cursor position changes to the right of this field once again. Typing in the title and pressing ENTER starts the search query which will be parsed with Lucene and passed on to the complex search query. The matched entries will show up in the main library pane. After clicking on an entry Metadata will show up; "Beat Schmutz" and "Morphological analysis" will be highlighted. +* Postconditions exception: The search finds no matching entries in the library. in this case the library pane stays empty or a pop-up could suggest that no entries were found. + diff --git a/docs/sweng/project-repo/w3-UML/diagrams.pu b/docs/sweng/project-repo/w3-UML/diagrams.pu new file mode 100644 index 00000000000..f27df339284 --- /dev/null +++ b/docs/sweng/project-repo/w3-UML/diagrams.pu @@ -0,0 +1,273 @@ +' +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +@startuml +mainframe **Class Diagram: "Better Search" Implementation ** +' package LEGEND { +' class Existing as " Existing Classes " #White +' hide Existing circle +' hide Existing methods +' hide Existing members + +' class New as " New Classes " +' hide New circle +' hide New methods +' hide New members + +' ' Workaround to prevent PlantUML from positioning the legend blocks randomly. +' Existing -[hidden]- New +' } + +' ======================================================= +' Grouping +' ======================================================= +together { + class DropDownMenu + class SearchFieldSynchronizer +} + +' ======================================================= +' Relations +' ======================================================= +CustomTextField - GlobalSearchbar +SearchItem *- itemType +RecentSearch <- SearchFieldSynchronizer +GlobalSearchbar -- Highlighting +GlobalSearchbar <-- Autocomplete +DropDownMenu <-- RecentSearch + +SearchFieldSynchronizer -- LuceneBasedSearchRule +SearchItem "0...*" -> "1 " SearchFieldSynchronizer +JFXChipView -- SearchFieldSynchronizer +DropDownMenu --> SearchFieldSynchronizer +CustomTextField <-- TextInputControl +Autocomplete <-- TextInputControl +TextInputControl <-- SearchFieldSynchronizer +RecentSearch "1 " <-- "0...*" SearchItem + +' ======================================================= +' Classes +' ======================================================= +class GlobalSearchbar #SkyBlue { + searchField: CustomTextField + updateSearchResultsForQuery() + ' initSearchModifierButtons() + ' initSearchModifierButton() + ' focus() + performSearch() + ' validRegex() + ' informUserAboutInvalidSearchQuery() + updateResults() + ' setSearchFieldHintTooltip() + ' updateHintVisibility() + ' registerEventListener() + ' onSuggestionChosen() +} + +class Highlighting #TECHNOLOGY { + +} + +class RecentSearch #PeachPuff { + pastSearchItemList: ArrayList + ListView authors + ListView titles + show() + hide() + add() + checkSearchItemExists() + +} + +class DropDownMenu #PeachPuff { + authorBtn: Button + journalBtn: Button + titleBtn: Button + yearBtn: Button + andBtn: Button + orBtn: Button + andMinusBtn: Button + orMinusBtn: Button + yearRange: RangeSlider + stringadder: Textfield + isPrevAttribute: boolean + checkAndAddSpace() + isPrevAttribute() +} + +class LuceneBasedSearchRule #TECHNOLOGY { + validateSearchStrings() +} + +class Autocomplete #TECHNOLOGY { + AutoCompletionTextFieldBinding String + AutoCompletePopup autoCompletionPopup + bindAutoCompletion() +} + +class SearchFieldSynchronizer #PeachPuff { + searchItemList: ArrayList + searchString: String + addSearchitem() + isValid() + returnLatest() + synchronize() + searchStringBuilder() + updateSearchItemList() + searchItemListToString() + textFieldToList() + syntaxHighlighting() + bracketsBalanced() + isValisLucene() + deleteAllEntries() +} + +class SearchItem #PeachPuff { + itemType: enum + item: String + getType() + get() + setType() + set() +} + +enum itemType #PeachPuff { + logicalOperator + attribute + bracket + query +} + +class CustomTextField #TECHNOLOGY { + get() + get(int start, int end) + length() + addListener() +} + +class TextInputControl #TECHNOLOGY { + getText() + insertText() + deleteText() + replaceText() + positionCaret() + getCarretPosition() + getLength() +} + +class JFXChipView #TECHNOLOGY { + chips + StringConverter() +} +@enduml + +' +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +' Julian: Recent Search +@startuml recent-search +mainframe **Sequence Diagram: Recent Search ** +actor User +User -> GlobalSearchBar: performSearch() +GlobalSearchBar -> RecentSearch: addQueryToRecentSearches +User -> RecentSearch: clickOnRecentSearchEntry +RecentSearch -> GlobalSearchBar: performSearch() +@enduml + +' +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +' Mark: User Search Input +@startuml user-search-input +mainframe **Sequence Diagram: User Search Input ** +actor User +User -> DropDownMenu: Button Input +box **SearchFieldSynchronizer** +DropDownMenu -> SearchFieldSynchronizer: ActionEvent +entity searchItemList +SearchFieldSynchronizer -> searchItemList: addSearchItemToList() +searchItemList --> SearchFieldSynchronizer: SearchStringBuilder() +end box +box **CustomTextField** #White +entity searchField +SearchFieldSynchronizer -> searchField: replaceText() +end box + +User -> searchField: Text Input +searchField --> SearchFieldSynchronizer: EventListener +SearchFieldSynchronizer -> searchItemList: updateList() +searchItemList --> SearchFieldSynchronizer: SearchStringBuilder() +SearchFieldSynchronizer -> searchField: replaceText() +@enduml + +' +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +' Albert: Autocomplete +@startuml +mainframe **Sequence Diagram: Autocomplete ** +actor User +User -> AutoCompletePopup: bindAutoCompletion() +AutoCompletePopup -> AutoCompletionTextInputBinding: convertSuggestionsToStrings +AutoCompletionTextInputBinding -> GlobalSearchBar: provideSuggestionsInGlobalSearch +GlobalSearchBar -> AutoCompletePopup: bindAutoCompletion() +@enduml + +' +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +' Dani: ChipView +@startuml +mainframe Sequence Diagram: ChipView +actor User +User->GlobalSearchBar: performSearch() +GlobalSearchBar->ChipView: addChipViewToSearchField() +User->ChipView: clickOnChipInSearchField +ChipView->GlobalSearchBar:deleteChosenSearchkey() +@enduml + +' +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +' Mark: Logical Operators +@startuml logic-operators +mainframe **Activity Diagram: Logic Operators ** +start +switch ( Button Press ) +case ( ) + :Attribute; + if (leftSideExists?) then + if (isLeftAttribute?) then + if (isLeftAttributeWithoutSearchString?) then + #pink:do nothing; + kill + endif + #palegreen:Add OR; + endif + endif + #palegreen:Add attribute; + stop +case ( ) + switch ( Logic Operator ) + case ( ) + :1; + if (leftSideExists?) then + if (isLeftAttribute?) then + if (isLeftAttributeWithSearchString?) then + #palegreen:Add logic operator; + kill + endif + endif + endif + #pink:do nothing; + stop + case ( ) + :-1; + if (!MinusOneExists?) then + if (leftSideExists?) then + if (isLeftAttribute?) then + if (isLeftAttributeWithSearchString?) then + #palegreen:Add right bracket; + #palegreen:Add logic operator; + #palegreen:Add left bracket; + :positionCarret to start; + #palegreen:Add left bracket; + :positionCarret to end; + #palegreen:Add right bracket; + :positionCarret to end - 1; + kill + endif + endif + endif + endif + #pink:do nothing; + stop +@enduml diff --git a/docs/sweng/project-repo/w3-UML/legend.pu b/docs/sweng/project-repo/w3-UML/legend.pu new file mode 100644 index 00000000000..c44b65b3988 --- /dev/null +++ b/docs/sweng/project-repo/w3-UML/legend.pu @@ -0,0 +1,7 @@ +@startuml legend #White +legend right + |<#TECHNOLOGY> | <#White> Existing class | + |<#PeachPuff> | <#White> New class | + |<#SkyBlue> | <#White> Modified class | +endlegend +@enduml \ No newline at end of file diff --git a/docs/sweng/project-repo/w3-UML/plantUML-in-VSCode.md b/docs/sweng/project-repo/w3-UML/plantUML-in-VSCode.md new file mode 100644 index 00000000000..2b542b6159e --- /dev/null +++ b/docs/sweng/project-repo/w3-UML/plantUML-in-VSCode.md @@ -0,0 +1,6 @@ +# How to install use PlantUML in VSCode + +- [Graphviz](http://www.graphviz.org/download/) +- [PlantUML Extension](https://marketplace.visualstudio.com/items?itemName=jebbs.plantuml) +- [PlantUML Reviewer Extension](https://marketplace.visualstudio.com/items?itemName=Mebrahtom.plantumlpreviewer) + - Context menu or Alt+D diff --git a/docs/sweng/project-repo/w3-UML/technical-documentation.md b/docs/sweng/project-repo/w3-UML/technical-documentation.md new file mode 100644 index 00000000000..2365b7e84ac --- /dev/null +++ b/docs/sweng/project-repo/w3-UML/technical-documentation.md @@ -0,0 +1,112 @@ +# Technical Documentation + +| Project Overview | | | | | | +| :---: | :-: | :-: | :-: | :-: | :-: | +| Version | Project Name | Authors | Status | Date | Comment | +| 0.1 | Better Search | MS, DM, JB, AJ | In Progress | 10.11.2021 | + +| Project Members | | | | +| :---: | :---: | :---: | :---: | +| Julian Bopp | Albert Jasari | Daniel Madoery | Mark Starzynski + +## 1 Introduction + +### 1.1 Purpose +- The goal of this document is to provide a good overview of the project that explains the use, functionality, creation and architecture of the product. It aims to function as a guide for its current and future developers. +kv +### 1.2 References +- Functional Specification Document: [functional-specification-document.md](https://github.com/josphstar/jabref/blob/functional_specifications_doc_w1/docs/sweng/functional-specification-document.md) +- Issue [#341](https://github.com/koppor/jabref/issues/341) + +## 2 System Overview + +The project goal is to implement an extension for the open-sourced citation and reference management software JabRef. The goal of this extension is to improve upon the existing global search function and make it more user-friendly by adding a dropdown menu with additional search functionalities. + +## 3 Design Goals + +For the design of the extension, at its core a GUI implementation of a dropdown menu, future extensability will be ensured because JabRef already has dropdown menus implemented with JavaFX. Those will be re-used, which will also satisfy low development costs and good performance. + +## System Performance + +The extension implements a dropdown menu for the existing global search bar. This dropdown menu will consist of clickable buttons which allows the user to search for specific fields (i.e. author, title) without the need of having to type in a complex search syntax. Furthermore the implemented dropdown menu has the option to browse through and re-search the most recent search queries. + +## Design + + + +### 5.1 Statics + +"Better Search" implementation will mainly evolve around the SeearchFieldSynchronizer class, which task it will be to keep user inputs up-to-date with the live search query and ensure proper syntax. + +For that an ArrayList will be kept and updated in accordance with the - by the user via search field or dropdown menu - intended live search query. + +RecentSearch will keep track of previously searched for important attributes - mainly authors and titles - in separate lists and also display them in the dropdown menu at the end. + +![Class Diagram](/w3-UML/out/w3-UML/better-search/class-diagram.png "class-diagram.png") + +### 5.2 Dynamics + +#### 5.2.1 User Search Input +With the implementation of a search dropdown menu the user can input information for search queries mainly in two ways: As usual via search field or - additionaly - via newly implemented dropdown menu. + +Handling synchronization between user interaction and proper display of the complete search string will be the class SearchFieldSynchronizer. +- When interacting with the dropdown menu it will check and add new items (attributes, logical operators, brackets) to the ArrayList searchItemList and build a full search string to update the search field. +- When typing directly into the search field it will fetch changes or additions to the ArrayList searchItemList and update the search field accordingly. + +![User Search Input](/w3-UML/out/w3-UML/better-search/user-search-input.png "user-search-input.png") + +#### 5.2.2 Recent Search + +The Recent Search functionality shows the user a record of past searches inside the dropdown menu. + +- When performSearch() is called, the query gets added to the RecentSearches List. +- Clicking on an entry in Recent Search performs a corresponding search. +- The List is ordered from bottom to top. + +![Recent Search](/w3-UML/out/w3-UML/better-search/recent-search.png "recent-search.png") + +#### 5.2.3 ChipView + +The ChipView transforms coherent search strings into a single chip. + +- Pressing a button in DropDownMenu creates a new chip. +- The resulting chip can then be filled with a search query. + +![ChipView](/w3-UML/out/w3-UML/better-search/chip-view.png "chip-view.png") + +#### 5.2.4 Autocomplete + +Autocomplete takes care of the automatically generated completion in the GlobalSearchBar which is done by the bindAutoCompletion() method in the AutoCompletePopup class. +The recommended strings are linked together and can thus be accepted or ignored by the user. +The input is tracked as far as possible. + +- A pop-up menu appears with all the recommended entries which you can click on. + +![Autocomplete](/w3-UML/out/w3-UML/better-search/autocomplete.png "autocomplete.png") + +### 5.3 Logic + +#### 5.3.1 Logic Operators +The buttons in the dropdown menu have to function differently under certain conditions. I.e. it is preferred to not allow two logical operators in a row without an attribute in between or to not allow adding search items while an attribute search string is still empty. + +Furthermore a rudimentary weighted logic operating option is depicted in the following activity diagram. A logic operator can be weighted normally (1) or weaker (-1) once, which allows for a little more complexity in the drop down menu. + +![Logic Operators](/w3-UML/out/w3-UML/better-search/logic-operators.png "logic-operators.png") + +#### 5.3.2 OCL Constraints + +| Constraint | OCL Constraint | +|---|---| +| The Search Query is not empty | **context** `GlobalSearchBar:performSearch()` **inv**: `self.SearchField != null` | +| After performing a search, the search query gets added to the recent search list| **context** `GlobalSearchBar:performSearch()` **post**: `RecentSearch.contains(GlobalSearchBar.searchField)`| +| AutoCompleter is reading searchfield and proposes different endings | **context** `GlobalSearchBar:AutoComplete.readCurrent(searchField)` **inv**: `AutoComplete.popup()`| +| If the GlobalSearchBar gets clicked the DropDownMenu will be activated | **context** `GlobalSearchBar:searchField.Mousevent.MOUS_CLICKED` **inv**: `GlobalSearchBar.dropDownMenu.show(searchField)` | +| If some button "Example" in the DropDownMenu is clicked the Searchbar gets updated with the input "example:"| **context** `DropDownMenu.ExampleButton` **inv**: `GlobalSearchBar.searchField.insertText()`| +| Only one "-1" logical operator is allowed | **context** `SearchFieldSynchronizer::SearchItemList` **inv**: `self.amountMinusOneLogical <= 1`| +| SearchItem object with itemType "attribute" must have a non empty item attribute | **context** `SearchItem` **inv**: `SearchItem.itemType == attribute` **implies** `SearchItem.item != null`| +| If button Year is clicked and the searchbar asks for input maximum 4 digits are allowed after "year:"| **context** `DropDownMenu.YearButton` **post**: `GlobalSearchBar:searchField.insertText().maximum = 9999`| \ No newline at end of file diff --git a/docs/sweng/project-repo/zalbi/funspecdoc.md b/docs/sweng/project-repo/zalbi/funspecdoc.md new file mode 100644 index 00000000000..b3128d5a735 --- /dev/null +++ b/docs/sweng/project-repo/zalbi/funspecdoc.md @@ -0,0 +1,111 @@ +# Functional Specification Document + +## 1. Introduction + +### 1.1 Purpose + + +- The actual purpose of Autocomplete is to assist the user in making entries in the Global Search Bar. This makes it easier to use and makes working in Jabref pleasant. +- Auto-complete suggestions must be displayed almost immediately after the user starts typing, with no delay. By providing suggestions quickly, the interface looks more interactive which improves the user experience. + To do this, we will highlight the differences to ensure that users can easily read and distinguish the suggestions, you should make the differences clear by highlighting or bolding them. + +### 1.2 Scope + + +- Auto-completion is case sensitive there should be a function which must distinguish for example at "prote", you may get "protein" but not "protease" if both are present. +- A mechanism to move to the next match also belongs in such an application, this should be easily executable via the tabulator. +- Autocomplete should correct typos by itself and thus create a pleasant way of working as mentioned above. +- All words to be matched should already be available in the database of Jabref so that an internet connection is not necessary. + +### 1.3 Definitions + + + +### 1.4 References + + + +### 1.5 Overview + + + +## 2. General Description + +### 2.1 Implementation + + + +### 2.2 Function Overview + +The autocompletions should also be able to be shared among concurrent users of the same database, so that there is consistency. + +One of JabRef's predecessors, Bibkeeper, had an autocomplete feature based on collecting all words used for the same field in the same or all open databases. This allowed suggestions to be made based on what keywords, etc. were used in other entries. + +### 2.3 User Requirements + + + +### 2.4 Limitations + + + +### 2.5 Assumptions and Dependencies + + + +### 3. Functional Requirements + + +- /F40/ Completes missing chars/words. +- /F41/ Completed text is selectable below the search bar. +- /F42/ Color-coded chars/words in the suggestions. +- /F43/ Correct typos and overwrite function. + + +### 4. Acceptance Criteria + + + +# Appendix + +## Appendix A. Use-cases + + +### Use Case 1: + + +### Use Case 2: + +* Name: Autocomplete +* Stakeholders: Users of JabRef +* Preconditions: JabRef must be started +* Procedure: + * Click on the global search bar. + * Choose the fields you would like to search in (i.e. author, title etc.). + * Type keywords to search for in search bar. + * choose from the suggested words from the list displayed below the search bar. +* Postconditions: + * After selection the word is transferred to the search bar and is ready for further entries +* Postconditions exception: + +## Appendix A. Open Questions +- \ No newline at end of file diff --git a/docs/sweng/project-repo/zalbi/jacocoAnleitung.md b/docs/sweng/project-repo/zalbi/jacocoAnleitung.md new file mode 100644 index 00000000000..a37bc074e07 --- /dev/null +++ b/docs/sweng/project-repo/zalbi/jacocoAnleitung.md @@ -0,0 +1,21 @@ +# Jacoco Test Coverage Anleitung + +######wenn du deinen aktuellen branch mit dem jacoco-test überprüfen willst, dann kannst du einen der beiden Wege gehen: + +### Methode 1 + + + +- du musst die beiden files "myproject.jacoco-aggregation" und "myproject.java-conventions" ins src/main/groovy reinmachen +- danach ins terminal "./gradlew jacocoTestReport" ausführen (achtung ist systemabhängig ob windows powershell oder sonst was eingestellt ist) + +### Methode 2 + +- führe den befehl "./gradlew jacocoTestReport" direkt im terminal aus + +### Methode 3 + +- gehe zu den "edit configurations" und wähle deine jabref-application +- rechts daneben drückst du auf die schaltfläche "modify options" und wählst aus der liste "specify alternative coverage runner" aus +- es erscheint ein dropdown menu auf welches du klicken musst und dann wählst du jacoco aus +- apply und oke drücken und auf das run-symbol clicken \ No newline at end of file diff --git a/docs/sweng/project-repo/zalbi/plantuml/sequenceDiagramRecentSearch.txt b/docs/sweng/project-repo/zalbi/plantuml/sequenceDiagramRecentSearch.txt new file mode 100644 index 00000000000..ed5e72c1407 --- /dev/null +++ b/docs/sweng/project-repo/zalbi/plantuml/sequenceDiagramRecentSearch.txt @@ -0,0 +1,8 @@ +@startuml +mainframe **Sequence Diagram: Autocomplete ** +actor User +User -> AutoCompletePopup: bindAutoCompletion() +AutoCompletePopup -> AutoCompletionTextInputBinding: convertSuggestionsToStrings +AutoCompletionTextInputBinding -> GlobalSearchBar: provideSuggestionsInGlobalSearch +GlobalSearchBar -> AutoCompletePopup: bindAutoCompletion() +@enduml \ No newline at end of file diff --git a/docs/sweng/project-repo/zalbi/zalbi.md b/docs/sweng/project-repo/zalbi/zalbi.md new file mode 100644 index 00000000000..53f61234a96 --- /dev/null +++ b/docs/sweng/project-repo/zalbi/zalbi.md @@ -0,0 +1,22 @@ +# Required Features + +##Realization of the problem +Problem with the auto-complete function for the various fields. + +For example, the list of journal abbreviations only works if the journal name matches exactly. This means that when I manually enter an entry, I have to find and copy and paste another entry from the same journal, or, if it's the first time a particular journal occurs, I have to open the abbreviation list and copy and paste from there. + +Suggested solution: + +It would be beneficial to start typing and the program would make suggestions for the end based on entries already in the database or abbreviation database, similar to what Firefox, Chrome and the like do with their text fields. It would also make sure that all entries conform to a consistent convention. + + +### CODE: AUTOCOMPLETE IMPLEMENTATION +- API [algoliasearch](https://github.com/algolia/algoliasearch-client-java-2) +- Implements a [ternary search tree](https://www.codeproject.com/articles/5819/ternary-search-tree-dictionary-in-c-faster-string?pageflow=Fluid&fid=31232&df=90&mpp=25&sort=Position&view=Normal&spc=Relaxed&prof=True&fr=26) + + +## pacakge: gui/search + +- ### class: + - Implements + \ No newline at end of file diff --git a/docs/sweng/project-repo/zbobby/OCL.md b/docs/sweng/project-repo/zbobby/OCL.md new file mode 100644 index 00000000000..0da34f27584 --- /dev/null +++ b/docs/sweng/project-repo/zbobby/OCL.md @@ -0,0 +1,12 @@ +# OCL Constraints + +| Constraint | OCL Constraint | +|---|---| +| The Search Query is not empty | **context** `GlobalSearchBar:performSearch()` **inv**: `self.SearchField != null` | +| After performing a search, the search query gets added to the recent search list| **context** `GlobalSearchBar:performSearch()` **post**: `RecentSearch.contains(GlobalSearchBar.searchField)`| +| If Lucene Syntax is not obeyed, search is not possible | | +| If the GlobalSearchBar gets clicked the DropDownMenu will be activated | **context** `GlobalSearchBar:searchField.Mousevent.MOUS_CLICKED` **inv**: `GlobalSearchBar.dropDownMenu.show(searchField)` | +| If some button "Example" in the DropDownMenu is clicked the Searchbar gets updated with the input "example:"| **context** `DropDownMenu.ExampleButton` **inv**: `GlobalSearchBar.searchField.insertText()`| +| Only one "-1" logical operator is allowed | **context** `SearchFieldSynchronizer::SearchItemList` **inv**: `self.amountMinusOneLogical <= 1`| +| SearchItem object with itemType "attribute" must have a non empty item attribute | **context** `SearchItem` **inv**: `SearchItem.itemType == attribute` **implies** `SearchItem.item != null`| +| If button Year is clicked and the searchbar asks for input maximum 4 digits are allowed after "year:"| **context** `DropDownMenu.YearButton` **post**: `GlobalSearchBar:searchField.insertText().maximum = 9999`|t**: `GlobalSearchBar:searchField.insertText().maximum = 9999`| \ No newline at end of file diff --git a/docs/sweng/project-repo/zbobby/functional-specification-document.md b/docs/sweng/project-repo/zbobby/functional-specification-document.md new file mode 100644 index 00000000000..c5544792d7d --- /dev/null +++ b/docs/sweng/project-repo/zbobby/functional-specification-document.md @@ -0,0 +1,109 @@ +# Functional Specification Document + +## 1. Introduction + +### 1.1 Purpose + + + +### 1.2 Scope + + + +### 1.3 Definitions + + + +### 1.4 References + + + +### 1.5 Overview + + + +## 2. General Description + +### 2.1 Implementation + + + +### 2.2 Function Overview + + + +### 2.3 User Requirements + + + +### 2.4 Limitations + + + +### 2.5 Assumptions and Dependencies + + + +### 3. Functional Requirements + + +- /F60/ Recent Search +- /F61/ Clickable item in global searchbar. +- /F62/ Once clicked, opens dropdown menu. +- /F63/ Dropdown menu contains a list of last search queries. +- /F64/ Selecting a querry performs a search with the selected querry. +- /F64/ Selecting a querry is possible by left clicking on it or by navigating with the up/down keys and then selecting with enter. + + +### 4. Acceptance Criteria + + + +# Appendix + +## Appendix A. Use-cases + + +### Use Case 1: + + +### Use Case 2: +* Name: Make a search +* Stakeholders: Users of JabRef +* Preconditions: JabRef must be started +* Procedure: + * Click on the global search bar. + * Choose the fields you would like to search in (i.e. author, title etc.). + * Click on author field (button or text) in the interface. + * Type in the author "Beat Schmutz" into the global search bar and press ENTER. + * Click on the OR field in the interface. + * Click on the title field in the interface. + * Type in "Morphological analysis" and press ENTER. + * Click on an entry match in the main library pane. +* Postconditions: + * After selection of the author field an author field shows up in the global search bar. The cursor will be positioned to the right of this field. + * After typing in the author and selecting the OR field the position of the cursor is to the right again. + * After selecting the title field the cursor position changes to the right of this field once again. Typing in the title and pressing ENTER starts the search query which will be parsed with Lucene and passed on to the complex search query. The matched entries will show up in the main library pane. After clicking on an entry Metadata will show up; "Beat Schmutz" and "Morphological analysis" will be highlighted. +* Postconditions exception: The search finds no matching entries in the library. in this case the library pane stays empty or a pop-up could suggest that no entries were found. + +## Appendix A. Open Questions +- AND/OR buttons? +- \ No newline at end of file diff --git a/docs/sweng/project-repo/zbobby/plantuml/sequenceDiagramRecentSearch.txt b/docs/sweng/project-repo/zbobby/plantuml/sequenceDiagramRecentSearch.txt new file mode 100644 index 00000000000..956c3bf8daa --- /dev/null +++ b/docs/sweng/project-repo/zbobby/plantuml/sequenceDiagramRecentSearch.txt @@ -0,0 +1,7 @@ +@startuml +actor User +User -> GlobalSearchBar: performSearch() +GlobalSearchBar -> RecentSearch: addQueryToRecentSearches +User -> RecentSearch: clickOnRecentSearchEntry +RecentSearch -> GlobalSearchBar: performSearch() +@enduml \ No newline at end of file diff --git a/docs/sweng/project-repo/zbobby/zbobby.md b/docs/sweng/project-repo/zbobby/zbobby.md new file mode 100644 index 00000000000..a6efc217302 --- /dev/null +++ b/docs/sweng/project-repo/zbobby/zbobby.md @@ -0,0 +1,31 @@ +# Week 1 notes + +Local Library search uses Reg Ex and not Lucene. Web search uses Lucene Syntax. +## package: logic/search + +- ### class: **DatabaseSearcher** + - Only used in JabRef **cli** (?) + - Class that handles local search. + + - Initialize with a "**Query**" and "**database**" + ```java + public DatabaseSearcher(SearchQuery query, BibDatabase database){...} + ``` + - Method getMatches() finds matching entries in database + - See **SearchBasedFetcher** class for web based search with Lucene + +- ### class SearchQuery + - isMatch method to check if a given entry is match to the Query + - Matches displayed in GUI search + + ## pacakge: gui/search + +- ### class: **GlobalSearchBar** + - Implements GUI of Global Search Bar (Search bar used for local library search) + - **import error (need to fix)** + - + +# Java FX +## ToggleButton +.setSelected(...) +.setTooltip(...) diff --git a/docs/sweng/project-repo/zdani/testplan.md b/docs/sweng/project-repo/zdani/testplan.md new file mode 100644 index 00000000000..1d2605e9301 --- /dev/null +++ b/docs/sweng/project-repo/zdani/testplan.md @@ -0,0 +1,124 @@ +--- +layout: default +title : Woche 6 +--- +# Testplan template + +| Version | Projectname | Authors | Status | Date | Comment | +| ------- | ----- | ------ | ----- |------------| --------- | +| 0.1 | Implement better search | Julian Bopp, Albert Jasari, Daniel Madoery, Mark Starzynski | | 08.12.2021 | | + + +# 1. Introduction + +- The open-sourced citation and reference management software JabRef is in need of a more functional global search and a user-friendly interface thereof. +- The users of JabRef should be able to conveniently search in their citations library and perform complex search querys, also via GUI. +- The goal of this project is to develop an implementation that improves upon the current library search capabilities and enables users of the JabRef software to very easily navigate their libraries. + +## 1.1 Purpose + +- The test plan includes features about the management software JabRef which are tested in their function and the features are explained. +- In addition, a system overview with the components to be tested is listed, so you can get an overview of the functionality contained in JabRef (for example its complex query search). +- You can examine thereby its hardware and software requirements as well as modules. The functions of the methods are represented by automated unit tests. +- In this test plan you can find all important information about the purpose and use of the individual functions that merge into a unified product - better search. + +## 1.2 References + +- Technical Specification Document: [technical-documentation.md](https://github.com/josphstar/jabref/blob/designdoc/docs/sweng/technical-documentation.md) +- Functional Specification Document: [functional-specification-document.md](https://github.com/josphstar/jabref/blob/designdoc/docs/sweng/functional-specification-document.md) +- [Issue #341](https://github.com/koppor/jabref/issues/341) + +## 2. Systemoverview + +- The class diagram shows the dependency and the respective affiliations of the listed classes and methods. A insight into the functionality which is checked with unit tests can be seen in the system overview. +- The components to be tested are the methods in RecentSearch, the different Buttons in the DropDownMenu and its methods, the methods in SearchItem, ItemType, Highlighting and Autocomplete. + +![Class Diagram](/w3-UML/out/w3-UML/better-search/class-diagram.png "class-diagram.png") + +- Further information on the tests can be found from point 4 onwards. + +## 3. Features + +### 3.1 To be tested features (Features / Funktionen) + +#### DropDownMenu + +- GUI visible +- Buttons working on mouse clicked + +#### Recent Search + +- Recent search removes duplicates + +#### SearchFieldSynchronizer + +- Get search string +- Search string builder +- Search item list +- Update search item list +- Is previous attribute +- Is previous operator +- Return latest really returns latest +- Add item does not create invalid search +- Searchbar highlighting works +- Searchstring builder builds mixed strings correctly +- Update Search Itemlist + +#### Autocomplete + +- Autocomplete creates recommendations in searchbar/list + +#### 3.1.1 Functional Requirements + +- The tested functions can be found in this list under point 3. "Specific requirements" from the [functional specification.md](https://github.com/josphstar/jabref/blob/designdoc/docs/sweng/functional-specification-document.md). + + +### 3.2 Not to be tested features (Features / Functions) + +- Funktion tests of the GUI are not tested. + +## 4 Proceeding + +### 4.1 Components and Integration Tests + +- The tests are performed by unit tests. The modules are executed in a separate test environment. + +### 4.2 Functional Tests + +- The tests are performed by unit tests in which we check the implementation of the respective methods and functions (tests autocompletion in GlobalSearchbar/Dropdown). + The modules are executed in a separate test environment. + +### 5 Hardware and Software Requirements + +- There are no special hardware or software requirements needed. + +## 6 Testcases + +### 6.1 Modultests + +| Name of the Class | Name of the Testcase | Status | +| ------- |----------------------------------------| -----| +| SearchFieldSynchronizer | returnLatestReallyReturnsLatest | | +| SearchFieldSynchronizer | addItemDoesNotCreateInvalidSearch | | +| SearchFieldSynchronizer | SearchBarHighlightingWorks | | +| SearchFieldSynchronizer | SearchStringBuilderBuildsMixedStringCorrectly | | +| SearchFieldSynchronizer | testGetSearchString | | +| SearchFieldSynchronizer | testSearchItemList | | +| SearchFieldSynchronizer | testIsPrevAttribute | | +| SearchFieldSynchronizer | testIsPrevOperator | | +| RecentSearch | RecentSearchRemovesDuplicates | | + + + +### 6.2 Funktionstests + +| Name of the Class | Name of the Testcase | Status | +| ------- | ----- | ----- | +| SearchFieldSynchronizer | testSearchStringBuilder | | +| SearchFieldSynchronizer | testUpdateSearchItemList | | +| GlobalSearchBar | testEntryInDropdownSearchbar | | +| GlobalSearchBar | completeWithoutAddingAnythingReturnsSomething | | +| GlobalSearchBar | completeReturnsMultipleResultsInDropdown | | +| DropDownMenu | testButtonWorking | | +| DropDownMenu | testDropDownShowing | | +| DropDownMenu | testDropDownNotShowing | | \ No newline at end of file diff --git a/docs/sweng/project-repo/zdani/zdani.md b/docs/sweng/project-repo/zdani/zdani.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/sweng/project-repo/zmark/_how-we-work.md b/docs/sweng/project-repo/zmark/_how-we-work.md new file mode 100644 index 00000000000..3fef697dab6 --- /dev/null +++ b/docs/sweng/project-repo/zmark/_how-we-work.md @@ -0,0 +1,36 @@ +# How We Work ("work in progress") + +### Before +- Check for new commits in repos "SE_PROJECT_FILES" and "josphstar/jabref" +- `git pull origin main` +- `git pull upstream main` + - Alternatively (more control): + - `git fetch upstream main` + - `git merge upstream/main` +- `git checkout WORKING_BRANCH` + +### During +- Commit often and after every specific task + - Commit msg example: `"Add search feature"` + - Capitalization at the start + - Imperative mood (Add, Merge, Update...) + - [Complete Guidelines](https://chris.beams.io/posts/git-commit/) +- How to commit: + - Commit often and after every specific task + - Before pushing your commits: + - `git pull origin main` + - Resolve any occuring merge conflicts + - `git push origin BRANCH` + +### Code Reviews +- Make notes about the code you are studying and push everything to "SE_PROJECT_FILES" +- Regular code review meetings where team members explain their specific feature to the other members + +### Git Branch Structure +- main + - develop + - designdoc + - lucene_syntax + - gui + - searchbar + - ... \ No newline at end of file diff --git a/docs/sweng/project-repo/zmark/_issues.md b/docs/sweng/project-repo/zmark/_issues.md new file mode 100644 index 00000000000..00e0fcbd809 --- /dev/null +++ b/docs/sweng/project-repo/zmark/_issues.md @@ -0,0 +1,18 @@ +# Issues +##### OPEN +- [Search for type does not work in biblatex mode #193](https://github.com/koppor/jabref/issues/193) + - type=book and title=management: no hit + - publisher=springer and year=2006 and title=management: one hit +- [Improve Search Interface](https://github.com/JabRef/jabref/issues/7423) +- [Add ChipView for web search interface #7806](https://github.com/JabRef/jabref/issues/7806) +- [Search highlighting broken at complex searches #8067](https://github.com/JabRef/jabref/issues/8067) + - search title=TALEN and abstract=plant, the and will be highlighted, but the TALEN and plant do not. +- [Add support for Lucene as search syntax #8206](https://github.com/JabRef/jabref/pull/8206) + - reg ex button in search bar should be removed. all regex searches should be handled by lucene based search. + - search rule describer for Lucene has to be created. Currently only a dummy. + - think of removing the search grammar completely ???? +- + +##### MERGED +- [Feature/enable lucene query parsing #6799](https://github.com/JabRef/jabref/pull/6799) +- [Feature/add ui for query parsing #6805](https://github.com/JabRef/jabref/pull/6805) \ No newline at end of file diff --git a/docs/sweng/project-repo/zmark/_links.md b/docs/sweng/project-repo/zmark/_links.md new file mode 100644 index 00000000000..e52a7d14c11 --- /dev/null +++ b/docs/sweng/project-repo/zmark/_links.md @@ -0,0 +1,14 @@ +# Link Resources + +## General + + +## W1 +- [w1 übersicht](https://unibas-marcelluethi.github.io/software-engineering/project/week6/exercises.html) +- [projektübersicht](https://unibas-marcelluethi.github.io/software-engineering/project/project-summary.html) +- +### Functional Specification Document +- [english example](https://s-pro.io/static/pdf/specification-example.pdf) +- [quick guide](https://www.justinmind.com/blog/functional-specification-documentation-quick-guide-to-making-your-own/) +- [acceptance criteria best practices](https://www.altexsoft.com/blog/business/acceptance-criteria-purposes-formats-and-best-practices/) +- \ No newline at end of file diff --git a/docs/sweng/project-repo/zmark/_organisation.md b/docs/sweng/project-repo/zmark/_organisation.md new file mode 100644 index 00000000000..b6e62cdf533 --- /dev/null +++ b/docs/sweng/project-repo/zmark/_organisation.md @@ -0,0 +1,14 @@ +# Currently OPEN QUESTIONS +- + + +# DEADLINES (immer 23:55) +- Prototyp/Design/Technische Dokumentation + - am 15. November --> 24. November +- Testen + - am 22. November --> 1. Dezember +- Präsentation 10min / Demo + - am 15. Dezember +- Finale Abgabe + - am 22. Dezember + diff --git a/docs/sweng/project-repo/zmark/_project notes.md b/docs/sweng/project-repo/zmark/_project notes.md new file mode 100644 index 00000000000..d17e112a035 --- /dev/null +++ b/docs/sweng/project-repo/zmark/_project notes.md @@ -0,0 +1,47 @@ +# Packages and Classes + +- org\jabref\logic\importer\ + - fetcher\ComplexSearchQuery.java + - fetcher\CompositeSearchBasedFetcher.java + - SearchBasedFetcher.java + - SearchBasedParserFetcher.java + +- org\jabref\logic\search + - DatabaseSearcher.java + - SearchQuery.java +- org\jabref\gui\search + - rules.describer + - GlobalSearchBar.java + - GlobalSearchResultDialog.java + - GlobalSearchResultDialogViewModel.java + - RebuildFulltextSearchIndexAction.java + - SearchDisplayMode.java + - SearchResultsTable.java + - SearchResultsTableDataModel.java + - SearchTextField.java +- org\jabref\model\search + - matchers + - rules + - GroupSearchQuery.java + - SearchMatcher.java +- logic\pdf\search\ ??? + + +# Lucene Notes +- [Description](https://lucene.apache.org/core/2_9_4/queryparsersyntax.html) Gute Beschreibung für benutzung von Lucene +- AddDoc() --> AddDocuments() +- Searching needs an already built index; ... + +# PopOver usage and JavaDoc +- ord.jabref.gui.JabrefFrame +- Zeile: 174, 949-959 +- method: createTaskIndicator +- [JavaDoc](http://javadox.com/org.controlsfx/controlsfx/8.0.5/org/controlsfx/control/PopOver.html) + +# GlobalSearchbar +- org.jabref.gui.JabrefFrame +- initialisation: 160, 184 +- case search: 248 +- toolbar: 487 +- method getGlobalSearchbar: 1181 +- org jabref.gui.search.GlobalSearchBar diff --git a/docs/sweng/project-repo/zmark/func-spec-doc_Mark.md b/docs/sweng/project-repo/zmark/func-spec-doc_Mark.md new file mode 100644 index 00000000000..9a5ef32b37d --- /dev/null +++ b/docs/sweng/project-repo/zmark/func-spec-doc_Mark.md @@ -0,0 +1,100 @@ +# Functional Specification Document "Better Search" Feature for JabRef + +## 1. Introduction + +### 1.1 Vision and Goals + +- The open-sourced citation and reference managment software JabRef is in need of a more functional global search and a user-friendly interface thereof. +- The users of JabRef should be able to conveniently search in their citations library and perform complex search querys, also via GUI. +- The goal of this project is to develop an implementation that improves upon the current library search capabilities and enables users of the JabRef software to very easily navigate their libraries. + +### 1.2 Scope Conditions + +- "Better Search" is a feature implementation for the citation and reference management software JabRef. +- Target group of the JabRef software are researchers, PHD candidates, university members, students and the scientific community in general. +- An internet conneciton is not needed for this feature. +- Search queries in global search must have the same syntax as the already implemented web search. +- Search queries need to be performed reliably and without any bugs or errors. + + +### 1.3 Definitions + +- GUI: Graphical User Interface +- Cross-platform: +- Dropdown menu: +- Search queries: + + +### 1.4 Overview + +- The search strings in the global search bar is parsed to an existing QueryParser. +- The search user interface is a dropdown menu. + +## 2. General Description + +### 2.1 Implementation + + + +### 2.2 Function Overview + + + +### 2.3 User Requirements + + + +### 2.4 Limitations + + + +### 2.5 Assumptions and Dependencies + + + +### 3. Functional Requirements + + + +### 4. Acceptance Criteria + + + +# Appendix + +## Appendix A. Use-cases + + +### Use Case 1: +* Name: Click on global search bar +* Stakeholders: Users of JabRef +* Preconditions: JabRef must be started +* Procedure: + * Click on the global search bar +* Postconditions: Dropdown menu shows up below the search bar with suggestions for syntax use. Last seen searches are listed. +* Postconditions exception: Dropdown menu shows up below the search bar with suggestions for syntax use. No prior searches show empty "Last searches" list + +### Use Case 2: + + +## Appendix A. Open Questions +- AND/OR buttons? +- \ No newline at end of file diff --git a/docs/sweng/project-repo/zmark/lucene-syntax-feature.md b/docs/sweng/project-repo/zmark/lucene-syntax-feature.md new file mode 100644 index 00000000000..3ab6a87cb84 --- /dev/null +++ b/docs/sweng/project-repo/zmark/lucene-syntax-feature.md @@ -0,0 +1,36 @@ +# Lucene Syntax Feature + +* All terms in the query are whitespace separated and will be ORed +* Default and certain fielded terms are supported +* Fielded Terms: + * `author` + * `title` + * `journal` + * `year` (for single year) + * `year-range` (for range e.g. `year-range:2012-2015`) +* The `journal`, `year`, and `year-range` fields should only be populated once in each query +* Example: + * `author:"Igor Steinmacher" author:"Christoph Treude" year:2017` will be converted to + * `author:"Igor Steinmacher" AND author:"Christoph Treude" AND year:2017` + + +### QueryParser +- Converts query string written in lucene syntax into a complex search query + +- OPENQUESTION: Should AND or OR be implemented as standard? + +- new: [Add support for Lucene as search syntax #8206](https://github.com/JabRef/jabref/pull/8206) + - reg ex button in search bar should be removed. all regex searches should be handled by lucene based search. + - search rule describer for Lucene has to be created. Currently only a dummy. + - think of removing the search grammar completely + + +### CODE: LUCENE QUERY SYNTAX IMPLEMENTATION +- [lucene](https://lucene.apache.org/core/8_6_1/queryparser/org/apache/lucene/queryparser/classic/package-summary.html) +- [query syntax](http://www.lucenetutorial.com/lucene-query-syntax.html) +- [query syntax 2](https://lucene.apache.org/core/2_9_4/queryparsersyntax.html) +- [#6799](https://github.com/JabRef/jabref/pull/6799/files) + - [AST: abstract syntax tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree) +- class [QueryParser](https://lucene.apache.org/core/4_0_0/queryparser/org/apache/lucene/queryparser/classic/QueryParser.html) is used to convert a lucene search input (in web search i.e.) to a complex query, which can be then used to fetch the results. it returns a complexSearchQuery object. + - use this class to apply to lib search +- requested OR query in case of empty spaces in between search terms is already implemented \ No newline at end of file diff --git a/docs/sweng/project-repo/zmark/new-repo.md b/docs/sweng/project-repo/zmark/new-repo.md new file mode 100644 index 00000000000..7db45d6231f --- /dev/null +++ b/docs/sweng/project-repo/zmark/new-repo.md @@ -0,0 +1,10 @@ +# Switch to our new repo + +1. Rename your old local repo directory to jabref_backup +2. Clone new repo with + - HTTPS: `git clone https://github.com/josphstar/jabref.git` + - SSH: `git clone git@github.com:josphstar/jabref.git` +3. Backup the .idea folder somewhere else outside the repo directory +4. Delete the .idea folder from the new repo and copy the .idea folder from the old repo to the new one. +5. Start IntelliJ with the build.gradle in the new repo +6. `git remote add upstream git@github.com:JabRef/jabref.git` diff --git a/docs/sweng/project-repo/zmark/plantUML-in-VSCode.md b/docs/sweng/project-repo/zmark/plantUML-in-VSCode.md new file mode 100644 index 00000000000..2b542b6159e --- /dev/null +++ b/docs/sweng/project-repo/zmark/plantUML-in-VSCode.md @@ -0,0 +1,6 @@ +# How to install use PlantUML in VSCode + +- [Graphviz](http://www.graphviz.org/download/) +- [PlantUML Extension](https://marketplace.visualstudio.com/items?itemName=jebbs.plantuml) +- [PlantUML Reviewer Extension](https://marketplace.visualstudio.com/items?itemName=Mebrahtom.plantumlpreviewer) + - Context menu or Alt+D diff --git a/docs/sweng/project-repo/zmark/vscode_extensions.md b/docs/sweng/project-repo/zmark/vscode_extensions.md new file mode 100644 index 00000000000..0110477ad8c --- /dev/null +++ b/docs/sweng/project-repo/zmark/vscode_extensions.md @@ -0,0 +1,24 @@ +# VS Code Extensions +- [Markdown All in One](https://marketplace.visualstudio.com/items?itemName=yzhang.markdown-all-in-one) + - i.e. for autocomplete list, tables, table of contents etc +- [Markdown Print](https://marketplace.visualstudio.com/items?itemName=pdconsec.vscode-print) +- [Markdown Shortcuts](https://marketplace.visualstudio.com/items?itemName=mdickin.markdown-shortcuts) +- [Preview](https://marketplace.visualstudio.com/items?itemName=searKing.preview-vscode) + - Ctrl+Shift+v +- [Sync Scroll](https://marketplace.visualstudio.com/items?itemName=dqisme.sync-scroll) + - allows for syncing windows when scrolling, i.e. markdown code window and preview window +- [Avanced New File](https://marketplace.visualstudio.com/items?itemName=dkundel.vscode-new-file) +- [Debugger for Java](https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-debug) +- [Extension Pack for Java](https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-pack) +- [Git Graph](https://marketplace.visualstudio.com/items?itemName=mhutchie.git-graph) +- [Git Indicators](https://marketplace.visualstudio.com/items?itemName=lamartire.git-indicators) +- [Github Remote Repositories](https://marketplace.visualstudio.com/items?itemName=GitHub.remotehub) +- [GitLink +](https://marketplace.visualstudio.com/items?itemName=qezhu.gitlink) +- [Jupyter](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter) +- [LaTeX Workshop](https://marketplace.visualstudio.com/items?itemName=James-Yu.latex-workshop) +- [Make Hidden](https://marketplace.visualstudio.com/items?itemName=devine-davies.make-hidden) +- [Material Icon Theme](https://marketplace.visualstudio.com/items?itemName=PKief.material-icon-theme) +- [Path IntelliSense](https://marketplace.visualstudio.com/items?itemName=christian-kohler.path-intellisense) +- [Python](https://marketplace.visualstudio.com/items?itemName=ms-python.python) +- [Rewrap](https://marketplace.visualstudio.com/items?itemName=stkb.rewrap) \ No newline at end of file diff --git a/docs/sweng/project-repo/zmark/zmark.md b/docs/sweng/project-repo/zmark/zmark.md new file mode 100644 index 00000000000..f4550392faf --- /dev/null +++ b/docs/sweng/project-repo/zmark/zmark.md @@ -0,0 +1,34 @@ +# Required Features / Functional Overview + +### MINOR BUGFIX +- possible use case scenario + - [Is it really a problem if the user searches for an empty string, e.g. author = ""?](https://github.com/JabRef/jabref/pull/6687/files#r464082845) + - ...(We should keep this in the back of our heads when the user interface is implemented. There you would like to show a helping message instead of an error dialog). + +### CODE: LUCENE QUERY SYNTAX IMPLEMENTATION +- [lucene](https://lucene.apache.org/core/8_6_1/queryparser/org/apache/lucene/queryparser/classic/package-summary.html) +- [query syntax](http://www.lucenetutorial.com/lucene-query-syntax.html) +- [query syntax 2](https://lucene.apache.org/core/2_9_4/queryparsersyntax.html) +- [#6799](https://github.com/JabRef/jabref/pull/6799/files) + - [AST: abstract syntax tree](https://en.wikipedia.org/wiki/Abstract_syntax_tree) +- class [QueryParser](https://lucene.apache.org/core/4_0_0/queryparser/org/apache/lucene/queryparser/classic/QueryParser.html) is used to convert a lucene search input (in web search i.e.) to a complex query, which can be then used to fetch the results. it returns a complexSearchQuery object. + - use this class to apply to lib search +- requested OR query in case of empty spaces in between search terms is already implemented + +### CODE: GUI +- hint box adjustments +- red/green syntax indicator [#6805](https://github.com/JabRef/jabref/pull/6805/files) +- search interface example in [#7423](https://github.com/JabRef/jabref/issues/7423) +- + + + + + + +# Appendix + +### OPEN QUESTIONS +- should green/red highlighting as seen in web search be implemented into lib search? ([#6805](https://github.com/JabRef/jabref/pull/6805))? +- is chipview a requirement ([7806](https://github.com/JabRef/jabref/issues/7806))? +- \ No newline at end of file diff --git a/docs/sweng/technical-documentation.md b/docs/sweng/technical-documentation.md new file mode 100644 index 00000000000..8a848335053 --- /dev/null +++ b/docs/sweng/technical-documentation.md @@ -0,0 +1,114 @@ +# Technical Documentation + +| Project Overview | | | | | | +| :---: | :---: | :---: | :---: | :---: | :---: | +| Version | Project Name | Authors | Status | Date | Comment | +| 0.1 | Better Search | MS, DM, JB, AJ | In Progress | 2021-11-20 | | + +| Project Members | | | | +| :---: | :---: | :---: | :---: | +| Julian Bopp | Albert Jasari | Daniel Madoery | Mark Starzynski + + + +## 1 Introduction + +### 1.1 Purpose +- The goal of this document is to provide a good overview of the project that explains the use, functionality, creation and architecture of the product. It aims to function as a guide for its current and future developers. +kv +### 1.2 References +- Functional Specification Document: [functional-specification-document.md](https://github.com/josphstar/jabref/blob/designdoc/docs/sweng/functional-specification-document.md) +- Issue [#341](https://github.com/koppor/jabref/issues/341) + +## 2 System Overview + +The project goal is to implement an extension for the open-sourced citation and reference management software JabRef. The goal of this extension is to improve upon the existing global search function and make it more user-friendly by adding a dropdown menu with additional search functionalities. + +## 3 Design Goals + +For the design of the extension, at its core a GUI implementation of a dropdown menu, future extensability will be ensured because JabRef already has dropdown menus implemented with JavaFX. Those will be re-used, which will also satisfy low development costs and good performance. + +## 4 System Performance + +The extension implements a dropdown menu for the existing global search bar. This dropdown menu will consist of clickable buttons which allows the user to search for specific fields (i.e. author, title) without the need of having to type in a complex search syntax. Furthermore the implemented dropdown menu has the option to browse through and re-search the most recent search queries. + +## 5 Design + + + +### 5.1 Statics + +"Better Search" implementation will mainly evolve around the SeearchFieldSynchronizer class, which task it will be to keep user inputs up-to-date with the live search query and ensure proper syntax. + +For that an ArrayList will be kept and updated in accordance with the - by the user via search field or dropdown menu - intended live search query. + +RecentSearch will keep track of previously searched for important attributes - mainly authors and titles - in separate lists and also display them in the dropdown menu at the end. + +![Class Diagram](/docs/sweng/diagrams/class-diagram-withLegend.png "class-diagram.png") + +### 5.2 Dynamics + +#### 5.2.1 User Search Input +With the implementation of a search dropdown menu the user can input information for search queries mainly in two ways: As usual via search field or - additionaly - via newly implemented dropdown menu. + +Handling synchronization between user interaction and proper display of the complete search string will be the class SearchFieldSynchronizer. +- When interacting with the dropdown menu it will check and add new items (attributes, logical operators, brackets) to the ArrayList searchItemList and build a full search string to update the search field. +- When typing directly into the search field it will fetch changes or additions to the ArrayList searchItemList and update the search field accordingly. + +![User Search Input](/docs/sweng/diagrams/user-search-input.png "user-search-input.png") + +#### 5.2.2 Recent Search + +The Recent Search functionality shows the user a record of past searches inside the dropdown menu. + +- When performSearch() is called, the query gets added to the RecentSearches List. +- Clicking on an entry in Recent Search performs a corresponding search. +- The List is ordered from bottom to top. + +![Recent Search](/docs/sweng/diagrams/recent-search.png "recent-search.png") + +#### 5.2.3 ChipView + +The ChipView transforms coherent search strings into a single chip. + +- Pressing a button in DropDownMenu creates a new chip. +- The resulting chip can then be filled with a search query. + +![ChipView](/docs/sweng/diagrams/chip-view.png "chip-view.png") + +#### 5.2.4 Autocomplete + +Autocomplete takes care of the automatically generated completion in the GlobalSearchBar which is done by the bindAutoCompletion() method in the AutoCompletePopup class. +The recommended strings are linked together and can thus be accepted or ignored by the user. +The input is tracked as far as possible. + +- A pop-up menu appears with all the recommended entries which you can click on. + +![Autocomplete](/docs/sweng/diagrams/autocomplete.png "autocomplete.png") + +### 5.3 Logic + +#### 5.3.1 Logic Operators +The buttons in the dropdown menu have to function differently under certain conditions. I.e. it is preferred to not allow two logical operators in a row without an attribute in between or to not allow adding search items while an attribute search string is still empty. + +Furthermore a rudimentary weighted logic operating option is depicted in the following activity diagram. A logic operator can be weighted normally (1) or weaker (-1) once, which allows for a little more complexity in the drop down menu. + +![Logic Operators](/docs/sweng/diagrams/logic-operators.png "logic-operators.png") + +#### 5.3.2 OCL Constraints + +| Constraint | OCL Constraint | +|---|---| +| The Search Query is not empty | **context** `GlobalSearchBar:performSearch()` **inv**: `self.SearchField != null` | +| After performing a search, the search query gets added to the recent search list| **context** `GlobalSearchBar:performSearch()` **post**: `RecentSearch.contains(GlobalSearchBar.searchField)`| +| AutoCompleter is reading searchfield and proposes different endings | **context** `GlobalSearchBar:AutoComplete.readCurrent(searchField)` **inv**: `AutoComplete.popup()`| +| If the GlobalSearchBar gets clicked the DropDownMenu will be activated | **context** `GlobalSearchBar:searchField.Mousevent.MOUS_CLICKED` **inv**: `GlobalSearchBar.dropDownMenu.show(searchField)` | +| If some button "Example" in the DropDownMenu is clicked the Searchbar gets updated with the input "example:"| **context** `DropDownMenu.ExampleButton` **inv**: `GlobalSearchBar.searchField.insertText()`| +| Only one "-1" logical operator is allowed | **context** `SearchFieldSynchronizer::SearchItemList` **inv**: `self.amountMinusOneLogical <= 1`| +| SearchItem object with itemType "attribute" must have a non empty item attribute | **context** `SearchItem` **inv**: `SearchItem.itemType == attribute` **implies** `SearchItem.item != null`| +| If button Year is clicked and the searchbar asks for input maximum 4 digits are allowed after "year:"| **context** `DropDownMenu.YearButton` **post**: `GlobalSearchBar:searchField.insertText().maximum = 9999`| diff --git a/docs/sweng/testplan.md b/docs/sweng/testplan.md new file mode 100644 index 00000000000..f7e2f31f102 --- /dev/null +++ b/docs/sweng/testplan.md @@ -0,0 +1,124 @@ +--- +layout: default +title : Woche 6 +--- +# Testplan template + +| Version | Projectname | Authors | Status | Date | Comment | +| ------- | ----- | ------ | ----- |------------| --------- | +| 0.1 | Implement better search | Julian Bopp, Albert Jasari, Daniel Madoery, Mark Starzynski | | 08.12.2021 | | + + +# 1. Introduction + +- The open-sourced citation and reference management software JabRef is in need of a more functional global search and a user-friendly interface thereof. +- The users of JabRef should be able to conveniently search in their citations library and perform complex search querys, also via GUI. +- The goal of this project is to develop an implementation that improves upon the current library search capabilities and enables users of the JabRef software to very easily navigate their libraries. + +## 1.1 Purpose + +- The test plan includes features about the management software JabRef which are tested in their function and the features are explained. +- In addition, a system overview with the components to be tested is listed, so you can get an overview of the functionality contained in JabRef (for example its complex query search). +- You can examine thereby its hardware and software requirements as well as modules. The functions of the methods are represented by automated unit tests. +- In this test plan you can find all important information about the purpose and use of the individual functions that merge into a unified product - better search. + +## 1.2 References + +- Technical Specification Document: [technical-documentation.md](https://github.com/josphstar/jabref/blob/designdoc/docs/sweng/technical-documentation.md) +- Functional Specification Document: [functional-specification-document.md](https://github.com/josphstar/jabref/blob/designdoc/docs/sweng/functional-specification-document.md) +- [Issue #341](https://github.com/koppor/jabref/issues/341) + +## 2. Systemoverview + +- The class diagram shows the dependency and the respective affiliations of the listed classes and methods. A insight into the functionality which is checked with unit tests can be seen in the system overview. +- The components to be tested are the methods in RecentSearch, the different Buttons in the DropDownMenu and its methods, the methods in SearchItem, ItemType, Highlighting and Autocomplete. + +![Class Diagram](/w3-UML/out/w3-UML/better-search/class-diagram.png "class-diagram.png") + +- Further information on the tests can be found from point 4 onwards. + +## 3. Features + +### 3.1 To be tested features (Features / Funktionen) + +#### DropDownMenu + +- GUI visible +- Buttons working on mouse clicked + +#### Recent Search + +- Recent search removes duplicates + +#### SearchFieldSynchronizer + +- Get search string +- Search string builder +- Search item list +- Update search item list +- Is previous attribute +- Is previous operator +- Return latest really returns latest +- Add item does not create invalid search +- Searchbar highlighting works +- Searchstring builder builds mixed strings correctly +- Update Search Itemlist + +#### Autocomplete + +- Autocomplete creates recommendations in searchbar/list + +#### 3.1.1 Functional Requirements + +- The tested functions can be found in this list under point 3. "Specific requirements" from the [functional specification.md](https://github.com/josphstar/jabref/blob/designdoc/docs/sweng/functional-specification-document.md). + + +### 3.2 Not to be tested features (Features / Functions) + +- Funktion tests of the GUI are not tested. + +## 4 Proceeding + +### 4.1 Components and Integration Tests + +- The tests are performed by unit tests. The modules are executed in a separate test environment. + +### 4.2 Functional Tests + +- The tests are performed by unit tests in which we check the implementation of the respective methods and functions (tests autocompletion in GlobalSearchbar/Dropdown). + The modules are executed in a separate test environment. + +### 5 Hardware and Software Requirements + +- There are no special hardware or software requirements needed. + +## 6 Testcases + +### 6.1 Modultests + +| Name of the Class | Name of the Testcase | Status | +| ------- |----------------------------------------| -----| +| SearchFieldSynchronizer | returnLatestReallyReturnsLatest | | +| SearchFieldSynchronizer | addItemDoesNotCreateInvalidSearch | | +| SearchFieldSynchronizer | SearchBarHighlightingWorks | | +| SearchFieldSynchronizer | SearchStringBuilderBuildsMixedStringCorrectly | | +| SearchFieldSynchronizer | testGetSearchString | | +| SearchFieldSynchronizer | testSearchItemList | | +| SearchFieldSynchronizer | testIsPrevAttribute | | +| SearchFieldSynchronizer | testIsPrevOperator | | +| RecentSearch | RecentSearchRemovesDuplicates | | + + + +### 6.2 Funktionstests + +| Name of the Class | Name of the Testcase | Status | +| ------- | ----- | ----- | +| SearchFieldSynchronizer | testSearchStringBuilder | | +| SearchFieldSynchronizer | testUpdateSearchItemList | | +| GlobalSearchBar | testEntryInDropdownSearchbar | | +| GlobalSearchBar | completeWithoutAddingAnythingReturnsSomething | | +| GlobalSearchBar | completeReturnsMultipleResultsInDropdown | | +| DropDownMenu | testButtonWorking | | +| DropDownMenu | testDropDownShowing | | +| DropDownMenu | testDropDownNotShowing | | diff --git a/src/main/java/org/jabref/gui/JabRefFrame.java b/src/main/java/org/jabref/gui/JabRefFrame.java index 073371ea4bf..937e1c3864c 100644 --- a/src/main/java/org/jabref/gui/JabRefFrame.java +++ b/src/main/java/org/jabref/gui/JabRefFrame.java @@ -181,6 +181,7 @@ public class JabRefFrame extends BorderPane { private final TaskExecutor taskExecutor; + public JabRefFrame(Stage mainStage) { this.mainStage = mainStage; this.dialogService = new JabRefDialogService(mainStage, this, prefs); @@ -197,6 +198,7 @@ public JabRefFrame(Stage mainStage) { } } }); + } private void initDragAndDrop() { diff --git a/src/main/java/org/jabref/gui/search/DropDownMenu.java b/src/main/java/org/jabref/gui/search/DropDownMenu.java new file mode 100644 index 00000000000..e11ca3dca60 --- /dev/null +++ b/src/main/java/org/jabref/gui/search/DropDownMenu.java @@ -0,0 +1,270 @@ +package org.jabref.gui.search; + +import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; + +import org.jabref.gui.icon.IconTheme; + +import org.controlsfx.control.PopOver; +import org.controlsfx.control.RangeSlider; +import org.controlsfx.control.textfield.CustomTextField; + +public class DropDownMenu { + public PopOver searchbarDropDown; + public Button authorButton; + public Button journalButton; + public Button titleButton; + public Button yearButton; + public Button andButton; + public Button orButton; + public Button andGroupButton; + public Button orGroupButton; + public Button leftBracketButton; + public Button rightBracketButton; + public Button deleteButton; + public Button searchStart; + public Button addString; + public RecentSearch recentSearch; + public TextField searchString; + + @SuppressWarnings("checkstyle:NoWhitespaceBefore") + public DropDownMenu(CustomTextField searchField, GlobalSearchBar globalSearchBar, SearchFieldSynchronizer searchFieldSynchronizer) { + + authorButton = new Button("Author"); + journalButton = new Button("Journal"); + titleButton = new Button("Title"); + yearButton = new Button("Year"); + andButton = new Button("AND"); + orButton = new Button("OR"); + andGroupButton = new Button("AND^-1"); + orGroupButton = new Button("OR^-1"); + leftBracketButton = new Button("("); + rightBracketButton = new Button(")"); + deleteButton = IconTheme.JabRefIcons.DELETE_ENTRY.asButton(); + searchStart = IconTheme.JabRefIcons.SEARCH.asButton(); + addString = IconTheme.JabRefIcons.ADD_ENTRY.asButton(); + searchString = new TextField(); + + Text titleLucene = new Text(" Lucene Search"); + Text titleRecent = new Text(" Recent Searches"); + recentSearch = new RecentSearch(globalSearchBar); + TextField searchString = new TextField(); + searchString.setPrefWidth(200); + + // yearRangeSlider horizontal + Text titelYearRangeSlider = new Text("Year-Range"); + final RangeSlider hSlider = new RangeSlider(1800, 2022, 10, 90); + hSlider.setShowTickMarks(true); + hSlider.setShowTickLabels(true); + hSlider.setBlockIncrement(10); + hSlider.setPrefWidth(100); + hSlider.setMajorTickUnit(100); + hSlider.setMinorTickCount(10); + hSlider.showTickMarksProperty(); + Label label = new Label(); + + HBox luceneString = new HBox(searchString, addString, searchStart, deleteButton); + HBox recentSearchBox = recentSearch.getHBox(); + HBox buttonsLucene = new HBox(2, authorButton, journalButton, titleButton, + yearButton); + HBox andOrButtons = new HBox(2, andButton, orButton, andGroupButton, orGroupButton); + HBox bracketButtons = new HBox(2, leftBracketButton, rightBracketButton); + HBox yearRangeSlider = new HBox(2, titelYearRangeSlider, hSlider, label); + + VBox mainBox = new VBox(4, titleLucene, luceneString, buttonsLucene, yearRangeSlider, andOrButtons, bracketButtons, titleRecent, recentSearchBox); + // mainBox.setMinHeight(500); + // mainBox.setMinWidth(500); + Node buttonBox = mainBox; + + searchField.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> { + if (searchbarDropDown == null || !searchbarDropDown.isShowing()) { + searchbarDropDown = new PopOver(buttonBox); + searchbarDropDown.setWidth(searchField.getWidth()); + searchbarDropDown.setArrowLocation(PopOver.ArrowLocation.TOP_CENTER); + searchbarDropDown.setContentNode(buttonBox); + searchbarDropDown.setDetachable(false); // not detachable + searchbarDropDown.show(searchField); + searchString.setFocusTraversable(false); + } + searchbarDropDown.setOnHiding(event1 -> { + recentSearch.add(searchField.getText()); + }); + }); + + // addString action + addString.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> { + String current = searchField.getText(); + String adder = searchString.getText(); + String newString = ""; + int pos = current.length() - 1; + int pos2 = current.length() - 1; + while (pos > 0) { + char ch = current.charAt(pos); + if (ch == ':') { + break; + } + pos--; + } + while (pos2 > 0) { + char cha = current.charAt(pos2); + if (cha == ')') { + break; + } + pos2--; + } + if (searchField.getText().isEmpty()) { + searchField.setText(adder); + searchField.positionCaret(searchField.getText().length()); + searchString.clear(); + } else { + if (searchFieldSynchronizer.searchItemList.get(searchFieldSynchronizer.searchItemList.size() - 1).isLogical()) { + newString = current + adder; + } else if (searchFieldSynchronizer.searchItemList.get(searchFieldSynchronizer.searchItemList.size() - 1).isRightBracket() && searchFieldSynchronizer.searchItemList.get(searchFieldSynchronizer.searchItemList.size() - 2).isLeftBracket()) { + String subi = current.substring(0, pos2); + newString = subi + adder + ")"; + } else if (searchFieldSynchronizer.searchItemList.get(searchFieldSynchronizer.searchItemList.size() - 1).isRightBracket() && searchFieldSynchronizer.searchItemList.get(searchFieldSynchronizer.searchItemList.size() - 2).isLogical()) { + String subs = current.substring(0, pos2); + newString = subs + adder + ")"; + } else if (searchFieldSynchronizer.searchItemList.get(searchFieldSynchronizer.searchItemList.size() - 1).isQuery() && pos == 0 && !searchString.getText().isEmpty()) { + newString = current + " " + adder; + } else if (searchFieldSynchronizer.searchItemList.get(searchFieldSynchronizer.searchItemList.size() - 1).isRightBracket() && searchFieldSynchronizer.searchItemList.get(searchFieldSynchronizer.searchItemList.size() - 2).isQuery() && !searchFieldSynchronizer.searchItemList.get(searchFieldSynchronizer.searchItemList.size() - 3).isAttribute()) { + String subs = current.substring(0, pos2); + newString = subs + " " + adder + ")"; + } else if (searchFieldSynchronizer.searchItemList.get(searchFieldSynchronizer.searchItemList.size() - 1).isAttribute()) { + newString = current + adder; + } else if (searchFieldSynchronizer.searchItemList.get(searchFieldSynchronizer.searchItemList.size() - 1).isRightBracket() && searchFieldSynchronizer.searchItemList.get(searchFieldSynchronizer.searchItemList.size() - 2).isAttribute()) { + String subidu = current.substring(0, pos2); + newString = subidu + adder + ")"; + } else if (searchFieldSynchronizer.searchItemList.get(searchFieldSynchronizer.searchItemList.size() - 1).isRightBracket() && searchFieldSynchronizer.searchItemList.get(searchFieldSynchronizer.searchItemList.size() - 2).isQuery() && searchFieldSynchronizer.searchItemList.get(searchFieldSynchronizer.searchItemList.size() - 3).isAttribute()) { + String sub = current.substring(0, pos + 1); + newString = sub + adder + ")"; + } else if (searchFieldSynchronizer.searchItemList.get(searchFieldSynchronizer.searchItemList.size() - 1).isQuery() && searchFieldSynchronizer.searchItemList.get(searchFieldSynchronizer.searchItemList.size() - 2).isAttribute()) { + String nsub = current.substring(0, pos + 1); + newString = nsub + adder; + } + searchField.setText(newString); + searchField.positionCaret(searchField.getText().length()); + searchString.clear(); + } + searchString.setFocusTraversable(false); + }); + + // searchStart action + searchStart.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> { + globalSearchBar.focus(); + globalSearchBar.performSearch(); + searchbarDropDown.hide(); + }); + + // deleteButton action + deleteButton.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> { + searchField.clear(); + searchString.clear(); + searchFieldSynchronizer.deleteAllEntries(); + searchString.setFocusTraversable(false); + }); + + // authorButton action + authorButton.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> { + searchFieldSynchronizer.addSearchItem("attribute", "author:"); + searchFieldSynchronizer.synchronize(); + }); + + // journalButton action + journalButton.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> { + searchFieldSynchronizer.addSearchItem("attribute", "journal:"); + searchFieldSynchronizer.synchronize(); + }); + + // titleButton action + titleButton.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> { + searchFieldSynchronizer.addSearchItem("attribute", "title:"); + searchFieldSynchronizer.synchronize(); + }); + + // yearButton action + yearButton.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> { + searchFieldSynchronizer.addSearchItem("attribute", "year:"); + searchFieldSynchronizer.synchronize(); + }); + + // yearRangeSlider action + hSlider.addEventFilter(MouseEvent.MOUSE_RELEASED, event -> { + int pos = 0; + boolean bol = false; + SearchItem item = null; + if (!searchField.getText().isEmpty()) { + for (int i = 0; i < searchFieldSynchronizer.searchItemList.size(); i++) { + item = searchFieldSynchronizer.searchItemList.get(i); + if (item.getItemType().equals("attribute")) { + if (item.getItem().equals("year-range:")) { + pos = i; + bol = true; + } + } + } + } + if (!bol) { + searchFieldSynchronizer.addSearchItem("attribute", "year-range:"); + searchFieldSynchronizer.addSearchItem("query", Integer.toString((int) hSlider.getLowValue()) + "to" + Integer.toString((int) hSlider.getHighValue())); + searchFieldSynchronizer.synchronize(); + } else { + searchFieldSynchronizer.searchItemList.get(pos + 1).setItem(Integer.toString((int) hSlider.getLowValue()) + "to" + Integer.toString((int) hSlider.getHighValue())); + searchFieldSynchronizer.synchronize(); + } + }); + + // yearRangeSlider output on action + hSlider.highValueProperty().addListener((observable, oldValue, newValue) -> { + label.setText("from " + Integer.toString((int) hSlider.getLowValue()) + " to " + Integer.toString((int) hSlider.getHighValue())); + }); + hSlider.lowValueChangingProperty().addListener((observable, oldValue, newValue) -> { + label.setText("from " + Integer.toString((int) hSlider.getLowValue()) + " to " + Integer.toString((int) hSlider.getHighValue())); + }); + + // andButton action + andButton.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> { + searchFieldSynchronizer.addSearchItem("logical", "AND"); + searchFieldSynchronizer.synchronize(); + }); + + // orButton action + orButton.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> { + searchFieldSynchronizer.addSearchItem("logical", "OR"); + searchFieldSynchronizer.synchronize(); + }); + + // leftBracketButton action + leftBracketButton.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> { + searchFieldSynchronizer.addSearchItem("bracket", "("); + searchFieldSynchronizer.synchronize(); + }); + + // rightBracketButton action + rightBracketButton.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> { + searchFieldSynchronizer.addSearchItem("bracket", ")"); + searchFieldSynchronizer.synchronize(); + }); + + // andGroupButton action + andGroupButton.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> { + searchFieldSynchronizer.addBrackets(); + searchFieldSynchronizer.addSearchItem("logical", "AND"); + searchFieldSynchronizer.synchronize(); + }); + + // orGroupButton action + orGroupButton.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> { + searchFieldSynchronizer.addBrackets(); + searchFieldSynchronizer.addSearchItem("logical", "OR"); + searchFieldSynchronizer.synchronize(); + }); + } + +} diff --git a/src/main/java/org/jabref/gui/search/GlobalSearchBar.java b/src/main/java/org/jabref/gui/search/GlobalSearchBar.java index da016e1f219..c8d805f1ddb 100644 --- a/src/main/java/org/jabref/gui/search/GlobalSearchBar.java +++ b/src/main/java/org/jabref/gui/search/GlobalSearchBar.java @@ -9,7 +9,6 @@ import javax.swing.undo.UndoManager; -import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; import javafx.beans.property.BooleanProperty; @@ -55,7 +54,6 @@ import org.jabref.gui.undo.CountingUndoManager; import org.jabref.gui.util.BindingsHelper; import org.jabref.gui.util.DefaultTaskExecutor; -import org.jabref.gui.util.IconValidationDecorator; import org.jabref.gui.util.TooltipTextUtil; import org.jabref.logic.l10n.Localization; import org.jabref.logic.search.SearchQuery; @@ -64,10 +62,6 @@ import org.jabref.preferences.PreferencesService; import org.jabref.preferences.SearchPreferences; -import de.saxsys.mvvmfx.utils.validation.FunctionBasedValidator; -import de.saxsys.mvvmfx.utils.validation.ValidationMessage; -import de.saxsys.mvvmfx.utils.validation.Validator; -import de.saxsys.mvvmfx.utils.validation.visualization.ControlsFxVisualizer; import impl.org.controlsfx.skin.AutoCompletePopup; import org.controlsfx.control.textfield.AutoCompletionBinding; import org.controlsfx.control.textfield.CustomTextField; @@ -88,7 +82,6 @@ public class GlobalSearchBar extends HBox { private final CustomTextField searchField = SearchTextField.create(); private final ToggleButton caseSensitiveButton; - private final ToggleButton regularExpressionButton; private final ToggleButton fulltextButton; private final Button openGlobalSearchButton; private final ToggleButton keepSearchString; @@ -98,7 +91,6 @@ public class GlobalSearchBar extends HBox { private final StateManager stateManager; private final PreferencesService preferencesService; - private final Validator regexValidator; private final UndoManager undoManager; private final SearchPreferences searchPreferences; @@ -106,6 +98,8 @@ public class GlobalSearchBar extends HBox { private final BooleanProperty globalSearchActive = new SimpleBooleanProperty(false); private GlobalSearchResultDialog globalSearchResultDialog; + private final DropDownMenu dropDownMenu; + public GlobalSearchBar(JabRefFrame frame, StateManager stateManager, PreferencesService preferencesService, CountingUndoManager undoManager) { super(); this.stateManager = stateManager; @@ -123,7 +117,11 @@ public GlobalSearchBar(JabRefFrame frame, StateManager stateManager, Preferences searchFieldTooltip.setMaxHeight(10); updateHintVisibility(); + // Prototype DropDownMenu + SearchFieldSynchronizer searchFieldSynchronizer = new SearchFieldSynchronizer(searchField); + this.dropDownMenu = new DropDownMenu(searchField, this, searchFieldSynchronizer); KeyBindingRepository keyBindingRepository = Globals.getKeyPrefs(); + searchField.addEventFilter(KeyEvent.KEY_PRESSED, event -> { Optional keyBinding = keyBindingRepository.mapToKeyBinding(event); if (keyBinding.isPresent()) { @@ -136,9 +134,20 @@ public GlobalSearchBar(JabRefFrame frame, StateManager stateManager, Preferences } }); +// searchFieldSynchronizer.searchItemList.addListener(new ListChangeListener() { +// @Override +// public void onChanged(Change c) { +// searchFieldSynchronizer.synchronize(); +// } +// }); + // Listens to global search bar textfield changes and updates searchItemList in searchFieldSynchronizer + searchField.textProperty().addListener((observable, oldValue, newValue) -> { +// System.out.println("textfield changed from " + oldValue + " to " + newValue); + searchFieldSynchronizer.updateSearchItemList(searchFieldSynchronizer.textFieldToList()); + searchFieldSynchronizer.syntaxHighlighting(); + }); ClipBoardManager.addX11Support(searchField); - regularExpressionButton = IconTheme.JabRefIcons.REG_EX.asToggleButton(); caseSensitiveButton = IconTheme.JabRefIcons.CASE_SENSITIVE.asToggleButton(); fulltextButton = IconTheme.JabRefIcons.FULLTEXT.asToggleButton(); openGlobalSearchButton = IconTheme.JabRefIcons.OPEN_GLOBAL_SEARCH.asButton(); @@ -147,15 +156,12 @@ public GlobalSearchBar(JabRefFrame frame, StateManager stateManager, Preferences initSearchModifierButtons(); BooleanBinding focusedOrActive = searchField.focusedProperty() - .or(regularExpressionButton.focusedProperty()) .or(caseSensitiveButton.focusedProperty()) .or(fulltextButton.focusedProperty()) .or(keepSearchString.focusedProperty()) .or(searchField.textProperty() .isNotEmpty()); - regularExpressionButton.visibleProperty().unbind(); - regularExpressionButton.visibleProperty().bind(focusedOrActive); caseSensitiveButton.visibleProperty().unbind(); caseSensitiveButton.visibleProperty().bind(focusedOrActive); fulltextButton.visibleProperty().unbind(); @@ -163,21 +169,13 @@ public GlobalSearchBar(JabRefFrame frame, StateManager stateManager, Preferences keepSearchString.visibleProperty().unbind(); keepSearchString.visibleProperty().bind(focusedOrActive); - StackPane modifierButtons = new StackPane(new HBox(regularExpressionButton, caseSensitiveButton, fulltextButton, keepSearchString)); + StackPane modifierButtons = new StackPane(new HBox(caseSensitiveButton, fulltextButton, keepSearchString)); modifierButtons.setAlignment(Pos.CENTER); searchField.setRight(new HBox(searchField.getRight(), modifierButtons)); searchField.getStyleClass().add("search-field"); searchField.setMinWidth(100); HBox.setHgrow(searchField, Priority.ALWAYS); - regexValidator = new FunctionBasedValidator<>( - searchField.textProperty(), - query -> !(regularExpressionButton.isSelected() && !validRegex()), - ValidationMessage.error(Localization.lang("Invalid regular expression"))); - ControlsFxVisualizer visualizer = new ControlsFxVisualizer(); - visualizer.setDecoration(new IconValidationDecorator(Pos.CENTER_LEFT)); - Platform.runLater(() -> visualizer.initVisualization(regexValidator.getValidationStatus(), searchField)); - this.getChildren().addAll(searchField, openGlobalSearchButton, currentResults); this.setSpacing(4.0); this.setAlignment(Pos.CENTER_LEFT); @@ -202,14 +200,6 @@ private void updateSearchResultsForQuery(SearchQuery query) { } private void initSearchModifierButtons() { - regularExpressionButton.setSelected(searchPreferences.isRegularExpression()); - regularExpressionButton.setTooltip(new Tooltip(Localization.lang("regular expression"))); - initSearchModifierButton(regularExpressionButton); - regularExpressionButton.setOnAction(event -> { - searchPreferences.setSearchFlag(SearchRules.SearchFlags.REGULAR_EXPRESSION, regularExpressionButton.isSelected()); - performSearch(); - }); - caseSensitiveButton.setSelected(searchPreferences.isCaseSensitive()); caseSensitiveButton.setTooltip(new Tooltip(Localization.lang("Case sensitive"))); initSearchModifierButton(caseSensitiveButton); @@ -268,10 +258,11 @@ public void focus() { } public void performSearch() { - LOGGER.debug("Flags: {}", searchPreferences.getSearchFlags()); LOGGER.debug("Run search " + searchField.getText()); + // Prototype DropDownMenu + // An empty search field should cause the search to be cleared. if (searchField.getText().isEmpty()) { currentResults.setText(""); @@ -280,12 +271,6 @@ public void performSearch() { return; } - // Invalid regular expression - if (!regexValidator.getValidationStatus().isValid()) { - currentResults.setText(Localization.lang("Invalid regular expression")); - return; - } - SearchQuery searchQuery = new SearchQuery(this.searchField.getText(), searchPreferences.getSearchFlags()); if (!searchQuery.isValid()) { informUserAboutInvalidSearchQuery(); @@ -315,7 +300,7 @@ private void informUserAboutInvalidSearchQuery() { public void setAutoCompleter(SuggestionProvider searchCompleter) { if (preferencesService.getAutoCompletePreferences().shouldAutoComplete()) { - AutoCompletionTextInputBinding autoComplete = AutoCompletionTextInputBinding.autoComplete(searchField, + AutoCompletionTextInputBinding autoComplete = AutoCompletionTextInputBinding.autoComplete(dropDownMenu.searchString, searchCompleter::provideSuggestions, new PersonNameStringConverter(false, false, AutoCompleteFirstNameMode.BOTH), new AppendPersonNamesStrategy()); @@ -361,7 +346,7 @@ private void updateResults(int matched, TextFlow description, boolean grammarBas private void setSearchFieldHintTooltip(TextFlow description) { if (preferencesService.getGeneralPreferences().shouldShowAdvancedHints()) { - String genericDescription = Localization.lang("Hint:\n\nTo search all fields for Smith, enter:\nsmith\n\nTo search the field author for Smith and the field title for electrical, enter:\nauthor=Smith and title=electrical"); + String genericDescription = Localization.lang("Hint:\n\nTo search all fields for Smith, enter:\nsmith\n\nTo search the field author for Smith and the field title for electrical, enter:\nauthor:Smith and title:electrical"); List genericDescriptionTexts = TooltipTextUtil.createTextsFromHtml(genericDescription); if (description == null) { diff --git a/src/main/java/org/jabref/gui/search/RecentSearch.java b/src/main/java/org/jabref/gui/search/RecentSearch.java new file mode 100644 index 00000000000..76a22c9c972 --- /dev/null +++ b/src/main/java/org/jabref/gui/search/RecentSearch.java @@ -0,0 +1,36 @@ +package org.jabref.gui.search; + +import javafx.scene.control.ListView; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; + +public class RecentSearch { + private ListView RecentSearches = new ListView<>(); + + public RecentSearch(GlobalSearchBar searchBar) { + this.RecentSearches.setOnMouseClicked(event -> { + String query = this.RecentSearches.getSelectionModel().getSelectedItem(); + if (query != null) { + searchBar.setSearchTerm(query); + } + searchBar.performSearch(); + }); + } + + public void add(String Query) { + RecentSearches.getItems().removeIf(entry -> entry.equals(Query)); + if (Query.equals("")) { + return; + } + + this.RecentSearches.getItems().add(Query); + } + + public HBox getHBox() { + return new HBox(RecentSearches); + } + + public VBox getVBox() { + return new VBox(RecentSearches); + } +} diff --git a/src/main/java/org/jabref/gui/search/SearchFieldSynchronizer.java b/src/main/java/org/jabref/gui/search/SearchFieldSynchronizer.java new file mode 100644 index 00000000000..5b65db8c3c4 --- /dev/null +++ b/src/main/java/org/jabref/gui/search/SearchFieldSynchronizer.java @@ -0,0 +1,364 @@ +package org.jabref.gui.search; + +import java.util.ArrayList; +import java.util.Stack; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import org.apache.lucene.queryparser.flexible.core.QueryNodeException; +import org.apache.lucene.queryparser.flexible.standard.StandardQueryParser; +import org.controlsfx.control.textfield.CustomTextField; + +public class SearchFieldSynchronizer { + ObservableList searchItemList = FXCollections.observableList(new ArrayList()); + CustomTextField searchField; + + public SearchFieldSynchronizer(CustomTextField searchField) { + this.searchField = searchField; + + } + + /* Add item to list if valid */ + public void addSearchItem(String itemType, String item) { + SearchItem newItem = new SearchItem(itemType, item); + + // Remove empty query item (appears after clearing the searchBar text) + if (!searchItemList.isEmpty()) { + if (searchItemList.get(0).getItem().equals("")) { + searchItemList.remove(searchItemList.get(0)); + } + } + if (isValid(this.searchItemList, newItem)) { + searchItemList.add(newItem); + } + } + + /* Does the same as the function above but accepts SearchItems objects directly*/ + public void addSearchItem(SearchItem newItem) { + if (!searchItemList.isEmpty()) { + if (searchItemList.get(0).getItem().equals("")) { + searchItemList.remove(searchItemList.get(0)); + } + } + if (isValid(this.searchItemList, newItem)) { + searchItemList.add(newItem); + } + } + + public boolean isValid() { + StandardQueryParser queryParser = new StandardQueryParser(); + + boolean valid = true; + try { + queryParser.parse(this.searchStringBuilder(), ""); + } catch (QueryNodeException e) { + valid = false; + } + + if (valid) { + return true; + } + + valid = true; + SearchItem Foo = new SearchItem("query", "foo"); + + if (searchItemList.get(searchItemList.size() - 1).getItem().equals(")")) { + searchItemList.remove(searchItemList.size() - 1); + searchItemList.add(Foo); + searchItemList.add(new SearchItem("bracket", ")")); + } else { + searchItemList.add(Foo); + } + try { + queryParser.parse(this.searchStringBuilder(), ""); + } catch (QueryNodeException e) { + valid = false; + } + + searchItemList.remove(Foo); + + return valid; + } + + /* Used mainly by the DropDownMenu to check if a button press + should add an item to the searchItemList or not. */ + public boolean isValid(ObservableList searchItemList, SearchItem newItem) { + StandardQueryParser queryParser = new StandardQueryParser(); + + searchItemList.add(newItem); + boolean valid = true; + try { + queryParser.parse(this.searchStringBuilder(), ""); + } catch (QueryNodeException e) { + valid = false; + } + searchItemList.remove(newItem); + if (valid) { + return true; + } + + valid = true; + SearchItem Foo = new SearchItem("query", "foo"); + searchItemList.add(newItem); + searchItemList.add(Foo); + try { + queryParser.parse(this.searchStringBuilder(), ""); + } catch (QueryNodeException e) { + valid = false; + } + + searchItemList.remove(newItem); + searchItemList.remove(Foo); + + return valid; + } + + /* Returns the last SearchItem from a searchItemList */ + public SearchItem returnLatest(ObservableList searchItemList) { + return searchItemList.get(searchItemList.size() - 1); + } + + public boolean latestIsBracket() { + if (searchItemList.size() == 0) { + return false; + } + return searchItemList.get(searchItemList.size() - 1).getItemType().equals("bracket") + && searchItemList.get(searchItemList.size() - 1).getItem().equals(")"); + } + + /* Text before --------, Text after addBrackets (--------) */ + public void addBrackets() { + if (isValidLucene()) { + searchItemList.add(0, new SearchItem("bracket", "(")); + searchItemList.add(new SearchItem("bracket", ")")); + } else { + System.out.print("TEST"); + } + } + + /* builds a new searchString by calling searchStringBuilder, + sets the searchField text and positions caret at the right position */ + public void synchronize() { + String searchString = this.searchStringBuilder(); + searchField.clear(); + searchField.setText(searchString); + searchField.positionCaret(searchField.getText().length()); + syntaxHighlighting(); + } + + private int caretPos() { + if (searchField.getText().endsWith(")")) { + return searchField.getText().length() - 1; + } + return searchField.getText().length(); + } + + /* Builds a searchString from the searchItemList */ + public String searchStringBuilder() { + + // create StringBuilder that later becomes our search String + StringBuilder searchString = new StringBuilder(); + + // count the item index + int i = 0; + + // loop over all SearchItem in searchItemList + for (SearchItem item : searchItemList) { + if (item.isQuery()) { + // item is query + + // if it's not the first item append " " if the item before the current one was not an attribute or bracket + if (i > 0) { + // not first item + if (!searchItemList.get(i - 1).isAttribute() && !searchItemList.get(i - 1).isLeftBracket()) { + // item before was not attribute or left bracket + searchString.append(" "); + } + } + + // add item to the end of searchString + searchString.append(item.getItem()); + } + + if (item.isLogical()) { + // item is logical + if (i > 0) { + // not first item + searchString.append(" "); + } + searchString.append(item.getItem()); + + // if item is last in list append " " after item + if (returnLatest(searchItemList).equals(item)) { + // item is last in list + searchString.append(" "); + } + } + + if (item.isAttribute()) { + // item is attribute + // if it's not the first item append " " if the item before the current one was not a bracket + if (i > 0) { + // not first item + if (!searchItemList.get(i - 1).isLeftBracket()) { + // item before was not attribute or left bracket + searchString.append(" "); + } + } + searchString.append(item.getItem()); + } + + if (item.isBracket()) { + // item is bracket + if (item.isRightBracket()) { + searchString.append(")"); + } + if (item.isLeftBracket()) { + searchString.append("("); + } + } + // raise index counter by one + i = i + 1; + } + + // return String from StringBuilder + return searchString.toString(); + } + + /* Updates the searchItemList from a string ArrayList. + * In reality this string ArrayList is created by textFieldToList */ + public void updateSearchItemList(ArrayList list) { + // clear the current searchItemList as we rebuild it from the input ArrayList + searchItemList.clear(); + + if (!list.isEmpty()) { + // list is not empty + + // loop over all Strings in ArrayList + for (String s : list) { + if (s.endsWith(":")) { + // s is attribute + // add attribute to ItemList + searchItemList.add(new SearchItem("attribute", s)); + } else if (s.equals("AND")) { + // s is logical + // add logical to ItemList + searchItemList.add(new SearchItem("logical", s)); + } else if (s.equals("OR")) { + // s is logical + // add logical to ItemList + searchItemList.add(new SearchItem("logical", s)); + } else if (s.equals("(")) { + // s is left bracket + // add bracket to list + searchItemList.add(new SearchItem("bracket", s)); + } else if (s.equals(")")) { + // s is right bracket + // add bracket to list + searchItemList.add(new SearchItem("bracket", s)); + } else if (s.equals(" ")) { + // s is " " + // continue without adding s + continue; + } else { + // s is none of the above -> s is query + // add query to list + searchItemList.add(new SearchItem("query", s)); + } + } + searchItemListToString(searchItemList); + } + } + + // Prints a searchItemList to the console for testing purposes. + public void searchItemListToString(ObservableList searchItemList) { + for (SearchItem item : searchItemList) { + System.out.print("| "); + System.out.print("Type: " + item.getItemType() + ", "); + System.out.print("Value: " + item.getItem()); + System.out.print(" |"); + } + System.out.println("----"); + } + + // Highlights the searchField green if the search query is valid and red if it is invalid + public void syntaxHighlighting() { + if (isValidLucene()) { + this.searchField.setStyle("-fx-border-color: green"); + } else { + this.searchField.setStyle("-fx-border-color: red"); + } + + } + + public ArrayList textFieldToList() { + String str = searchField.getText(); + + // splits a string "author:luh AND year:2013 OR author:\"lee smith\"" into + // [(] [author:] [luh] [AND] [year:] [2013] [)] [OR] [(] [author:] ["lee smith" [)]] + // String[] words = str.split("(?<=:)|\\ "); + String[] words = str.split("(?<=:)|(?<=\\()|(?=\\))|\\ "); + ArrayList list = new ArrayList<>(); + + for (int i = 0; i < words.length; i++) { + if (words[i].equals(")(")) { + list.add(")"); + list.add("("); + continue; + } + if (words[i].startsWith("\"")) { + boolean isWordAfterwards = i + 1 < words.length; + if (isWordAfterwards && words[i + 1].endsWith("\"") && !words[i].endsWith(":")) { + String str2 = words[i] + " " + words[i + 1]; + list.add(str2); + i++; + } else { + list.add(words[i]); + } + } else { + list.add(words[i]); + } + } + + return list; + } + + /* Check if brackets are balanced */ + public boolean bracketsBalanced(ObservableList searchItemList) { + Stack bracketStack = new Stack<>(); + + for (SearchItem item : searchItemList) { + // if it's a left bracket -> push to Stack + // if it's a right bracket -> pop from stack and check if we get a left bracket + if (item.isLeftBracket()) { + bracketStack.push(item); + } else if (item.isRightBracket()) { + if (bracketStack.isEmpty()) { + return false; + } else { + if (bracketStack.pop().isRightBracket()) { + return false; + } + } + } + } + return bracketStack.isEmpty(); + } + + // Uses StandardQueryParser to parse searchString. Throws exception if not valid syntax + public boolean isValidLucene() { + StandardQueryParser queryParser = new StandardQueryParser(); + try { + queryParser.parse(this.searchStringBuilder(), ""); + } catch (QueryNodeException e) { + return false; + } + return true; + } + + // Deletes all entries in the List + public void deleteAllEntries() { + searchItemList.clear(); + } +} diff --git a/src/main/java/org/jabref/gui/search/SearchItem.java b/src/main/java/org/jabref/gui/search/SearchItem.java new file mode 100644 index 00000000000..3a8b0d6c9b8 --- /dev/null +++ b/src/main/java/org/jabref/gui/search/SearchItem.java @@ -0,0 +1,51 @@ +package org.jabref.gui.search; + +public class SearchItem { + private String itemType; + private String item; + + public SearchItem(String itemType, String item) { + this.itemType = itemType; + this.item = item; + } + + public String getItemType() { + return itemType; + } + + public void setItemType(String itemType) { + this.itemType = itemType; + } + + public String getItem() { + return item; + } + + public void setItem(String item) { + this.item = item; + } + + public boolean isQuery() { + return this.itemType.equals("query"); + } + + public boolean isAttribute() { + return this.itemType.equals("attribute"); + } + + public boolean isLogical() { + return this.itemType.equals("logical"); + } + + public boolean isBracket() { + return this.itemType.equals("bracket"); + } + + public boolean isLeftBracket() { + return this.itemType.equals("bracket") && this.item.equals("("); + } + + public boolean isRightBracket() { + return this.itemType.equals("bracket") && this.item.equals(")"); + } +} diff --git a/src/main/java/org/jabref/gui/search/rules/describer/LuceneBasedSearchRuleDescriber.java b/src/main/java/org/jabref/gui/search/rules/describer/LuceneBasedSearchRuleDescriber.java new file mode 100644 index 00000000000..ecd132e9bac --- /dev/null +++ b/src/main/java/org/jabref/gui/search/rules/describer/LuceneBasedSearchRuleDescriber.java @@ -0,0 +1,24 @@ +package org.jabref.gui.search.rules.describer; + +import java.util.EnumSet; + +import javafx.scene.text.TextFlow; + +import org.jabref.model.search.rules.SearchRules.SearchFlags; + +public class LuceneBasedSearchRuleDescriber implements SearchDescriber { + + private final EnumSet searchFlags; + private final String query; + + public LuceneBasedSearchRuleDescriber(EnumSet searchFlags, String query) { + this.searchFlags = searchFlags; + this.query = query; + } + + @Override + public TextFlow getDescription() { + TextFlow searchDescription = new TextFlow(); + return searchDescription; + } +} diff --git a/src/main/java/org/jabref/gui/search/rules/describer/SearchDescribers.java b/src/main/java/org/jabref/gui/search/rules/describer/SearchDescribers.java index 4cc5e2ebd82..06155b580fa 100644 --- a/src/main/java/org/jabref/gui/search/rules/describer/SearchDescribers.java +++ b/src/main/java/org/jabref/gui/search/rules/describer/SearchDescribers.java @@ -1,8 +1,9 @@ package org.jabref.gui.search.rules.describer; import org.jabref.logic.search.SearchQuery; -import org.jabref.model.search.rules.ContainBasedSearchRule; +import org.jabref.model.search.rules.ContainsBasedSearchRule; import org.jabref.model.search.rules.GrammarBasedSearchRule; +import org.jabref.model.search.rules.LuceneBasedSearchRule; import org.jabref.model.search.rules.RegexBasedSearchRule; public class SearchDescribers { @@ -17,12 +18,14 @@ private SearchDescribers() { * @return the search describer to turn the search into something human understandable */ public static SearchDescriber getSearchDescriberFor(SearchQuery searchQuery) { - if (searchQuery.getRule() instanceof GrammarBasedSearchRule grammarBasedSearchRule) { - return new GrammarBasedSearchRuleDescriber(grammarBasedSearchRule.getSearchFlags(), grammarBasedSearchRule.getTree()); - } else if (searchQuery.getRule() instanceof ContainBasedSearchRule containBasedSearchRule) { + if (searchQuery.getRule() instanceof LuceneBasedSearchRule lucenceBasedSearchRule) { + return new LuceneBasedSearchRuleDescriber(lucenceBasedSearchRule.getSearchFlags(), searchQuery.getQuery()); + } else if (searchQuery.getRule() instanceof ContainsBasedSearchRule containBasedSearchRule) { return new ContainsAndRegexBasedSearchRuleDescriber(containBasedSearchRule.getSearchFlags(), searchQuery.getQuery()); } else if (searchQuery.getRule() instanceof RegexBasedSearchRule regexBasedSearchRule) { return new ContainsAndRegexBasedSearchRuleDescriber(regexBasedSearchRule.getSearchFlags(), searchQuery.getQuery()); + } else if (searchQuery.getRule() instanceof GrammarBasedSearchRule grammarBasedSearchRule) { + return new GrammarBasedSearchRuleDescriber(grammarBasedSearchRule.getSearchFlags(), grammarBasedSearchRule.getTree()); } else { throw new IllegalStateException("Cannot find a describer for searchRule " + searchQuery.getRule() + " and query " + searchQuery.getQuery()); } diff --git a/src/main/java/org/jabref/logic/importer/fetcher/transformers/AbstractQueryTransformer.java b/src/main/java/org/jabref/logic/importer/fetcher/transformers/AbstractQueryTransformer.java index 21d45a13952..563bbd8fc10 100644 --- a/src/main/java/org/jabref/logic/importer/fetcher/transformers/AbstractQueryTransformer.java +++ b/src/main/java/org/jabref/logic/importer/fetcher/transformers/AbstractQueryTransformer.java @@ -23,7 +23,7 @@ public abstract class AbstractQueryTransformer { public static final String NO_EXPLICIT_FIELD = "default"; private static final Logger LOGGER = LoggerFactory.getLogger(AbstractQueryTransformer.class); - // These can be used for filtering in post processing + // These can be used for filtering in post-processing protected int startYear = Integer.MAX_VALUE; protected int endYear = Integer.MIN_VALUE; diff --git a/src/main/java/org/jabref/logic/search/SearchQuery.java b/src/main/java/org/jabref/logic/search/SearchQuery.java index 883830c3eab..4dc9fd40a58 100644 --- a/src/main/java/org/jabref/logic/search/SearchQuery.java +++ b/src/main/java/org/jabref/logic/search/SearchQuery.java @@ -12,7 +12,7 @@ import org.jabref.logic.l10n.Localization; import org.jabref.model.entry.BibEntry; import org.jabref.model.search.SearchMatcher; -import org.jabref.model.search.rules.ContainBasedSearchRule; +import org.jabref.model.search.rules.ContainsBasedSearchRule; import org.jabref.model.search.rules.GrammarBasedSearchRule; import org.jabref.model.search.rules.SearchRule; import org.jabref.model.search.rules.SearchRules; @@ -82,7 +82,7 @@ public boolean isValid() { } public boolean isContainsBasedSearch() { - return rule instanceof ContainBasedSearchRule; + return rule instanceof ContainsBasedSearchRule; } private String getCaseSensitiveDescription() { diff --git a/src/main/java/org/jabref/model/search/rules/BibQueryVisitor.java b/src/main/java/org/jabref/model/search/rules/BibQueryVisitor.java new file mode 100644 index 00000000000..d213920a7e0 --- /dev/null +++ b/src/main/java/org/jabref/model/search/rules/BibQueryVisitor.java @@ -0,0 +1,154 @@ +package org.jabref.model.search.rules; + +import java.util.EnumSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.Field; +import org.jabref.model.entry.field.FieldFactory; + +import org.apache.lucene.queryparser.flexible.core.nodes.BooleanQueryNode; +import org.apache.lucene.queryparser.flexible.core.nodes.FieldQueryNode; +import org.apache.lucene.queryparser.flexible.core.nodes.GroupQueryNode; +import org.apache.lucene.queryparser.flexible.core.nodes.ModifierQueryNode; +import org.apache.lucene.queryparser.flexible.core.nodes.OrQueryNode; +import org.apache.lucene.queryparser.flexible.core.nodes.QueryNode; +import org.apache.lucene.queryparser.flexible.standard.nodes.RegexpQueryNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class BibQueryVisitor { + public static final String NO_EXPLICIT_FIELD = "default"; + private static final Logger LOGGER = LoggerFactory.getLogger(BibQueryVisitor.class); + + private final BibEntry bibEntry; + private EnumSet searchFlags; + + public BibQueryVisitor(BibEntry bibEntry, EnumSet searchFlags) { + this.bibEntry = bibEntry; + this.searchFlags = searchFlags; + } + + public boolean matchFound(QueryNode query) { + if (query instanceof FieldQueryNode) { + return matchFound((FieldQueryNode) query); + } else if (query instanceof BooleanQueryNode) { + return matchFound((BooleanQueryNode) query); + } else if (query instanceof ModifierQueryNode) { + return matchFound((ModifierQueryNode) query); + } else if (query instanceof RegexpQueryNode) { + return matchFound((RegexpQueryNode) query); + } else if (query instanceof GroupQueryNode) { + return matchFound(((GroupQueryNode) query).getChild()); + } else { + LOGGER.error("Unsupported case when evaluating the query:\n {}", query); + return false; + } + } + + private boolean matchFound(String searchField, String searchString, boolean regExMode) { + Pattern pattern = null; + if (regExMode) { + try { + pattern = Pattern.compile(searchString, searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE) ? 0 : Pattern.CASE_INSENSITIVE); + } catch (PatternSyntaxException ex) { + LOGGER.error("Could not compile regex {}", searchString, ex); + return false; + } + } else { + if (!searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE)) { + searchString = searchString.toLowerCase(Locale.ROOT); + } + } + String finalSearchString = searchString; + + Pattern finalPattern = pattern; + + if (searchField.isEmpty()) { + return bibEntry.getFields().stream() + .map(field -> bibEntry.getLatexFreeField(field)) + .filter(content -> matchFound(finalSearchString, finalPattern, content)) + .findAny() + .isPresent(); + } else { + Field field = FieldFactory.parseField(searchField); + return matchFound(finalSearchString, finalPattern, bibEntry.getLatexFreeField(field)); + } + } + + private boolean matchFound(FieldQueryNode query) { + if (searchFlags.contains(SearchRules.SearchFlags.REGULAR_EXPRESSION)) { + LOGGER.info("RegEx search is not supported in lucene search"); + } + return matchFound(query.getFieldAsString(), query.getTextAsString(), false); + } + + private boolean matchFound(RegexpQueryNode query) { + return matchFound(query.getFieldAsString(), query.getText().toString(), true); + } + + /** + * @param finalPattern if not null, the RegEx pattern to match for. If null, a "normal" search is executed + */ + private boolean matchFound(String finalSearchString, Pattern finalPattern, Optional content) { + return content.map(value -> searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE) ? value : value.toLowerCase(Locale.ROOT)) + .filter(value -> { + if (finalPattern != null) { + return finalPattern.matcher(value).find(); + } else { + List unmatchedWords = new SentenceAnalyzer(finalSearchString).getWords(); + Iterator unmatchedWordsIterator = unmatchedWords.iterator(); + while (unmatchedWordsIterator.hasNext()) { + String word = unmatchedWordsIterator.next(); + if (value.contains(word)) { + unmatchedWordsIterator.remove(); + } + } + return unmatchedWords.isEmpty(); + } + }) + .isPresent(); + } + + private boolean matchFound(BooleanQueryNode query) { + if (query instanceof OrQueryNode) { + return matchFound((OrQueryNode) query); + } else { + // AND is the default + return matchFoundForAnd(query); + } + } + + private boolean matchFoundForAnd(BooleanQueryNode query) { + for (QueryNode child : query.getChildren()) { + if (!matchFound(child)) { + return false; + } + } + return true; + } + + private boolean matchFound(OrQueryNode query) { + for (QueryNode child : query.getChildren()) { + if (matchFound(child)) { + return true; + } + } + return false; + } + + private boolean matchFound(ModifierQueryNode query) { + ModifierQueryNode.Modifier modifier = query.getModifier(); + if (modifier == ModifierQueryNode.Modifier.MOD_NOT) { + return !matchFound(query.getChild()); + } else { + // optional not yet supported + return matchFound(query.getChild()); + } + } +} diff --git a/src/main/java/org/jabref/model/search/rules/ContainBasedSearchRule.java b/src/main/java/org/jabref/model/search/rules/ContainBasedSearchRule.java deleted file mode 100644 index cf7c7568368..00000000000 --- a/src/main/java/org/jabref/model/search/rules/ContainBasedSearchRule.java +++ /dev/null @@ -1,110 +0,0 @@ -package org.jabref.model.search.rules; - -import java.io.IOException; -import java.util.EnumSet; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Vector; -import java.util.stream.Collectors; - -import org.jabref.architecture.AllowedToUseLogic; -import org.jabref.gui.Globals; -import org.jabref.gui.LibraryTab; -import org.jabref.logic.pdf.search.retrieval.PdfSearcher; -import org.jabref.model.database.BibDatabaseContext; -import org.jabref.model.entry.BibEntry; -import org.jabref.model.entry.field.Field; -import org.jabref.model.pdf.search.PdfSearchResults; -import org.jabref.model.pdf.search.SearchResult; -import org.jabref.model.search.rules.SearchRules.SearchFlags; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Search rule for contain-based search. - */ -@AllowedToUseLogic("Because access to the lucene index is needed") -public class ContainBasedSearchRule implements SearchRule { - - private static final Logger LOGGER = LoggerFactory.getLogger(LibraryTab.class); - - private final EnumSet searchFlags; - - private String lastQuery; - private List lastSearchResults; - - private final BibDatabaseContext databaseContext; - - public ContainBasedSearchRule(EnumSet searchFlags) { - this.searchFlags = searchFlags; - this.lastQuery = ""; - lastSearchResults = new Vector<>(); - - databaseContext = Globals.stateManager.getActiveDatabase().orElse(null); - } - - @Override - public boolean validateSearchStrings(String query) { - return true; - } - - @Override - public boolean applyRule(String query, BibEntry bibEntry) { - - String searchString = query; - if (!searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE)) { - searchString = searchString.toLowerCase(Locale.ROOT); - } - - List unmatchedWords = new SentenceAnalyzer(searchString).getWords(); - - for (Field fieldKey : bibEntry.getFields()) { - String formattedFieldContent = bibEntry.getLatexFreeField(fieldKey).get(); - if (!searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE)) { - formattedFieldContent = formattedFieldContent.toLowerCase(Locale.ROOT); - } - - Iterator unmatchedWordsIterator = unmatchedWords.iterator(); - while (unmatchedWordsIterator.hasNext()) { - String word = unmatchedWordsIterator.next(); - if (formattedFieldContent.contains(word)) { - unmatchedWordsIterator.remove(); - } - } - - if (unmatchedWords.isEmpty()) { - return true; - } - } - - return getFulltextResults(query, bibEntry).numSearchResults() > 0; // Didn't match all words. - } - - @Override - public PdfSearchResults getFulltextResults(String query, BibEntry bibEntry) { - - if (!searchFlags.contains(SearchRules.SearchFlags.FULLTEXT) || databaseContext == null) { - return new PdfSearchResults(List.of()); - } - - if (!query.equals(this.lastQuery)) { - this.lastQuery = query; - lastSearchResults = List.of(); - try { - PdfSearcher searcher = PdfSearcher.of(databaseContext); - PdfSearchResults results = searcher.search(query, 5); - lastSearchResults = results.getSortedByScore(); - } catch (IOException e) { - LOGGER.error("Could not retrieve search results!", e); - } - } - - return new PdfSearchResults(lastSearchResults.stream().filter(searchResult -> searchResult.isResultFor(bibEntry)).collect(Collectors.toList())); - } - - public EnumSet getSearchFlags() { - return searchFlags; - } -} diff --git a/src/main/java/org/jabref/model/search/rules/ContainsBasedSearchRule.java b/src/main/java/org/jabref/model/search/rules/ContainsBasedSearchRule.java new file mode 100644 index 00000000000..989a4f34e0c --- /dev/null +++ b/src/main/java/org/jabref/model/search/rules/ContainsBasedSearchRule.java @@ -0,0 +1,59 @@ +package org.jabref.model.search.rules; + +import java.util.EnumSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; + +import org.jabref.architecture.AllowedToUseLogic; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.Field; +import org.jabref.model.search.rules.SearchRules.SearchFlags; + +/** + * Search rule for a search based on String.contains() + */ +@AllowedToUseLogic("Because access to the lucene index is needed") +public class ContainsBasedSearchRule extends FullTextSearchRule { + + public ContainsBasedSearchRule(EnumSet searchFlags) { + super(searchFlags); + } + + @Override + public boolean validateSearchStrings(String query) { + return true; + } + + @Override + public boolean applyRule(String query, BibEntry bibEntry) { + String searchString = query; + if (!searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE)) { + searchString = searchString.toLowerCase(Locale.ROOT); + } + + List unmatchedWords = new SentenceAnalyzer(searchString).getWords(); + + for (Field fieldKey : bibEntry.getFields()) { + String formattedFieldContent = bibEntry.getLatexFreeField(fieldKey).get(); + if (!searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE)) { + formattedFieldContent = formattedFieldContent.toLowerCase(Locale.ROOT); + } + + Iterator unmatchedWordsIterator = unmatchedWords.iterator(); + while (unmatchedWordsIterator.hasNext()) { + String word = unmatchedWordsIterator.next(); + if (formattedFieldContent.contains(word)) { + unmatchedWordsIterator.remove(); + } + } + + if (unmatchedWords.isEmpty()) { + return true; + } + } + + return getFulltextResults(query, bibEntry).numSearchResults() > 0; // Didn't match all words. + } + +} diff --git a/src/main/java/org/jabref/model/search/rules/FullTextSearchRule.java b/src/main/java/org/jabref/model/search/rules/FullTextSearchRule.java new file mode 100644 index 00000000000..97084748a6b --- /dev/null +++ b/src/main/java/org/jabref/model/search/rules/FullTextSearchRule.java @@ -0,0 +1,70 @@ +package org.jabref.model.search.rules; + +import java.io.IOException; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.stream.Collectors; + +import org.jabref.gui.Globals; +import org.jabref.logic.pdf.search.retrieval.PdfSearcher; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.pdf.search.PdfSearchResults; +import org.jabref.model.pdf.search.SearchResult; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * All classes providing full text search results inherit from this class. + *

+ * Some kind of caching of the full text search results is implemented. + */ +public abstract class FullTextSearchRule implements SearchRule { + + private static final Logger LOGGER = LoggerFactory.getLogger(FullTextSearchRule.class); + + protected final EnumSet searchFlags; + + protected String lastQuery; + protected List lastSearchResults; + + private final BibDatabaseContext databaseContext; + + public FullTextSearchRule(EnumSet searchFlags) { + this.searchFlags = searchFlags; + this.lastQuery = ""; + lastSearchResults = Collections.emptyList(); + + databaseContext = Globals.stateManager.getActiveDatabase().orElse(null); + } + + public EnumSet getSearchFlags() { + return searchFlags; + } + + @Override + public PdfSearchResults getFulltextResults(String query, BibEntry bibEntry) { + if (!searchFlags.contains(SearchRules.SearchFlags.FULLTEXT) || databaseContext == null) { + return new PdfSearchResults(); + } + + if (!query.equals(this.lastQuery)) { + this.lastQuery = query; + lastSearchResults = Collections.emptyList(); + try { + PdfSearcher searcher = PdfSearcher.of(databaseContext); + PdfSearchResults results = searcher.search(query, 5); + lastSearchResults = results.getSortedByScore(); + } catch (IOException e) { + LOGGER.error("Could not retrieve search results!", e); + } + } + + return new PdfSearchResults(lastSearchResults.stream() + .filter(searchResult -> searchResult.isResultFor(bibEntry)) + .collect(Collectors.toList())); + } + +} diff --git a/src/main/java/org/jabref/model/search/rules/LuceneBasedSearchRule.java b/src/main/java/org/jabref/model/search/rules/LuceneBasedSearchRule.java new file mode 100644 index 00000000000..90f838c2214 --- /dev/null +++ b/src/main/java/org/jabref/model/search/rules/LuceneBasedSearchRule.java @@ -0,0 +1,77 @@ +package org.jabref.model.search.rules; + +import java.util.EnumSet; +import java.util.Locale; +import java.util.concurrent.ExecutionException; + +import org.jabref.architecture.AllowedToUseLogic; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.search.rules.SearchRules.SearchFlags; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import org.apache.lucene.queryparser.flexible.core.QueryNodeException; +import org.apache.lucene.queryparser.flexible.core.nodes.QueryNode; +import org.apache.lucene.queryparser.flexible.core.parser.SyntaxParser; +import org.apache.lucene.queryparser.flexible.standard.parser.StandardSyntaxParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Search rule for a search based on String.contains() + */ +@AllowedToUseLogic("Because access to the lucene index is needed") +public class LuceneBasedSearchRule extends FullTextSearchRule { + + private static final Logger LOGGER = LoggerFactory.getLogger(LuceneBasedSearchRule.class); + + // We use SyntaxParser to be consistent with org.jabref.logic.importer + // We do not use "PrecedenceQueryParser", because this parser keeps the term "AND" + private SyntaxParser parser = new StandardSyntaxParser(); + private LoadingCache cache = CacheBuilder.newBuilder() + .maximumSize(100) + .build( + new CacheLoader<>() { + @Override + public QueryNode load(String query) throws Exception { + return parser.parse(query, ""); + } + } + ); + + public LuceneBasedSearchRule(EnumSet searchFlags) { + super(searchFlags); + } + + @Override + public boolean validateSearchStrings(String query) { + // Using the Lucene parser and checking for an exception is the only way to check whether the query is valid + try { + parser.parse(query, ""); + } catch (QueryNodeException e) { + return false; + } + return true; + } + + @Override + public boolean applyRule(String queryString, BibEntry bibEntry) { + String searchString = queryString; + QueryNode query; + try { + query = cache.get(searchString); + } catch (ExecutionException e) { + LOGGER.error("Could not parse query {}", queryString, e); + return false; + } + + BibQueryVisitor bibQueryVisitor = new BibQueryVisitor(bibEntry, searchFlags); + if (bibQueryVisitor.matchFound(query)) { + return true; + } + + return getFulltextResults(queryString, bibEntry).numSearchResults() > 0; // Didn't match all words. + } + +} diff --git a/src/main/java/org/jabref/model/search/rules/RegexBasedSearchRule.java b/src/main/java/org/jabref/model/search/rules/RegexBasedSearchRule.java index 0cc4bcb1f0a..456cded97be 100644 --- a/src/main/java/org/jabref/model/search/rules/RegexBasedSearchRule.java +++ b/src/main/java/org/jabref/model/search/rules/RegexBasedSearchRule.java @@ -1,23 +1,14 @@ package org.jabref.model.search.rules; -import java.io.IOException; import java.util.EnumSet; -import java.util.List; -import java.util.Locale; import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; -import java.util.stream.Collectors; import org.jabref.architecture.AllowedToUseLogic; -import org.jabref.gui.Globals; -import org.jabref.logic.pdf.search.retrieval.PdfSearcher; -import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.field.Field; -import org.jabref.model.pdf.search.PdfSearchResults; -import org.jabref.model.pdf.search.SearchResult; import org.jabref.model.search.rules.SearchRules.SearchFlags; import org.slf4j.Logger; @@ -27,36 +18,18 @@ * Search rule for regex-based search. */ @AllowedToUseLogic("Because access to the lucene index is needed") -public class RegexBasedSearchRule implements SearchRule { +public class RegexBasedSearchRule extends FullTextSearchRule { - private static final Logger LOGGER = LoggerFactory.getLogger(GrammarBasedSearchRule.class); - - private final EnumSet searchFlags; - - private String lastQuery; - private List lastSearchResults; - - private final BibDatabaseContext databaseContext; + private static final Logger LOGGER = LoggerFactory.getLogger(RegexBasedSearchRule.class); public RegexBasedSearchRule(EnumSet searchFlags) { - this.searchFlags = searchFlags; - - databaseContext = Globals.stateManager.getActiveDatabase().orElse(null); - } - - public EnumSet getSearchFlags() { - return searchFlags; + super(searchFlags); } @Override public boolean validateSearchStrings(String query) { - String searchString = query; - if (!searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE)) { - searchString = searchString.toLowerCase(Locale.ROOT); - } - try { - Pattern.compile(searchString, searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE) ? 0 : Pattern.CASE_INSENSITIVE); + Pattern.compile(query, searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE) ? 0 : Pattern.CASE_INSENSITIVE); } catch (PatternSyntaxException ex) { return false; } @@ -66,10 +39,10 @@ public boolean validateSearchStrings(String query) { @Override public boolean applyRule(String query, BibEntry bibEntry) { Pattern pattern; - try { pattern = Pattern.compile(query, searchFlags.contains(SearchRules.SearchFlags.CASE_SENSITIVE) ? 0 : Pattern.CASE_INSENSITIVE); } catch (PatternSyntaxException ex) { + LOGGER.debug("Could not compile regex {}", query, ex); return false; } @@ -86,24 +59,4 @@ public boolean applyRule(String query, BibEntry bibEntry) { return getFulltextResults(query, bibEntry).numSearchResults() > 0; } - @Override - public PdfSearchResults getFulltextResults(String query, BibEntry bibEntry) { - - if (!searchFlags.contains(SearchRules.SearchFlags.FULLTEXT) || databaseContext == null) { - return new PdfSearchResults(List.of()); - } - - if (!query.equals(this.lastQuery)) { - this.lastQuery = query; - lastSearchResults = List.of(); - try { - PdfSearcher searcher = PdfSearcher.of(databaseContext); - PdfSearchResults results = searcher.search(query, 5); - lastSearchResults = results.getSortedByScore(); - } catch (IOException e) { - LOGGER.error("Could not retrieve search results!", e); - } - } - return new PdfSearchResults(lastSearchResults.stream().filter(searchResult -> searchResult.isResultFor(bibEntry)).collect(Collectors.toList())); - } } diff --git a/src/main/java/org/jabref/model/search/rules/SearchRules.java b/src/main/java/org/jabref/model/search/rules/SearchRules.java index 614c353f834..c949f4af1ee 100644 --- a/src/main/java/org/jabref/model/search/rules/SearchRules.java +++ b/src/main/java/org/jabref/model/search/rules/SearchRules.java @@ -3,9 +3,16 @@ import java.util.EnumSet; import java.util.regex.Pattern; +/** + * This is a factory to instantiate the matching SearchRule implementation matching a given query + */ public class SearchRules { - private static final Pattern SIMPLE_EXPRESSION = Pattern.compile("[^\\p{Punct}]*"); + // In case Lucene keywords are contained, it is not a simple expression any more + private static final Pattern SIMPLE_EXPRESSION = Pattern.compile("^((?! AND | OR )[^\\p{Punct}])*$"); + + // used for checking the syntax of the query + private static LuceneBasedSearchRule luceneBasedSearchRule = new LuceneBasedSearchRule(EnumSet.noneOf(SearchFlags.class)); private SearchRules() { } @@ -15,7 +22,11 @@ private SearchRules() { */ public static SearchRule getSearchRuleByQuery(String query, EnumSet searchFlags) { if (isSimpleQuery(query)) { - return new ContainBasedSearchRule(searchFlags); + return new ContainsBasedSearchRule(searchFlags); + } + + if (luceneBasedSearchRule.validateSearchStrings(query)) { + return new LuceneBasedSearchRule(searchFlags); } // this searches specified fields if specified, @@ -36,7 +47,7 @@ static SearchRule getSearchRule(EnumSet searchFlags) { if (searchFlags.contains(SearchFlags.REGULAR_EXPRESSION)) { return new RegexBasedSearchRule(searchFlags); } else { - return new ContainBasedSearchRule(searchFlags); + return new ContainsBasedSearchRule(searchFlags); } } diff --git a/src/main/resources/l10n/JabRef_en.properties b/src/main/resources/l10n/JabRef_en.properties index 3cffb6fc69f..3c9eec2b060 100644 --- a/src/main/resources/l10n/JabRef_en.properties +++ b/src/main/resources/l10n/JabRef_en.properties @@ -2068,7 +2068,7 @@ Keyword\ delimiter=Keyword delimiter Hierarchical\ keyword\ delimiter=Hierarchical keyword delimiter Escape\ ampersands=Escape ampersands -Hint\:\n\nTo\ search\ all\ fields\ for\ Smith,\ enter\:\nsmith\n\nTo\ search\ the\ field\ author\ for\ Smith\ and\ the\ field\ title\ for\ electrical,\ enter\:\nauthor\=Smith\ and\ title\=electrical=Hint:\n\nTo search all fields for Smith, enter:\nsmith\n\nTo search the field author for Smith and the field title for electrical, enter:\nauthor=Smith and title=electrical +Hint\:\n\nTo\ search\ all\ fields\ for\ Smith,\ enter\:\nsmith\n\nTo\ search\ the\ field\ author\ for\ Smith\ and\ the\ field\ title\ for\ electrical,\ enter\:\nauthor\:Smith\ and\ title\:electrical=Hint:\n\nTo search all fields for Smith, enter:\nsmith\n\nTo search the field author for Smith and the field title for electrical, enter:\nauthor:Smith and title:electrical Copied\ '%0'\ to\ clipboard.=Copied '%0' to clipboard. This\ operation\ requires\ an\ open\ library.=This operation requires an open library. diff --git a/src/test/java/org/jabref/gui/autocompleter/AutoCompletionTest.java b/src/test/java/org/jabref/gui/autocompleter/AutoCompletionTest.java new file mode 100644 index 00000000000..292bdde0c5f --- /dev/null +++ b/src/test/java/org/jabref/gui/autocompleter/AutoCompletionTest.java @@ -0,0 +1,82 @@ +package org.jabref.gui.autocompleter; + +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.layout.HBox; +import javafx.scene.layout.StackPane; +import javafx.stage.Stage; + +import org.controlsfx.control.PopOver; +import org.controlsfx.control.textfield.CustomTextField; +import org.jabref.gui.search.SearchTextField; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.testfx.api.FxRobot; +import org.testfx.framework.junit5.ApplicationExtension; +import org.testfx.framework.junit5.Start; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import org.jabref.model.database.BibDatabase; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.jabref.gui.autocompleter.AutoCompleterUtil.getRequest; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("dropdown") +public class AutoCompletionTest { + + private WordSuggestionProvider autoCompleter; + private BibDatabase database; + BibEntry entry; + private PopOver searchbarDropDown; + private CustomTextField searchField; + + @Start + public void start(Stage stage) { + this.searchbarDropDown = new PopOver(); + this.searchField = SearchTextField.create(); + stage.setScene(new Scene(new StackPane(new HBox(searchField)), 500, 500)); + stage.show(); + } + + @BeforeEach + void setUp() throws Exception { + database = new BibDatabase(); + autoCompleter = new WordSuggestionProvider(StandardField.TITLE, database); + } + + @Test//in progress + void completeSearchbarInDropdown() { //tests autocomplete word in searchbar + // assertThrows(.class, () -> new WordSuggestionProvider(null, database)); + } + + @Test//in progress + void completeWithoutAddingAnythingReturnsSomething() { //tests recommended words from past + Collection result = autoCompleter.provideSuggestions(getRequest(("test"))); + assertEquals(Collections.emptyList(), result); + + } + + @Test//in progress + void completeReturnsMultipleResultsInDropdown() { //tests list of recommended words below searchbar + entry = new BibEntry(); + entry.setField(StandardField.TITLE, "testValueOne"); + database.insertEntry(entry); + searchField.appendText("testValueOne"); + + Collection result = autoCompleter.provideSuggestions(getRequest(("testValue"))); + assertEquals(Arrays.asList("testValueOne", "testValueTwo"), result); + } +} diff --git a/src/test/java/org/jabref/gui/search/DropDownTest.java b/src/test/java/org/jabref/gui/search/DropDownTest.java new file mode 100644 index 00000000000..12511b3ddf6 --- /dev/null +++ b/src/test/java/org/jabref/gui/search/DropDownTest.java @@ -0,0 +1,72 @@ +package org.jabref.gui.search; + +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.layout.HBox; +import javafx.scene.layout.StackPane; +import javafx.stage.Stage; + +import org.controlsfx.control.PopOver; +import org.controlsfx.control.textfield.CustomTextField; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.testfx.api.FxRobot; +import org.testfx.framework.junit5.ApplicationExtension; +import org.testfx.framework.junit5.Start; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@ExtendWith(ApplicationExtension.class) +@Tag("dropdown") +class DropDownTest { + + private PopOver searchbarDropDown; + private Button button; + private Button popOverNotShowing; + private Button buttonText; + private CustomTextField searchField; + + + @Start + public void start(Stage stage) { + this.searchbarDropDown = new PopOver(); + this.button = new Button("button"); + this.popOverNotShowing = new Button("popOverNotShowing"); + this.buttonText = new Button("WriteSomeText"); + this.searchField = SearchTextField.create(); + button.setOnAction(actionEvent -> { + searchbarDropDown.show(button); + }); + popOverNotShowing.setOnAction(actionEvent -> { + popOverNotShowing.setText("popOverGone"); + searchbarDropDown.hide(); + }); + buttonText.setOnAction(actionEvent -> { + searchField.setText("hello"); + }); + stage.setScene(new Scene(new StackPane(new HBox(button, popOverNotShowing, buttonText)), 500, 500)); + stage.show(); + } + + @Test + void testButtonWorking(FxRobot robot) { + robot.clickOn(buttonText); //enter Text into searchField + assertEquals("hello", searchField.getText()); + } + + @Test + void testDropDownShowing(FxRobot robot) { + robot.clickOn(button); //popOver is showing + assertTrue(searchbarDropDown.isShowing()); + } + + @Test + void dropDownNotShowing(FxRobot robot) { + robot.clickOn(button); //popOver is showing + robot.clickOn(popOverNotShowing); //popover hides + assertFalse(searchbarDropDown.isShowing()); + } +} diff --git a/src/test/java/org/jabref/gui/search/SearchFieldSynchronizerAndRecentSearchTest.java b/src/test/java/org/jabref/gui/search/SearchFieldSynchronizerAndRecentSearchTest.java new file mode 100644 index 00000000000..78e9ad27658 --- /dev/null +++ b/src/test/java/org/jabref/gui/search/SearchFieldSynchronizerAndRecentSearchTest.java @@ -0,0 +1,178 @@ +package org.jabref.gui.search; + +import java.util.ArrayList; +import java.util.Arrays; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.embed.swing.JFXPanel; +import javafx.scene.control.ListView; +import javafx.stage.Stage; + +import org.jabref.gui.Globals; +import org.jabref.gui.JabRefFrame; +import org.jabref.gui.StateManager; +import org.jabref.gui.undo.CountingUndoManager; +import org.jabref.preferences.PreferencesService; + +import org.controlsfx.control.textfield.CustomTextField; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("fast") +public class SearchFieldSynchronizerAndRecentSearchTest { + CustomTextField searchField; + SearchFieldSynchronizer searchFieldSynchronizer = new SearchFieldSynchronizer(searchField); + + @Test + void searchStringBuilderBuildsMixedStringCorrectly() { + JFXPanel fxPanel = new JFXPanel(); + searchField = new CustomTextField(); + searchFieldSynchronizer = new SearchFieldSynchronizer(searchField); + SearchItem item1 = new SearchItem("attribute","author:"); + SearchItem item2 = new SearchItem("query","julian"); + SearchItem item3 = new SearchItem("logical","OR"); + SearchItem item4 = new SearchItem("attribute","title:"); + SearchItem item5 = new SearchItem("query","Algebra"); + + searchFieldSynchronizer.addSearchItem(item1); + searchFieldSynchronizer.addSearchItem(item2); + searchFieldSynchronizer.addSearchItem(item3); + searchFieldSynchronizer.addSearchItem(item4); + searchFieldSynchronizer.addSearchItem(item5); + + String searchString = searchFieldSynchronizer.searchStringBuilder(); + String trueSearchString = "author:julian OR title:Algebra"; + System.out.println(searchString); + assertEquals(trueSearchString, searchString); + } + + @Test + void textFieldToListTest() { + JFXPanel fxPanel = new JFXPanel(); + searchField = new CustomTextField(); + searchFieldSynchronizer = new SearchFieldSynchronizer(searchField); + searchField.setText("author:Jon OR title:\"Software Engineering\""); + ArrayList list = searchFieldSynchronizer.textFieldToList(); + + String[] expectedList = new String[5]; + expectedList[0] = "author:"; + expectedList[1] = "Jon"; + expectedList[2] = "OR"; + expectedList[3] = "title:"; + expectedList[4] = "\"Software Engineering\""; + assertEquals(new ArrayList(Arrays.stream(expectedList).toList()), list); + + } + + @Test + void bracketsBalancedTest() { + searchFieldSynchronizer = new SearchFieldSynchronizer(searchField); + SearchItem item1 = new SearchItem("bracket", "("); + SearchItem item2 = new SearchItem("bracket", "("); + SearchItem item3 = new SearchItem("bracket", "("); + SearchItem item4 = new SearchItem("bracket", ")"); + SearchItem item5 = new SearchItem("bracket", ")"); + SearchItem item6 = new SearchItem("bracket", ")"); + + ObservableList searchItemList = FXCollections.observableList(new ArrayList()); + searchItemList.add(item1); + searchItemList.add(item2); + searchItemList.add(item3); + searchItemList.add(item4); + searchItemList.add(item5); + searchItemList.add(item6); + + assertTrue(searchFieldSynchronizer.bracketsBalanced(searchItemList)); + + SearchItem item7 = new SearchItem("bracket", ")"); + searchItemList.add(item7); + + assertFalse(searchFieldSynchronizer.bracketsBalanced(searchItemList)); + } + + @Test + void RecentSearchRemovesDuplicates() { + Stage mainStage = new Stage(); + JabRefFrame frame = new JabRefFrame(mainStage); + StateManager stateManager = new StateManager(); + PreferencesService preferencesService = Globals.prefs; + CountingUndoManager undoManager = new CountingUndoManager(); + GlobalSearchBar globalSearchBar = new GlobalSearchBar(frame, stateManager, preferencesService, undoManager); + RecentSearch recentSearch = new RecentSearch(globalSearchBar); + + recentSearch.add("author:John"); + recentSearch.add("Software Engineering"); + recentSearch.add("title:programming"); + recentSearch.add("author:John"); + recentSearch.add("Software Engineering"); + + + ListView RecentSearches = new ListView<>(); + RecentSearches.getItems().add("author:John"); + RecentSearches.getItems().add("Software Engineering"); + RecentSearches.getItems().add("title:programming"); + + assertEquals(recentSearch.getList().getItems().toString(), RecentSearches.getItems().toString()); + } + + @Test + void SearchBarHighlightingWorks() { + JFXPanel fxPanel = new JFXPanel(); + searchField = new CustomTextField(); + searchFieldSynchronizer = new SearchFieldSynchronizer(searchField); + searchField.clear(); + searchField.setStyle("-fx-border-color: blue"); + + // correct syntax + searchField.setText("author:testauthor AND title:TestTitle"); + + searchFieldSynchronizer.updateSearchItemList(searchFieldSynchronizer.textFieldToList()); + searchFieldSynchronizer.syntaxHighlighting(); + assertEquals("-fx-border-color: green", searchField.getStyle()); + + // wrong syntax + searchField.setText("AND author:test"); + + searchFieldSynchronizer.updateSearchItemList(searchFieldSynchronizer.textFieldToList()); + searchFieldSynchronizer.syntaxHighlighting(); + assertEquals("-fx-border-color: red", searchField.getStyle()); + } + + @Test + void addItemDoesNotCreateInvalidSearch() { + JFXPanel fxPanel = new JFXPanel(); + searchField = new CustomTextField(); + searchFieldSynchronizer = new SearchFieldSynchronizer(searchField); + + SearchItem item1 = new SearchItem("logical","AND"); + SearchItem item2 = new SearchItem("logical", "OR"); + + searchFieldSynchronizer.addSearchItem(item1); + searchFieldSynchronizer.addSearchItem(item2); + + assertTrue(searchFieldSynchronizer.searchItemList.isEmpty()); + + } + + @Test + void returnLatestReallyReturnsLatest() { + JFXPanel fxPanel = new JFXPanel(); + searchField = new CustomTextField(); + searchFieldSynchronizer = new SearchFieldSynchronizer(searchField); + + searchFieldSynchronizer.addSearchItem("query","one"); + searchFieldSynchronizer.addSearchItem("query","two"); + + SearchItem Three = new SearchItem("query", "three"); + searchFieldSynchronizer.addSearchItem(Three); + + assertEquals(Three, searchFieldSynchronizer.returnLatest(searchFieldSynchronizer.searchItemList)); + + } +} + diff --git a/src/test/java/org/jabref/gui/search/SearchFieldSynchronizerTest.java b/src/test/java/org/jabref/gui/search/SearchFieldSynchronizerTest.java new file mode 100644 index 00000000000..2723d5353a7 --- /dev/null +++ b/src/test/java/org/jabref/gui/search/SearchFieldSynchronizerTest.java @@ -0,0 +1,149 @@ +package org.jabref.gui.search; + +import java.util.ArrayList; + +import org.controlsfx.control.textfield.CustomTextField; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Tag("dropdown") +class SearchFieldSynchronizerTest { + private CustomTextField searchField; + + @Test + void testGetSearchString() { + // tests whether the actual text in the search field matches the one being recalled + searchField = SearchTextField.create(); + searchField.setText("author:Ruh AND year:2015 OR title:Corona"); + SearchFieldSynchronizer sync = new SearchFieldSynchronizer(searchField); + assertEquals(sync.getSearchString(), searchField.getText()); + } + + @Test + void testSearchStringBuilder() { + // tests whether the searchStringBuilder() method parses the correct string from searchItemList + // TODO: currently AND/OR are stored in itemType, hence the test fails + searchField = SearchTextField.create(); + searchField.setText(""); + SearchFieldSynchronizer sync = new SearchFieldSynchronizer(searchField); + sync.searchItemList.add(new SearchItem("author:", "Ruh")); + sync.searchItemList.add(new SearchItem("logicalOperator", "AND")); + sync.searchItemList.add(new SearchItem("year:", "2015")); + sync.searchItemList.add(new SearchItem("logicalOperator", "OR")); + sync.searchItemList.add(new SearchItem("title:", "Corona")); + assertEquals(sync.searchStringBuilder(), "author:Ruh AND year:2015 OR title:Corona"); + } + + @Test + void testSearchItemList() { + // tests whether the searchItemList is constructed correctly TODO: overhaul itemType + // TODO: currently AND/OR are stored in itemType, hence the test fails + searchField = SearchTextField.create(); + searchField.setText(""); + SearchFieldSynchronizer sync = new SearchFieldSynchronizer(searchField); + sync.searchItemList.add(new SearchItem("author:", "Ruh")); + sync.searchItemList.add(new SearchItem("logicalOperator", "AND")); + sync.searchItemList.add(new SearchItem("year:", "2015")); + sync.searchItemList.add(new SearchItem("logicalOperator", "OR")); + sync.searchItemList.add(new SearchItem("title:", "Corona")); + assertEquals(5, sync.searchItemList.size()); + assertEquals("author:", sync.searchItemList.get(0).getItemType()); + assertEquals("Ruh", sync.searchItemList.get(0).getItem()); + assertEquals("logicalOperator", sync.searchItemList.get(1).getItemType()); + assertEquals("AND", sync.searchItemList.get(1).getItem()); + assertEquals("year:", sync.searchItemList.get(2).getItemType()); + assertEquals("2015", sync.searchItemList.get(2).getItem()); + assertEquals("logicalOperator", sync.searchItemList.get(3).getItemType()); + assertEquals("OR", sync.searchItemList.get(3).getItem()); + assertEquals("title:", sync.searchItemList.get(4).getItemType()); + assertEquals("Corona", sync.searchItemList.get(4).getItem()); + } + + @Test + void testUpdateSearchItemList() { + // tests whether the method can parse the text from the search field correctly onto the searchItemList + searchField = SearchTextField.create(); + searchField.setText("author:Ruh AND year:2015 OR title:Corona"); + SearchFieldSynchronizer sync = new SearchFieldSynchronizer(searchField); + + String str = searchField.getText(); + String[]words = str.split("(?<=:)|\\ "); + ArrayList list = new ArrayList<>(); + + for (int i = 0; i < words.length; i++) { + if (words[i].startsWith("\"")) { + boolean isWordAfterwards = i + 1 < words.length; + if (isWordAfterwards && words[i + 1].endsWith("\"") && !words[i].endsWith(":")) { + String str2 = words[i] + " " + words[i + 1]; + list.add(str2); + i++; + } else { + list.add(words[i]); + } + } else { + list.add(words[i]); + } + } + + sync.updateSearchItemList(list); + + assertEquals("author:", sync.searchItemList.get(0).getItemType()); + assertEquals("Ruh", sync.searchItemList.get(0).getItem()); + assertEquals("logicalOperator", sync.searchItemList.get(1).getItemType()); + assertEquals("AND", sync.searchItemList.get(1).getItem()); + assertEquals("year:", sync.searchItemList.get(2).getItemType()); + assertEquals("2015", sync.searchItemList.get(2).getItem()); + assertEquals("logicalOperator", sync.searchItemList.get(1).getItemType()); + assertEquals("OR", sync.searchItemList.get(1).getItem()); + assertEquals("title:", sync.searchItemList.get(2).getItemType()); + assertEquals("Corona", sync.searchItemList.get(2).getItem()); + } + + @Deprecated + @Test + void testIsPrevAttribute() { + // deprecated + searchField = SearchTextField.create(); + searchField.setText(""); + SearchFieldSynchronizer sync = new SearchFieldSynchronizer(searchField); + sync.searchItemList.add(new SearchItem("author:", "Ruh")); + assertEquals(true, sync.isPrevAttribute()); + + sync.searchItemList.add(new SearchItem("logicalOperator", "AND")); + assertEquals(false, sync.isPrevAttribute()); + + sync.searchItemList.add(new SearchItem("year:", "2015")); + assertEquals(true, sync.isPrevAttribute()); + + sync.searchItemList.add(new SearchItem("logicalOperator", "OR")); + assertEquals(false, sync.isPrevAttribute()); + + sync.searchItemList.add(new SearchItem("title:", "Corona")); + assertEquals(true, sync.isPrevAttribute()); + } + + @Deprecated + @Test + void testIsPrevOperator() { + // deprecated + searchField = SearchTextField.create(); + searchField.setText(""); + SearchFieldSynchronizer sync = new SearchFieldSynchronizer(searchField); + sync.searchItemList.add(new SearchItem("author:", "Ruh")); + assertEquals(true, sync.isPrevOperator()); + + sync.searchItemList.add(new SearchItem("logicalOperator", "AND")); + assertEquals(false, sync.isPrevOperator()); + + sync.searchItemList.add(new SearchItem("year:", "2015")); + assertEquals(true, sync.isPrevOperator()); + + sync.searchItemList.add(new SearchItem("logicalOperator", "OR")); + assertEquals(false, sync.isPrevOperator()); + + sync.searchItemList.add(new SearchItem("title:", "Corona")); + assertEquals(true, sync.isPrevOperator()); + } +} diff --git a/src/test/java/org/jabref/logic/l10n/LocalizationTest.java b/src/test/java/org/jabref/logic/l10n/LocalizationTest.java index 596139f8b5d..69d7dd00f4a 100644 --- a/src/test/java/org/jabref/logic/l10n/LocalizationTest.java +++ b/src/test/java/org/jabref/logic/l10n/LocalizationTest.java @@ -51,7 +51,7 @@ void testKnownGermanTranslation() { @Test void newLineIsAvailableAndKeptUnescaped() { Localization.setLanguage(Language.ENGLISH); - assertEquals("Hint: To search specific fields only, enter for example:\nauthor=smith and title=electrical", Localization.lang("Hint: To search specific fields only, enter for example:\nauthor=smith and title=electrical")); + assertEquals("Hint: To search specific fields only, enter for example:\nauthor:smith and title:electrical", Localization.lang("Hint: To search specific fields only, enter for example:\nauthor:ssmith and title:electrical")); } @Test diff --git a/src/test/java/org/jabref/model/search/rules/ContainBasedSearchRuleTest.java b/src/test/java/org/jabref/model/search/rules/ContainBasedSearchRuleTest.java deleted file mode 100644 index 337263b0dbc..00000000000 --- a/src/test/java/org/jabref/model/search/rules/ContainBasedSearchRuleTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.jabref.model.search.rules; - -import java.util.EnumSet; - -import org.jabref.model.entry.BibEntry; -import org.jabref.model.entry.field.StandardField; -import org.jabref.model.entry.types.StandardEntryType; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * Test case for ContainBasedSearchRule. - */ -public class ContainBasedSearchRuleTest { - - @Test - public void testBasicSearchParsing() { - BibEntry be = makeBibtexEntry(); - ContainBasedSearchRule bsCaseSensitive = new ContainBasedSearchRule(EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); - ContainBasedSearchRule bsCaseInsensitive = new ContainBasedSearchRule(EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)); - RegexBasedSearchRule bsCaseSensitiveRegexp = new RegexBasedSearchRule(EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); - RegexBasedSearchRule bsCaseInsensitiveRegexp = new RegexBasedSearchRule(EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)); - - String query = "marine 2001 shields"; - - assertFalse(bsCaseSensitive.applyRule(query, be)); - assertTrue(bsCaseInsensitive.applyRule(query, be)); - assertFalse(bsCaseSensitiveRegexp.applyRule(query, be)); - assertFalse(bsCaseInsensitiveRegexp.applyRule(query, be)); - - query = "\"marine larviculture\""; - - assertFalse(bsCaseSensitive.applyRule(query, be)); - assertFalse(bsCaseInsensitive.applyRule(query, be)); - assertFalse(bsCaseSensitiveRegexp.applyRule(query, be)); - assertFalse(bsCaseInsensitiveRegexp.applyRule(query, be)); - - query = "marine [A-Za-z]* larviculture"; - - assertFalse(bsCaseSensitive.applyRule(query, be)); - assertFalse(bsCaseInsensitive.applyRule(query, be)); - assertFalse(bsCaseSensitiveRegexp.applyRule(query, be)); - assertTrue(bsCaseInsensitiveRegexp.applyRule(query, be)); - } - - public BibEntry makeBibtexEntry() { - return new BibEntry(StandardEntryType.InCollection) - .withCitationKey("shields01") - .withField(StandardField.TITLE, "Marine finfish larviculture in Europe") - .withField(StandardField.YEAR, "2001") - .withField(StandardField.AUTHOR, "Kevin Shields"); - } -} diff --git a/src/test/java/org/jabref/model/search/rules/ContainsBasedSearchRuleTest.java b/src/test/java/org/jabref/model/search/rules/ContainsBasedSearchRuleTest.java new file mode 100644 index 00000000000..ed77afd40be --- /dev/null +++ b/src/test/java/org/jabref/model/search/rules/ContainsBasedSearchRuleTest.java @@ -0,0 +1,58 @@ +package org.jabref.model.search.rules; + +import java.util.EnumSet; + +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.types.StandardEntryType; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test case for ContainBasedSearchRule. + */ +public class ContainsBasedSearchRuleTest { + + private final BibEntry be = new BibEntry(StandardEntryType.InCollection) + .withCitationKey("shields01") + .withField(StandardField.TITLE, "Marine finfish larviculture in Europe") + .withField(StandardField.YEAR, "2001") + .withField(StandardField.AUTHOR, "Kevin Shields"); + private final ContainsBasedSearchRule bsCaseSensitive = new ContainsBasedSearchRule(EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); + private final ContainsBasedSearchRule bsCaseInsensitive = new ContainsBasedSearchRule(EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)); + private final RegexBasedSearchRule bsCaseSensitiveRegexp = new RegexBasedSearchRule(EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE, SearchRules.SearchFlags.REGULAR_EXPRESSION)); + private final RegexBasedSearchRule bsCaseInsensitiveRegexp = new RegexBasedSearchRule(EnumSet.of(SearchRules.SearchFlags.REGULAR_EXPRESSION)); + + @Test + public void testContentOfSingleField() { + String query = "\"marine larviculture\""; + + assertFalse(bsCaseSensitive.applyRule(query, be)); + assertFalse(bsCaseInsensitive.applyRule(query, be)); + assertFalse(bsCaseSensitiveRegexp.applyRule(query, be)); + assertFalse(bsCaseInsensitiveRegexp.applyRule(query, be)); + } + + @Test + public void testContentDistributedOnMultipleFields() { + String query = "marine 2001 shields"; + + assertFalse(bsCaseSensitive.applyRule(query, be)); + assertTrue(bsCaseInsensitive.applyRule(query, be)); + assertFalse(bsCaseSensitiveRegexp.applyRule(query, be)); + assertFalse(bsCaseInsensitiveRegexp.applyRule(query, be)); + } + + @Test + public void testRegularExpressionMatch() { + String query = "marine [A-Za-z]* larviculture"; + + assertFalse(bsCaseSensitive.applyRule(query, be)); + assertFalse(bsCaseInsensitive.applyRule(query, be)); + assertFalse(bsCaseSensitiveRegexp.applyRule(query, be)); + assertTrue(bsCaseInsensitiveRegexp.applyRule(query, be)); + } +} diff --git a/src/test/java/org/jabref/model/search/rules/LuceneBasedSearchRuleTest.java b/src/test/java/org/jabref/model/search/rules/LuceneBasedSearchRuleTest.java new file mode 100644 index 00000000000..a4b804c3e4f --- /dev/null +++ b/src/test/java/org/jabref/model/search/rules/LuceneBasedSearchRuleTest.java @@ -0,0 +1,109 @@ +package org.jabref.model.search.rules; + +import java.util.EnumSet; + +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.types.StandardEntryType; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class LuceneBasedSearchRuleTest { + + private final BibEntry bibEntry = new BibEntry(StandardEntryType.InCollection) + .withCitationKey("shields01") + .withField(StandardField.TITLE, "Marine finfish larviculture in Europe") + .withField(StandardField.YEAR, "2001") + .withField(StandardField.AUTHOR, "Kevin Shields") + .withField(StandardField.GROUPS, "included"); + + private final LuceneBasedSearchRule luceneBasedSearchRuleCaseSensitive = new LuceneBasedSearchRule(EnumSet.of(SearchRules.SearchFlags.CASE_SENSITIVE)); + private final LuceneBasedSearchRule luceneBasedSearchRuleCaseInsensitive = new LuceneBasedSearchRule(EnumSet.noneOf(SearchRules.SearchFlags.class)); + + @ParameterizedTest + @ValueSource(strings = { + "year:2001", + "year=2001", + + // Current JabRef special feature: sub strings are also matched + "title:Marine", + "title=Marine", + + // TODO glob patterns: "title:Marine*" + + "year:2001 title:Marine", + "year=2001 title=Marine", + "year=2001 title:Marine", + + "year:2001 AND title:Marine", + + "year:2001 OR title:Marine", + + "Marine", + + // RegEx syntax of Lucene + "/M[a-z]+e/" + }) + public void findsCaseSensitive(String query) { + assertTrue(luceneBasedSearchRuleCaseSensitive.validateSearchStrings(query)); + assertTrue(luceneBasedSearchRuleCaseSensitive.applyRule(query, bibEntry)); + } + + @ParameterizedTest + @ValueSource(strings = { + "year:2002", + "year=2002", + + // Current JabRef special feature: sub strings are also matched + "title:Marie", + "title=Marie", + + // TODO glob patterns: "title:Marine*" + + "year:2002 title:Marine", + "year=2002 title=Marine", + "year=2002 title:Marine", + + "year:2002 AND title:Marine", + + "year:2002 OR title:Marie", + + "Marie", + + "/M[0-9]+e/", + + // this tests for grouping (indicated the brackets) + "(groups=excluded)", + + // this tests for the NOT operator, grouping and Boolean AND + "NOT(groups=excluded) AND NOT(groups=included)" + }) + public void notFindsCaseSensitive(String query) { + assertTrue(luceneBasedSearchRuleCaseSensitive.validateSearchStrings(query)); + assertFalse(luceneBasedSearchRuleCaseSensitive.applyRule(query, bibEntry)); + } + + @ParameterizedTest + @ValueSource(strings = { + "year:2001", + + "title:Marine", + + "year:2001 title:Marine", + + "year:2001 AND title:Marine", + + "year:2001 OR title:Marine", + + "Marine" + }) + public void findsCaseInSensitive(String query) { + assertTrue(luceneBasedSearchRuleCaseInsensitive.validateSearchStrings(query)); + assertTrue(luceneBasedSearchRuleCaseInsensitive.applyRule(query, bibEntry)); + } + +} diff --git a/src/test/java/org/jabref/model/search/rules/SearchRulesTest.java b/src/test/java/org/jabref/model/search/rules/SearchRulesTest.java new file mode 100644 index 00000000000..5558794adc1 --- /dev/null +++ b/src/test/java/org/jabref/model/search/rules/SearchRulesTest.java @@ -0,0 +1,29 @@ +package org.jabref.model.search.rules; + +import java.util.EnumSet; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +class SearchRulesTest { + + @Test + void simpleLuceneQueryReturnsContainsBasedSearchRule() { + SearchRule searchRule = SearchRules.getSearchRuleByQuery("test", EnumSet.noneOf(SearchRules.SearchFlags.class)); + assertInstanceOf(ContainsBasedSearchRule.class, searchRule); + } + + @Test + void andLuceneQueryReturnsLuceneBasedSearchRule() { + SearchRule searchRule = SearchRules.getSearchRuleByQuery("test AND lucene", EnumSet.noneOf(SearchRules.SearchFlags.class)); + assertInstanceOf(LuceneBasedSearchRule.class, searchRule); + } + + @Test + void simpleFieldedLuceneQueryReturnsLuceneBasedSearchRule() { + SearchRule searchRule = SearchRules.getSearchRuleByQuery("title:test", EnumSet.noneOf(SearchRules.SearchFlags.class)); + assertInstanceOf(LuceneBasedSearchRule.class, searchRule); + } + +}