diff --git a/.gitignore b/.gitignore index ff29930..0091606 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ dist/ nbdist/ .nb-gradle/ .elasticbeanstalk + +### VSCode ### +.vscode/ \ No newline at end of file diff --git a/Readme.md b/Readme.md index dd07c08..b41837c 100644 --- a/Readme.md +++ b/Readme.md @@ -39,7 +39,9 @@ java -jar target/easy-notes-1.0.0.jar Alternatively, you can run the app without packaging it using - ```bash -mvn spring-boot:run +mvn spring-boot:run +or +./mvnw spring-boot:run ``` The app will start running at . @@ -58,6 +60,19 @@ The app defines following CRUD APIs. DELETE /api/notes/{noteId} + GET /api/notes/search + { + "text" : "Operating System", + "limit" : 10 + } + + GET /api/notes/searches + { + "text" : "Operating System fear falling doubts", + "limit" : 10 + } + + You can test them using postman or any other rest client. ## Learn more diff --git a/pom.xml b/pom.xml index baec0d9..c0a0239 100644 --- a/pom.xml +++ b/pom.xml @@ -14,7 +14,7 @@ org.springframework.boot spring-boot-starter-parent - 2.5.5 + 2.6.0 @@ -52,6 +52,22 @@ spring-boot-starter-test test + + org.springframework.boot + spring-boot-starter-security + + + org.elasticsearch + elasticsearch + + + org.springframework.boot + spring-boot-starter-data-elasticsearch + + + org.elasticsearch.client + elasticsearch-rest-high-level-client + diff --git a/src/main/java/com/example/easynotes/config/SecurityConfig.java b/src/main/java/com/example/easynotes/config/SecurityConfig.java new file mode 100644 index 0000000..9dbd59d --- /dev/null +++ b/src/main/java/com/example/easynotes/config/SecurityConfig.java @@ -0,0 +1,32 @@ +package com.example.easynotes.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + +@Configuration +@EnableWebSecurity +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.cors().and().csrf().disable(); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.addAllowedOrigin("*"); // You can replace "*" with specific origins + configuration.addAllowedMethod("*"); + configuration.addAllowedHeader("*"); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } +} diff --git a/src/main/java/com/example/easynotes/controller/NoteController.java b/src/main/java/com/example/easynotes/controller/NoteController.java index 52f2ef0..1eb2742 100644 --- a/src/main/java/com/example/easynotes/controller/NoteController.java +++ b/src/main/java/com/example/easynotes/controller/NoteController.java @@ -2,17 +2,26 @@ import com.example.easynotes.exception.ResourceNotFoundException; import com.example.easynotes.model.Note; +import com.example.easynotes.model.SearchNote; import com.example.easynotes.repository.NoteRepository; +import com.example.easynotes.utils.esInterface; +import com.example.easynotes.utils.textUtils; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; + +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; -/** - * Created by rajeevkumarsingh on 27/06/17. - */ +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@CrossOrigin(origins = "*", allowedHeaders = "*") @RestController @RequestMapping("/api") public class NoteController { @@ -20,22 +29,86 @@ public class NoteController { @Autowired NoteRepository noteRepository; + @Autowired + esInterface es; + + private static final Logger logger = LoggerFactory.getLogger(NoteController.class); + + @CrossOrigin(origins = "*") @GetMapping("/notes") - public List getAllNotes() { - return noteRepository.findAll(); + public List getAllNotes(@RequestParam(defaultValue="10", name="limit") String limit, + @RequestParam(defaultValue="0", name="offset") String offset) { + List notes = noteRepository.findAll(); + List required_notes = new ArrayList(); + int I_limit = Integer.parseInt(limit); + int I_offset = Integer.parseInt(offset); + + for(int i=I_offset; i searchNote(@Valid @RequestBody SearchNote searchNote) { + String text = searchNote.getText(); + List splitTexts = textUtils.splitString(text); + Integer limit = searchNote.getLimit(); + Set resultantNotes = new HashSet(); + for(int i=0; i currentNotes = noteRepository.getNoteBySearchString(splitTexts.get(i)); + for(int j=0;j resultantListNotes = new ArrayList (resultantNotes); + List finalNotesWithLimit = new ArrayList (); + for(int i=0;i searchNoteES(@Valid @RequestBody SearchNote searchNote) { + String text = searchNote.getText(); + List splitTexts = textUtils.splitString(text); + Integer limit = searchNote.getLimit(); + Integer offset = searchNote.getOffset(); + List finalNoteIds = es.getAllUniqueDocNoteIds(splitTexts); + List ans = new ArrayList(); + for(int i=offset;i new ResourceNotFoundException("Note", "id", noteId)); + ans.add(curr); + } + catch (Exception e) { + logger.error("Exception occured while finding Note by id" + e.getMessage()); + continue; + } + } + return ans; + } + + @CrossOrigin(origins = "*") @GetMapping("/notes/{id}") public Note getNoteById(@PathVariable(value = "id") Long noteId) { return noteRepository.findById(noteId) .orElseThrow(() -> new ResourceNotFoundException("Note", "id", noteId)); } + @CrossOrigin(origins = "*") @PutMapping("/notes/{id}") public Note updateNote(@PathVariable(value = "id") Long noteId, @Valid @RequestBody Note noteDetails) { @@ -45,18 +118,30 @@ public Note updateNote(@PathVariable(value = "id") Long noteId, note.setTitle(noteDetails.getTitle()); note.setContent(noteDetails.getContent()); - + try{ + es.deleteDocFromES(note.getId()); + } catch (Exception e) { + logger.error("Exception occured while finding Note by id" + e.getMessage()); + } Note updatedNote = noteRepository.save(note); + es.addToES(updatedNote); return updatedNote; } + @CrossOrigin(origins = "*") @DeleteMapping("/notes/{id}") public ResponseEntity deleteNote(@PathVariable(value = "id") Long noteId) { Note note = noteRepository.findById(noteId) .orElseThrow(() -> new ResourceNotFoundException("Note", "id", noteId)); noteRepository.delete(note); + try{ + es.deleteDocFromES(note.getId()); + } catch (Exception e ) { + logger.error("Some error, probably Document missing" + e.getMessage()); + } return ResponseEntity.ok().build(); } + } diff --git a/src/main/java/com/example/easynotes/model/ESNote.java b/src/main/java/com/example/easynotes/model/ESNote.java new file mode 100644 index 0000000..7654037 --- /dev/null +++ b/src/main/java/com/example/easynotes/model/ESNote.java @@ -0,0 +1,84 @@ +package com.example.easynotes.model; + +// import java.text.SimpleDateFormat; +import java.util.Date; + +// import javax.validation.constraints.NotBlank; + +// import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; +// import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.elasticsearch.annotations.Document; + +// import javax.persistence.*; + +@Document(indexName = "notes", shards = 1, replicas = 0, refreshInterval = "-1") +public class ESNote { + @Id + // @GeneratedValue(strategy = GenerationType.IDENTITY) + private String id; + private Long noteId; + // @NotBlank + private String title; + // @NotBlank + private String content; + private Date createdAt; + private Date updatedAt; + + // public ESNote(Note note) { + // this.setId(note.getId()); + // this.setTitle(note.getTitle()); + // this.setContent(note.getContent()); + // // this.setCreatedAt(note.getCreatedAt()); + // // this.setUpdatedAt(note.getUpdatedAt()); + // } + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public Long getNoteId() { + return noteId; + } + + public void setNoteId(Long NoteId) { + this.noteId = NoteId; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public Date getCreatedAt() { + return createdAt; + } + + public void setCreatedAt() { + Date currentDate = new Date(); + this.createdAt = currentDate; + } + + public Date getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt() { + Date currentDate = new Date(); + this.updatedAt = currentDate; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/easynotes/model/SearchNote.java b/src/main/java/com/example/easynotes/model/SearchNote.java new file mode 100644 index 0000000..b617856 --- /dev/null +++ b/src/main/java/com/example/easynotes/model/SearchNote.java @@ -0,0 +1,21 @@ +package com.example.easynotes.model; + + + +public class SearchNote { + private String text; + private Integer limit; + private Integer offset; + + public String getText() { + return text; + } + + public Integer getLimit() { + return limit; + } + + public Integer getOffset() { + return offset; + } +} diff --git a/src/main/java/com/example/easynotes/repository/NoteESRepository.java b/src/main/java/com/example/easynotes/repository/NoteESRepository.java new file mode 100644 index 0000000..945b26a --- /dev/null +++ b/src/main/java/com/example/easynotes/repository/NoteESRepository.java @@ -0,0 +1,18 @@ +package com.example.easynotes.repository; + +import com.example.easynotes.model.ESNote; + +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface NoteESRepository extends ElasticsearchRepository { + + List findByTitleContainingOrContentContaining(String title, String content); + void deleteByNoteId(Long noteId); + List findByTitleContainingOrContentContainingAndNoteIdIsNotNull(String title, String content); +} + + diff --git a/src/main/java/com/example/easynotes/repository/NoteRepository.java b/src/main/java/com/example/easynotes/repository/NoteRepository.java index 4b7b8b3..429a9fa 100644 --- a/src/main/java/com/example/easynotes/repository/NoteRepository.java +++ b/src/main/java/com/example/easynotes/repository/NoteRepository.java @@ -1,7 +1,11 @@ package com.example.easynotes.repository; import com.example.easynotes.model.Note; + +import java.util.List; + import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; /** @@ -10,5 +14,7 @@ @Repository public interface NoteRepository extends JpaRepository { + @Query("select n from Note n where n.title like %?1% or n.content like %?1%") + List getNoteBySearchString(String query); } diff --git a/src/main/java/com/example/easynotes/utils/elasticSearch.java b/src/main/java/com/example/easynotes/utils/elasticSearch.java new file mode 100644 index 0000000..804d13c --- /dev/null +++ b/src/main/java/com/example/easynotes/utils/elasticSearch.java @@ -0,0 +1,63 @@ +package com.example.easynotes.utils; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.example.easynotes.model.ESNote; +import com.example.easynotes.model.Note; +import com.example.easynotes.repository.NoteESRepository; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Service +public class elasticSearch implements esInterface { + @Autowired + private NoteESRepository noteESRepository; + + private static final Logger logger = LoggerFactory.getLogger(elasticSearch.class); + + public void addToES(Note note) { + ESNote esnote = new ESNote(); + esnote.setNoteId(note.getId()); + esnote.setTitle(note.getTitle()); + esnote.setContent(note.getContent()); + esnote.setCreatedAt(); + esnote.setUpdatedAt(); + noteESRepository.save(esnote); + } + public List getFromES(String text) { + List res = noteESRepository.findByTitleContainingOrContentContaining(text, text); + return res; + } + + public void deleteDocFromES(Long noteId) { + logger.info("Deleting " + noteId.toString() + " from ES"); + noteESRepository.deleteByNoteId(noteId); + } + + private List getDocNotes(String text){ + logger.info("Searching " + text + " in ES documents"); + List esNoteDocs = noteESRepository.findByTitleContainingOrContentContaining(text, text); + logger.info("Found " + String.format("%d", esNoteDocs.size()) + " matching docs in ES documents"); + return esNoteDocs; + } + + public List getAllUniqueDocNoteIds(List splitTexts) { + Set uniqueNoteIds = new HashSet(); + for(int i=0;i esNoteDocs = getDocNotes(splitTexts.get(i)); + for(ESNote esnote: esNoteDocs) { + if(esnote.getNoteId() != null) uniqueNoteIds.add(esnote.getNoteId()); + } + } + logger.info("Found " + String.format("%d", uniqueNoteIds.size()) + " matching docs in ES"); + List uniqueNotesIdsList = new ArrayList (uniqueNoteIds); + return uniqueNotesIdsList; + } +} diff --git a/src/main/java/com/example/easynotes/utils/esInterface.java b/src/main/java/com/example/easynotes/utils/esInterface.java new file mode 100644 index 0000000..a2d1b3c --- /dev/null +++ b/src/main/java/com/example/easynotes/utils/esInterface.java @@ -0,0 +1,16 @@ +package com.example.easynotes.utils; + +import com.example.easynotes.model.Note; + +import java.util.List; + +import com.example.easynotes.model.ESNote; + + +public interface esInterface { + + void addToES(Note note); + List getFromES(String text); + void deleteDocFromES(Long noteId); + List getAllUniqueDocNoteIds(List splitTexts); +} diff --git a/src/main/java/com/example/easynotes/utils/textUtils.java b/src/main/java/com/example/easynotes/utils/textUtils.java new file mode 100644 index 0000000..08d9995 --- /dev/null +++ b/src/main/java/com/example/easynotes/utils/textUtils.java @@ -0,0 +1,30 @@ +package com.example.easynotes.utils; +import java.util.ArrayList; +import java.util.List; + +public class textUtils { + private textUtils() { + + } + + public static List splitString(String text) { + List splitStrings = new ArrayList<>(); + + int start_ptr = 0; + while(start_ptr < text.length()) { + while(start_ptr < text.length() && Character.isWhitespace(text.charAt(start_ptr))) { + start_ptr += 1; + } + String str=""; + int end_ptr = start_ptr; + while(end_ptr < text.length() && !Character.isWhitespace(text.charAt(end_ptr))) { + str = str + text.charAt(end_ptr); + end_ptr += 1; + } + if(str.length() > 0) splitStrings.add(str); + start_ptr = end_ptr; + } + return splitStrings; + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d357d46..72cd050 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,7 +1,7 @@ ## Spring DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties) spring.datasource.url = jdbc:mysql://localhost:3306/notes_app?useSSL=false&serverTimezone=UTC&useLegacyDatetimeCode=false spring.datasource.username = root -spring.datasource.password = callicoder +spring.datasource.password = root_1234 ## Hibernate Properties @@ -10,4 +10,10 @@ spring.datasource.password = callicoder spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect # Hibernate ddl auto (create, create-drop, validate, update) -spring.jpa.hibernate.ddl-auto = update \ No newline at end of file +spring.jpa.hibernate.ddl-auto = update + +# Elasticsearch node +spring.data.elasticsearch.cluster-name=elasticsearch +spring.data.elasticsearch.cluster-nodes=localhost:9200 +spring.data.elasticsearch.repositories.enabled=true +spring.main.allow-bean-definition-overriding=true \ No newline at end of file