Apostolos Fanakis
5 years ago
commit
7d9e806d75
35 changed files with 1897 additions and 0 deletions
@ -0,0 +1,131 @@ |
|||
### JetBrains template |
|||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm |
|||
|
|||
*.iml |
|||
|
|||
## Directory-based project format: |
|||
.idea/ |
|||
/out/ |
|||
|
|||
|
|||
### NetBeans template |
|||
nbproject/private/ |
|||
build/ |
|||
nbbuild/ |
|||
dist/ |
|||
nbdist/ |
|||
nbactions.xml |
|||
nb-configuration.xml |
|||
.nb-gradle/ |
|||
|
|||
|
|||
### Eclipse template |
|||
*.pydevproject |
|||
.metadata |
|||
.gradle |
|||
bin/ |
|||
tmp/ |
|||
*.tmp |
|||
*.bak |
|||
*.swp |
|||
*~.nib |
|||
local.properties |
|||
.settings/ |
|||
.loadpath |
|||
|
|||
# Eclipse Core |
|||
.project |
|||
|
|||
# External tool builders |
|||
.externalToolBuilders/ |
|||
|
|||
# Locally stored "Eclipse launch configurations" |
|||
*.launch |
|||
|
|||
# CDT-specific |
|||
.cproject |
|||
|
|||
# JDT-specific (Eclipse Java Development Tools) |
|||
.classpath |
|||
|
|||
# PDT-specific |
|||
.buildpath |
|||
|
|||
# sbteclipse plugin |
|||
.target |
|||
|
|||
# TeXlipse plugin |
|||
.texlipse |
|||
|
|||
|
|||
### Maven template |
|||
target/ |
|||
pom.xml.tag |
|||
pom.xml.releaseBackup |
|||
pom.xml.versionsBackup |
|||
pom.xml.next |
|||
release.properties |
|||
dependency-reduced-pom.xml |
|||
buildNumber.properties |
|||
.mvn/timing.properties |
|||
|
|||
|
|||
### Linux template |
|||
*~ |
|||
|
|||
# KDE directory preferences |
|||
.directory |
|||
|
|||
# Linux trash folder which might appear on any partition or disk |
|||
.Trash-* |
|||
|
|||
|
|||
### Windows template |
|||
# Windows image file caches |
|||
Thumbs.db |
|||
ehthumbs.db |
|||
|
|||
# Folder config file |
|||
Desktop.ini |
|||
|
|||
# Recycle Bin used on file shares |
|||
$RECYCLE.BIN/ |
|||
|
|||
# Windows Installer files |
|||
*.cab |
|||
*.msi |
|||
*.msm |
|||
*.msp |
|||
|
|||
# Windows shortcuts |
|||
*.lnk |
|||
|
|||
|
|||
### OSX template |
|||
.DS_Store |
|||
.AppleDouble |
|||
.LSOverride |
|||
|
|||
# Icon must end with two \r |
|||
Icon |
|||
|
|||
# Thumbnails |
|||
._* |
|||
|
|||
# Files that might appear in the root of a volume |
|||
.DocumentRevisions-V100 |
|||
.fseventsd |
|||
.Spotlight-V100 |
|||
.TemporaryItems |
|||
.Trashes |
|||
.VolumeIcon.icns |
|||
|
|||
# Directories potentially created on remote AFP share |
|||
.AppleDB |
|||
.AppleDesktop |
|||
Network Trash Folder |
|||
Temporary Items |
|||
.apdisk |
|||
|
|||
/secrets |
|||
/dump* |
@ -0,0 +1,10 @@ |
|||
FROM openjdk:8-jre-alpine as build |
|||
WORKDIR /workspace/app |
|||
|
|||
WORKDIR /topic-starters-app |
|||
COPY ./api/target/topicstarters-api.jar app.jar |
|||
COPY ./run.sh run.sh |
|||
|
|||
RUN chmod +x run.sh |
|||
|
|||
ENTRYPOINT ["./run.sh"] |
@ -0,0 +1,10 @@ |
|||
build: |
|||
@docker-compose -p topicstarters build; |
|||
run: |
|||
@docker-compose -p topicstarters up -d |
|||
stop: |
|||
@docker-compose -p topicstarters down |
|||
clean-data: |
|||
@docker-compose -p topicstarters down -v |
|||
clean-images: |
|||
@docker rmi `docker images -q -f "dangling=true"` |
@ -0,0 +1,107 @@ |
|||
# Thmmy Topic Starters |
|||
> A service that parses all the topics of thmmy.gr into a database and exposes an endpoint for getting filtered pages of them |
|||
|
|||
Thmmy topic starters is an application that crawls all thmmy.gr boards every day at 2 a.m. Information parsed are then saved in a |
|||
postgres database and can be accessed by the endpoint `/api/topicstarters`. |
|||
|
|||
--- |
|||
|
|||
# API endpoint |
|||
|
|||
## View topic starters |
|||
|
|||
``` |
|||
GET /api/topicstarters |
|||
``` |
|||
|
|||
### Parameters |
|||
|
|||
| Name | Type | Description | |
|||
| ----- | ------ | ----------- | |
|||
| user | String | **Optional**. The username or ID of the user. Filters the results by user. | |
|||
| board | String | **Optional**. The title or ID of the board. Filters the results by board. | |
|||
| topic | String | **Optional**. The subject or ID of the topic. Filters the results by topic. | |
|||
|
|||
#### Example |
|||
|
|||
```shell script |
|||
curl --location \ |
|||
--request GET 'localhost:8080/api/topicstarters' \ |
|||
--form 'user=14670' \ |
|||
--form 'board=Ανακοινώσεις και Έκτακτα νέα' \ |
|||
--form 'topic=68000' |
|||
``` |
|||
|
|||
### Response |
|||
|
|||
``` |
|||
Status: 200 OK |
|||
Content-Type: application/json;charset=UTF-8 |
|||
Content-Length: 962 |
|||
Content-Encoding: gzip |
|||
``` |
|||
```json |
|||
{ |
|||
"content": [ |
|||
{ |
|||
"id": "d806599f-ae77-4780-bd3d-510943588054", |
|||
"topicId": 68000, |
|||
"topicUrl": "https://www.thmmy.gr/smf/index.php?topic=68000.0", |
|||
"starterUsername": "Apostolof", |
|||
"starterUrl": "https://www.thmmy.gr/smf/index.php?action=profile;u=14670", |
|||
"starterId": 14670, |
|||
"boardTitle": "Ανακοινώσεις και Έκτακτα νέα", |
|||
"boardUrl": "https://www.thmmy.gr/smf/index.php?board=25.0", |
|||
"boardId": 25, |
|||
"topicSubject": "mTHMMY (alpha version)", |
|||
"numberOfReplies": 175, |
|||
"numberOfViews": 15729 |
|||
} |
|||
], |
|||
"pageable": { |
|||
[...] |
|||
}, |
|||
[...] |
|||
} |
|||
``` |
|||
|
|||
\* part of the response truncated for brevity |
|||
|
|||
--- |
|||
|
|||
# Build docker image |
|||
|
|||
To build the docker image you first need to build the java application for production: |
|||
```shell script |
|||
mvn clean install package |
|||
``` |
|||
|
|||
Define a username, password and database name for the postgres database in the file `./env/topic_starters_postgres.env`. |
|||
An example of what this file might look like is given in `./env/topic_starters_postgres.example.env`. |
|||
|
|||
If you want to get all the topics accessible by a logged-in user (rather that just those publicly available to guests) you also need to create two more files containing the username and password of a user for the application to use. |
|||
* `./secrets/username`: which should contain the username |
|||
* `./secrets/password`: which should contain the password |
|||
|
|||
Then just use the Makefile to handle the build: |
|||
```shell script |
|||
make build |
|||
``` |
|||
|
|||
Run the image using: |
|||
```shell script |
|||
make run |
|||
``` |
|||
|
|||
Stop the container using: |
|||
```shell script |
|||
make stop |
|||
``` |
|||
|
|||
The Makefile also provides targets for cleaning the data and dangling images. |
|||
|
|||
--- |
|||
|
|||
## License |
|||
|
|||
[![Beerware License](https://img.shields.io/badge/license-beerware%20%F0%9F%8D%BA-blue.svg)](https://gitlab.com/Apostolof/flavours-without-borders/blob/master/LICENSE.md) |
@ -0,0 +1,103 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" |
|||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
|||
<parent> |
|||
<artifactId>topicstarter</artifactId> |
|||
<groupId>gr.thmmy.mthmmy.topicstarter</groupId> |
|||
<version>1.0.0</version> |
|||
</parent> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
|
|||
<artifactId>api</artifactId> |
|||
|
|||
<properties> |
|||
<start-class>gr.thmmy.mthmmy.topicstarter.api.TopicStarterApplication</start-class> |
|||
</properties> |
|||
|
|||
<dependencies> |
|||
<!-- Module Dependencies --> |
|||
<dependency> |
|||
<groupId>gr.thmmy.mthmmy.topicstarter</groupId> |
|||
<artifactId>entity</artifactId> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>gr.thmmy.mthmmy.topicstarter</groupId> |
|||
<artifactId>repository</artifactId> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>gr.thmmy.mthmmy.topicstarter</groupId> |
|||
<artifactId>scheduled</artifactId> |
|||
<scope>compile</scope> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>gr.thmmy.mthmmy.topicstarter</groupId> |
|||
<artifactId>service</artifactId> |
|||
</dependency> |
|||
|
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-configuration-processor</artifactId> |
|||
<optional>true</optional> |
|||
</dependency> |
|||
|
|||
<!-- Web Dependencies --> |
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-web</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- Utility Dependencies--> |
|||
<dependency> |
|||
<groupId>org.projectlombok</groupId> |
|||
<artifactId>lombok</artifactId> |
|||
<scope>provided</scope> |
|||
</dependency> |
|||
|
|||
<!-- Database Dependencies --> |
|||
<dependency> |
|||
<groupId>org.postgresql</groupId> |
|||
<artifactId>postgresql</artifactId> |
|||
<scope>runtime</scope> |
|||
</dependency> |
|||
</dependencies> |
|||
|
|||
<build> |
|||
<plugins> |
|||
<plugin> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-maven-plugin</artifactId> |
|||
<configuration> |
|||
<finalName>topicstarters</finalName> |
|||
<classifier>api</classifier> |
|||
<executable>true</executable> |
|||
</configuration> |
|||
</plugin> |
|||
<plugin> |
|||
<groupId>org.apache.maven.plugins</groupId> |
|||
<artifactId>maven-source-plugin</artifactId> |
|||
<executions> |
|||
<execution> |
|||
<id>attach-sources</id> |
|||
<phase>verify</phase> |
|||
<goals> |
|||
<goal>jar</goal> |
|||
</goals> |
|||
</execution> |
|||
</executions> |
|||
</plugin> |
|||
<plugin> |
|||
<groupId>org.apache.maven.plugins</groupId> |
|||
<artifactId>maven-javadoc-plugin</artifactId> |
|||
<executions> |
|||
<execution> |
|||
<id>attach-javadocs</id> |
|||
<phase>verify</phase> |
|||
<goals> |
|||
<goal>jar</goal> |
|||
</goals> |
|||
</execution> |
|||
</executions> |
|||
</plugin> |
|||
</plugins> |
|||
</build> |
|||
</project> |
@ -0,0 +1,18 @@ |
|||
package gr.thmmy.mthmmy.topicstarter.api; |
|||
|
|||
import gr.thmmy.mthmmy.topicstarter.TopicStarterRepositoryConfig; |
|||
import gr.thmmy.mthmmy.topicstarter.entity.TopicStarterEntityConfiguration; |
|||
import gr.thmmy.mthmmy.topicstarter.scheduled.TopicStarterSchedulesConfig; |
|||
import gr.thmmy.mthmmy.topicstarter.service.TopicStarterServiceConfiguration; |
|||
import org.springframework.context.annotation.Configuration; |
|||
import org.springframework.context.annotation.Import; |
|||
|
|||
@Configuration |
|||
@Import({ |
|||
TopicStarterServiceConfiguration.class, |
|||
TopicStarterRepositoryConfig.class, |
|||
TopicStarterEntityConfiguration.class, |
|||
TopicStarterSchedulesConfig.class |
|||
}) |
|||
public class TopicStarterApiConfig { |
|||
} |
@ -0,0 +1,19 @@ |
|||
package gr.thmmy.mthmmy.topicstarter.api; |
|||
|
|||
import org.springframework.boot.SpringApplication; |
|||
import org.springframework.boot.autoconfigure.SpringBootApplication; |
|||
import org.springframework.boot.builder.SpringApplicationBuilder; |
|||
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; |
|||
|
|||
@SpringBootApplication |
|||
public class TopicStarterApplication extends SpringBootServletInitializer { |
|||
|
|||
public static void main(final String... args) { |
|||
SpringApplication.run(TopicStarterApplication.class, args); |
|||
} |
|||
|
|||
@Override |
|||
protected SpringApplicationBuilder configure(final SpringApplicationBuilder application) { |
|||
return application.sources(TopicStarterApplication.class); |
|||
} |
|||
} |
@ -0,0 +1,14 @@ |
|||
package gr.thmmy.mthmmy.topicstarter.api.controller.topic.starter; |
|||
|
|||
import gr.thmmy.mthmmy.topicstarter.entity.topic.TopicStarter; |
|||
import org.springframework.data.domain.Page; |
|||
import org.springframework.data.domain.Pageable; |
|||
import org.springframework.http.ResponseEntity; |
|||
|
|||
public interface TopicStarterController { |
|||
|
|||
ResponseEntity<Page<TopicStarter>> topics(String user, |
|||
String board, |
|||
String topic, |
|||
Pageable pageable); |
|||
} |
@ -0,0 +1,36 @@ |
|||
package gr.thmmy.mthmmy.topicstarter.api.controller.topic.starter; |
|||
|
|||
import gr.thmmy.mthmmy.topicstarter.entity.topic.TopicStarter; |
|||
import gr.thmmy.mthmmy.topicstarter.service.topic.starter.TopicStarterService; |
|||
import lombok.RequiredArgsConstructor; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.data.domain.Page; |
|||
import org.springframework.data.domain.Pageable; |
|||
import org.springframework.http.ResponseEntity; |
|||
import org.springframework.web.bind.annotation.GetMapping; |
|||
import org.springframework.web.bind.annotation.RequestMapping; |
|||
import org.springframework.web.bind.annotation.RequestParam; |
|||
import org.springframework.web.bind.annotation.RestController; |
|||
|
|||
@RestController |
|||
@RequestMapping("/api") |
|||
@RequiredArgsConstructor |
|||
@Slf4j |
|||
public class TopicStarterControllerImpl implements TopicStarterController { |
|||
|
|||
private final TopicStarterService topicStarterService; |
|||
|
|||
@Override |
|||
@GetMapping("/topicstarters") |
|||
public ResponseEntity<Page<TopicStarter>> topics(@RequestParam(required = false) String user, |
|||
@RequestParam(required = false) String board, |
|||
@RequestParam(required = false) String topic, |
|||
final Pageable pageable) { |
|||
|
|||
return topicStarterService |
|||
.getWithFilters(user, board, topic, pageable) |
|||
.onFailure(throwable -> log.error("An error has occurred while processing a GET request", throwable)) |
|||
.map(ResponseEntity::ok) |
|||
.get(); |
|||
} |
|||
} |
@ -0,0 +1,6 @@ |
|||
spring: |
|||
datasource: |
|||
driver-class-name: org.postgresql.Driver |
|||
url: jdbc:postgresql://localhost:5432/${POSTGRES_DB}?ApplicationName=topic-starters |
|||
username: ${POSTGRES_USER} |
|||
password: ${POSTGRES_PASSWORD} |
@ -0,0 +1,42 @@ |
|||
server: |
|||
port: 8080 |
|||
compression: |
|||
enabled: true |
|||
min-response-size: 1024 |
|||
mime-types: application/json,application/xml,text/plain |
|||
|
|||
spring: |
|||
datasource: |
|||
driver-class-name: org.postgresql.Driver |
|||
url: jdbc:postgresql://topic-starters-postgres:5432/${POSTGRES_DB}?ApplicationName=topic-starters |
|||
username: ${POSTGRES_USER} |
|||
password: ${POSTGRES_PASSWORD} |
|||
sql-script-encoding: UTF-8 |
|||
initialization-mode: never |
|||
|
|||
jpa: |
|||
hibernate: |
|||
ddl-auto: validate |
|||
naming: |
|||
implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyComponentPathImpl |
|||
|
|||
properties: |
|||
hibernate: |
|||
format_sql: true |
|||
show_sql: false |
|||
jdbc: |
|||
lob: |
|||
non_contextual_creation: true |
|||
|
|||
flyway: |
|||
enabled: true |
|||
|
|||
servlet: |
|||
multipart: |
|||
enabled: true |
|||
file-size-threshold: 2KB |
|||
max-file-size: 128MB |
|||
max-request-size: 256MB |
|||
|
|||
config: |
|||
additional-location: classpath:/config/development/ |
@ -0,0 +1,42 @@ |
|||
version: '3.2' |
|||
|
|||
services: |
|||
topic_starters_postgres_data: |
|||
image: postgres:10.7 |
|||
container_name: topic-starters-postgres |
|||
expose: |
|||
- "5432" |
|||
volumes: |
|||
- 'topic_starters_postgres_data:/var/lib/postgresql/data' |
|||
env_file: |
|||
- env/topic_starters_postgres.env |
|||
ports: |
|||
- "5432:5432" |
|||
networks: |
|||
- topic-starters-net |
|||
restart: on-failure |
|||
|
|||
topic_starters: |
|||
build: ./ |
|||
container_name: topic-starters-service |
|||
ports: |
|||
- "8080:8080" |
|||
env_file: |
|||
- ./env/topic_starters_postgres.env |
|||
secrets: |
|||
- topic_starters_username |
|||
- topic_starters_password |
|||
networks: |
|||
- topic-starters-net |
|||
restart: on-failure |
|||
|
|||
volumes: |
|||
topic_starters_postgres_data: |
|||
networks: |
|||
topic-starters-net: |
|||
driver: bridge |
|||
secrets: |
|||
topic_starters_username: |
|||
file: ./secrets/username |
|||
topic_starters_password: |
|||
file: ./secrets/password |
@ -0,0 +1,83 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" |
|||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
|||
<parent> |
|||
<artifactId>topicstarter</artifactId> |
|||
<groupId>gr.thmmy.mthmmy.topicstarter</groupId> |
|||
<version>1.0.0</version> |
|||
</parent> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
|
|||
<artifactId>entity</artifactId> |
|||
|
|||
<dependencies> |
|||
<!-- Data Access Dependencies --> |
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-data-jpa</artifactId> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.querydsl</groupId> |
|||
<artifactId>querydsl-jpa</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- Database migration dependencies --> |
|||
<dependency> |
|||
<groupId>org.flywaydb</groupId> |
|||
<artifactId>flyway-core</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- Utility Dependencies--> |
|||
<dependency> |
|||
<groupId>org.projectlombok</groupId> |
|||
<artifactId>lombok</artifactId> |
|||
<scope>provided</scope> |
|||
</dependency> |
|||
</dependencies> |
|||
|
|||
<build> |
|||
<plugins> |
|||
<plugin> |
|||
<groupId>com.mysema.maven</groupId> |
|||
<artifactId>apt-maven-plugin</artifactId> |
|||
<executions> |
|||
<execution> |
|||
<goals> |
|||
<goal>process</goal> |
|||
</goals> |
|||
<configuration> |
|||
<outputDirectory>target/generated-sources/java</outputDirectory> |
|||
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor> |
|||
</configuration> |
|||
</execution> |
|||
</executions> |
|||
</plugin> |
|||
<plugin> |
|||
<groupId>org.apache.maven.plugins</groupId> |
|||
<artifactId>maven-source-plugin</artifactId> |
|||
<executions> |
|||
<execution> |
|||
<id>attach-sources</id> |
|||
<phase>verify</phase> |
|||
<goals> |
|||
<goal>jar</goal> |
|||
</goals> |
|||
</execution> |
|||
</executions> |
|||
</plugin> |
|||
<plugin> |
|||
<groupId>org.apache.maven.plugins</groupId> |
|||
<artifactId>maven-javadoc-plugin</artifactId> |
|||
<executions> |
|||
<execution> |
|||
<id>attach-javadocs</id> |
|||
<phase>verify</phase> |
|||
<goals> |
|||
<goal>jar</goal> |
|||
</goals> |
|||
</execution> |
|||
</executions> |
|||
</plugin> |
|||
</plugins> |
|||
</build> |
|||
</project> |
@ -0,0 +1,23 @@ |
|||
package gr.thmmy.mthmmy.topicstarter.entity; |
|||
|
|||
import lombok.Data; |
|||
|
|||
import javax.persistence.Column; |
|||
import javax.persistence.Id; |
|||
import javax.persistence.MappedSuperclass; |
|||
import javax.persistence.PrePersist; |
|||
import java.util.UUID; |
|||
|
|||
@Data |
|||
@MappedSuperclass |
|||
public abstract class AbstractEntity { |
|||
|
|||
@Id |
|||
@Column(columnDefinition = "UUID") |
|||
protected String id; |
|||
|
|||
@PrePersist |
|||
protected void onCreate() { |
|||
this.id = UUID.randomUUID().toString(); |
|||
} |
|||
} |
@ -0,0 +1,9 @@ |
|||
package gr.thmmy.mthmmy.topicstarter.entity; |
|||
|
|||
import org.springframework.boot.autoconfigure.domain.EntityScan; |
|||
import org.springframework.context.annotation.Configuration; |
|||
|
|||
@Configuration |
|||
@EntityScan |
|||
public class TopicStarterEntityConfiguration { |
|||
} |
@ -0,0 +1,47 @@ |
|||
package gr.thmmy.mthmmy.topicstarter.entity.topic; |
|||
|
|||
import gr.thmmy.mthmmy.topicstarter.entity.AbstractEntity; |
|||
import lombok.Data; |
|||
import lombok.EqualsAndHashCode; |
|||
|
|||
import javax.persistence.Column; |
|||
import javax.persistence.Entity; |
|||
import javax.persistence.Table; |
|||
|
|||
@Data |
|||
@EqualsAndHashCode(callSuper = true) |
|||
@Entity |
|||
@Table(name = "topic_starter") |
|||
public class TopicStarter extends AbstractEntity { |
|||
|
|||
@Column(nullable = false) |
|||
private Long topicId; |
|||
|
|||
@Column(nullable = false) |
|||
private String topicUrl; |
|||
|
|||
@Column(nullable = false) |
|||
private String starterUsername; |
|||
|
|||
private String starterUrl; |
|||
|
|||
private Long starterId; |
|||
|
|||
@Column(nullable = false) |
|||
private String boardTitle; |
|||
|
|||
@Column(nullable = false) |
|||
private String boardUrl; |
|||
|
|||
@Column(nullable = false) |
|||
private Long boardId; |
|||
|
|||
@Column(nullable = false) |
|||
private String topicSubject; |
|||
|
|||
@Column(nullable = false) |
|||
private Long numberOfReplies; |
|||
|
|||
@Column(nullable = false) |
|||
private Long numberOfViews; |
|||
} |
@ -0,0 +1,16 @@ |
|||
create table topic_starter |
|||
( |
|||
id varchar(255) not null |
|||
constraint topic_starter_id_pkey primary key, |
|||
topic_id bigint not null, |
|||
topic_url varchar(255) not null, |
|||
starter_username varchar(255) not null, |
|||
starter_url varchar(255) not null, |
|||
starter_id bigint not null, |
|||
board_title varchar(255) not null, |
|||
board_url varchar(255) not null, |
|||
board_id bigint not null, |
|||
topic_subject varchar(255) not null, |
|||
number_of_replies bigint not null, |
|||
number_of_views bigint not null |
|||
); |
@ -0,0 +1,3 @@ |
|||
POSTGRES_USER=postgres |
|||
POSTGRES_PASSWORD=postgres |
|||
POSTGRES_DB=topic-starters |
@ -0,0 +1,201 @@ |
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?> |
|||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" |
|||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
|
|||
<groupId>gr.thmmy.mthmmy.topicstarter</groupId> |
|||
<artifactId>topicstarter</artifactId> |
|||
<version>1.0.0</version> |
|||
<packaging>pom</packaging> |
|||
|
|||
<parent> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter-parent</artifactId> |
|||
<version>2.1.6.RELEASE</version> |
|||
</parent> |
|||
|
|||
<modules> |
|||
<module>api</module> |
|||
<module>service</module> |
|||
<module>entity</module> |
|||
<module>repository</module> |
|||
<module>scheduled</module> |
|||
</modules> |
|||
|
|||
<properties> |
|||
<buildNumber/> |
|||
<excludePattern/> |
|||
|
|||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> |
|||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> |
|||
|
|||
<java.version>1.8</java.version> |
|||
|
|||
<maven-source-plugin.version>3.0.1</maven-source-plugin.version> |
|||
<maven-javadoc-plugin.version>3.0.1</maven-javadoc-plugin.version> |
|||
<maven-compiler-plugin.version>3.5.1</maven-compiler-plugin.version> |
|||
<apt-maven-plugin.version>1.1.3</apt-maven-plugin.version> |
|||
|
|||
<vavr.version>1.0.0-alpha-3</vavr.version> |
|||
<lombok.version>1.18.8</lombok.version> |
|||
<jsoup.version>1.12.1</jsoup.version> |
|||
<okhttp.version>3.9.0</okhttp.version> |
|||
<jackson.version>2.9.0</jackson.version> |
|||
</properties> |
|||
|
|||
<dependencyManagement> |
|||
<dependencies> |
|||
<!-- App modules --> |
|||
<dependency> |
|||
<groupId>gr.thmmy.mthmmy.topicstarter</groupId> |
|||
<artifactId>api</artifactId> |
|||
<version>${project.version}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>gr.thmmy.mthmmy.topicstarter</groupId> |
|||
<artifactId>entity</artifactId> |
|||
<version>${project.version}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>gr.thmmy.mthmmy.topicstarter</groupId> |
|||
<artifactId>repository</artifactId> |
|||
<version>${project.version}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>gr.thmmy.mthmmy.topicstarter</groupId> |
|||
<artifactId>scheduled</artifactId> |
|||
<version>${project.version}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>gr.thmmy.mthmmy.topicstarter</groupId> |
|||
<artifactId>service</artifactId> |
|||
<version>${project.version}</version> |
|||
</dependency> |
|||
|
|||
<!-- Utils --> |
|||
<dependency> |
|||
<groupId>io.vavr</groupId> |
|||
<artifactId>vavr</artifactId> |
|||
<version>${vavr.version}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>org.projectlombok</groupId> |
|||
<artifactId>lombok</artifactId> |
|||
<version>${lombok.version}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>org.jsoup</groupId> |
|||
<artifactId>jsoup</artifactId> |
|||
<version>${jsoup.version}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.squareup.okhttp3</groupId> |
|||
<artifactId>okhttp</artifactId> |
|||
<version>${okhttp.version}</version> |
|||
</dependency> |
|||
|
|||
<!-- DB --> |
|||
<dependency> |
|||
<groupId>com.querydsl</groupId> |
|||
<artifactId>querydsl-jpa</artifactId> |
|||
<version>${querydsl.version}</version> |
|||
</dependency> |
|||
|
|||
<!-- Other --> |
|||
<dependency> |
|||
<groupId>com.fasterxml.jackson.core</groupId> |
|||
<artifactId>jackson-annotations</artifactId> |
|||
<version>${jackson.version}</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.fasterxml.jackson.datatype</groupId> |
|||
<artifactId>jackson-datatype-jsr310</artifactId> |
|||
<version>${jackson.version}</version> |
|||
</dependency> |
|||
</dependencies> |
|||
</dependencyManagement> |
|||
|
|||
<build> |
|||
<pluginManagement> |
|||
<plugins> |
|||
<plugin> |
|||
<groupId>org.apache.maven.plugins</groupId> |
|||
<artifactId>maven-compiler-plugin</artifactId> |
|||
<version>${maven-compiler-plugin.version}</version> |
|||
</plugin> |
|||
<plugin> |
|||
<groupId>com.mysema.maven</groupId> |
|||
<artifactId>apt-maven-plugin</artifactId> |
|||
<version>${apt-maven-plugin.version}</version> |
|||
<dependencies> |
|||
<dependency> |
|||
<groupId>com.querydsl</groupId> |
|||
<artifactId>querydsl-apt</artifactId> |
|||
<version>${querydsl.version}</version> |
|||
</dependency> |
|||
</dependencies> |
|||
</plugin> |
|||
<plugin> |
|||
<groupId>org.apache.maven.plugins</groupId> |
|||
<artifactId>maven-source-plugin</artifactId> |
|||
<version>${maven-source-plugin.version}</version> |
|||
</plugin> |
|||
<plugin> |
|||
<groupId>org.apache.maven.plugins</groupId> |
|||
<artifactId>maven-javadoc-plugin</artifactId> |
|||
<version>${maven-javadoc-plugin.version}</version> |
|||
</plugin> |
|||
</plugins> |
|||
</pluginManagement> |
|||
</build> |
|||
|
|||
<repositories> |
|||
<repository> |
|||
<id>spring-milestones</id> |
|||
<name>Spring Milestones</name> |
|||
<url>https://repo.spring.io/milestone</url> |
|||
<snapshots> |
|||
<enabled>false</enabled> |
|||
</snapshots> |
|||
</repository> |
|||
<repository> |
|||
<id>spring-snapshots</id> |
|||
<name>Spring Snapshots</name> |
|||
<url>https://repo.spring.io/snapshot</url> |
|||
<snapshots> |
|||
</snapshots> |
|||
</repository> |
|||
|
|||
<repository> |
|||
<id>jitpack.io</id> |
|||
<url>https://jitpack.io</url> |
|||
</repository> |
|||
</repositories> |
|||
|
|||
<pluginRepositories> |
|||
<pluginRepository> |
|||
<snapshots> |
|||
<enabled>false</enabled> |
|||
</snapshots> |
|||
<id>jcenter-central</id> |
|||
<name>bintray-plugins</name> |
|||
<url>https://jcenter.bintray.com</url> |
|||
</pluginRepository> |
|||
|
|||
<pluginRepository> |
|||
<id>spring-milestones</id> |
|||
<name>Spring Milestones</name> |
|||
<url>https://repo.spring.io/milestone</url> |
|||
<snapshots> |
|||
<enabled>false</enabled> |
|||
</snapshots> |
|||
</pluginRepository> |
|||
<pluginRepository> |
|||
<id>spring-snapshots</id> |
|||
<name>Spring Snapshots</name> |
|||
<url>https://repo.spring.io/snapshot</url> |
|||
<snapshots> |
|||
</snapshots> |
|||
</pluginRepository> |
|||
</pluginRepositories> |
|||
</project> |
@ -0,0 +1,74 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" |
|||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
|||
<parent> |
|||
<artifactId>topicstarter</artifactId> |
|||
<groupId>gr.thmmy.mthmmy.topicstarter</groupId> |
|||
<version>1.0.0</version> |
|||
</parent> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
|
|||
<artifactId>repository</artifactId> |
|||
|
|||
<dependencies> |
|||
<!-- Module Dependencies --> |
|||
<dependency> |
|||
<groupId>gr.thmmy.mthmmy.topicstarter</groupId> |
|||
<artifactId>entity</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- Data Querying Dependencies --> |
|||
<dependency> |
|||
<groupId>com.querydsl</groupId> |
|||
<artifactId>querydsl-apt</artifactId> |
|||
<scope>provided</scope> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.querydsl</groupId> |
|||
<artifactId>querydsl-jpa</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- Utility Dependencies--> |
|||
|
|||
<dependency> |
|||
<groupId>org.projectlombok</groupId> |
|||
<artifactId>lombok</artifactId> |
|||
<scope>provided</scope> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>io.vavr</groupId> |
|||
<artifactId>vavr</artifactId> |
|||
</dependency> |
|||
</dependencies> |
|||
|
|||
<build> |
|||
<plugins> |
|||
<plugin> |
|||
<groupId>org.apache.maven.plugins</groupId> |
|||
<artifactId>maven-source-plugin</artifactId> |
|||
<executions> |
|||
<execution> |
|||
<id>attach-sources</id> |
|||
<phase>verify</phase> |
|||
<goals> |
|||
<goal>jar</goal> |
|||
</goals> |
|||
</execution> |
|||
</executions> |
|||
</plugin> |
|||
<plugin> |
|||
<groupId>org.apache.maven.plugins</groupId> |
|||
<artifactId>maven-javadoc-plugin</artifactId> |
|||
<executions> |
|||
<execution> |
|||
<id>attach-javadocs</id> |
|||
<phase>verify</phase> |
|||
<goals> |
|||
<goal>jar</goal> |
|||
</goals> |
|||
</execution> |
|||
</executions> |
|||
</plugin> |
|||
</plugins> |
|||
</build> |
|||
</project> |
@ -0,0 +1,11 @@ |
|||
package gr.thmmy.mthmmy.topicstarter; |
|||
|
|||
import org.springframework.data.jpa.repository.JpaRepository; |
|||
import org.springframework.data.querydsl.QuerydslPredicateExecutor; |
|||
import org.springframework.data.repository.NoRepositoryBean; |
|||
|
|||
import java.util.UUID; |
|||
|
|||
@NoRepositoryBean |
|||
public interface GenericRepository<T> extends JpaRepository<T, UUID>, QuerydslPredicateExecutor<T> { |
|||
} |
@ -0,0 +1,11 @@ |
|||
package gr.thmmy.mthmmy.topicstarter; |
|||
|
|||
import org.springframework.context.annotation.ComponentScan; |
|||
import org.springframework.context.annotation.Configuration; |
|||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories; |
|||
|
|||
@Configuration |
|||
@ComponentScan |
|||
@EnableJpaRepositories |
|||
public class TopicStarterRepositoryConfig { |
|||
} |
@ -0,0 +1,9 @@ |
|||
package gr.thmmy.mthmmy.topicstarter.repository.topic; |
|||
|
|||
import gr.thmmy.mthmmy.topicstarter.GenericRepository; |
|||
import gr.thmmy.mthmmy.topicstarter.entity.topic.TopicStarter; |
|||
import org.springframework.stereotype.Repository; |
|||
|
|||
@Repository |
|||
public interface TopicStarterRepository extends GenericRepository<TopicStarter> { |
|||
} |
@ -0,0 +1,8 @@ |
|||
#!/bin/sh |
|||
|
|||
topic_starters_username=$(cat /run/secrets/topic_starters_username) |
|||
topic_starters_password=$(cat /run/secrets/topic_starters_password) |
|||
|
|||
java -DTOPIC_STARTERS_USERNAME="$topic_starters_username" \ |
|||
-DTOPIC_STARTERS_PASSWORD="$topic_starters_password" \ |
|||
-jar app.jar gr.thmmy.mthmmy.topicstarter.api.TopicStarterApplication |
@ -0,0 +1,89 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" |
|||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
|
|||
<parent> |
|||
<artifactId>topicstarter</artifactId> |
|||
<groupId>gr.thmmy.mthmmy.topicstarter</groupId> |
|||
<version>1.0.0</version> |
|||
</parent> |
|||
|
|||
<artifactId>scheduled</artifactId> |
|||
|
|||
<dependencies> |
|||
<!-- Module Dependencies --> |
|||
|
|||
|
|||
<!-- Basic Spring Dependencies --> |
|||
|
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter</artifactId> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-configuration-processor</artifactId> |
|||
<optional>true</optional> |
|||
</dependency> |
|||
|
|||
<!-- Utility Dependencies--> |
|||
|
|||
<dependency> |
|||
<groupId>org.projectlombok</groupId> |
|||
<artifactId>lombok</artifactId> |
|||
<scope>provided</scope> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>io.vavr</groupId> |
|||
<artifactId>vavr</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- Template Compiler --> |
|||
|
|||
<dependency> |
|||
<groupId>com.github.spullara.mustache.java</groupId> |
|||
<artifactId>compiler</artifactId> |
|||
<version>0.9.5</version> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>gr.thmmy.mthmmy.topicstarter</groupId> |
|||
<artifactId>entity</artifactId> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>gr.thmmy.mthmmy.topicstarter</groupId> |
|||
<artifactId>service</artifactId> |
|||
</dependency> |
|||
</dependencies> |
|||
|
|||
<build> |
|||
<plugins> |
|||
<plugin> |
|||
<groupId>org.apache.maven.plugins</groupId> |
|||
<artifactId>maven-source-plugin</artifactId> |
|||
<executions> |
|||
<execution> |
|||
<id>attach-sources</id> |
|||
<phase>verify</phase> |
|||
<goals> |
|||
<goal>jar</goal> |
|||
</goals> |
|||
</execution> |
|||
</executions> |
|||
</plugin> |
|||
<plugin> |
|||
<groupId>org.apache.maven.plugins</groupId> |
|||
<artifactId>maven-javadoc-plugin</artifactId> |
|||
<executions> |
|||
<execution> |
|||
<id>attach-javadocs</id> |
|||
<phase>verify</phase> |
|||
<goals> |
|||
<goal>jar</goal> |
|||
</goals> |
|||
</execution> |
|||
</executions> |
|||
</plugin> |
|||
</plugins> |
|||
</build> |
|||
</project> |
@ -0,0 +1,36 @@ |
|||
package gr.thmmy.mthmmy.topicstarter.scheduled; |
|||
|
|||
import gr.thmmy.mthmmy.topicstarter.service.topic.starter.parser.TopicStarterParserService; |
|||
import lombok.Data; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.scheduling.annotation.Scheduled; |
|||
import org.springframework.stereotype.Component; |
|||
|
|||
import javax.annotation.PostConstruct; |
|||
|
|||
@Data |
|||
@Slf4j |
|||
@Component |
|||
public class TopicStarterScheduled { |
|||
|
|||
private final TopicStarterParserService topicStarterParserService; |
|||
|
|||
// Runs at 02:00am every day of every month
|
|||
@Scheduled(cron = "0 0 02 * * *") |
|||
public void run() throws Exception { |
|||
|
|||
topicStarterParserService |
|||
.parseTopicStarters() |
|||
.onFailure(throwable -> log.error("An error has occurred while processing a GET request", throwable)) |
|||
.get(); |
|||
} |
|||
|
|||
@PostConstruct |
|||
public void init() { |
|||
|
|||
topicStarterParserService |
|||
.parseTopicStarters() |
|||
.onFailure(throwable -> log.error("An error has occurred while processing a GET request", throwable)) |
|||
.get(); |
|||
} |
|||
} |
@ -0,0 +1,11 @@ |
|||
package gr.thmmy.mthmmy.topicstarter.scheduled; |
|||
|
|||
import org.springframework.context.annotation.ComponentScan; |
|||
import org.springframework.context.annotation.Configuration; |
|||
import org.springframework.scheduling.annotation.EnableScheduling; |
|||
|
|||
@Configuration |
|||
@ComponentScan |
|||
@EnableScheduling |
|||
public class TopicStarterSchedulesConfig { |
|||
} |
@ -0,0 +1,93 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0" |
|||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
|||
<modelVersion>4.0.0</modelVersion> |
|||
|
|||
<parent> |
|||
<artifactId>topicstarter</artifactId> |
|||
<groupId>gr.thmmy.mthmmy.topicstarter</groupId> |
|||
<version>1.0.0</version> |
|||
</parent> |
|||
|
|||
<artifactId>service</artifactId> |
|||
|
|||
<dependencies> |
|||
<!-- Module Dependencies --> |
|||
<dependency> |
|||
<groupId>gr.thmmy.mthmmy.topicstarter</groupId> |
|||
<artifactId>entity</artifactId> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>gr.thmmy.mthmmy.topicstarter</groupId> |
|||
<artifactId>repository</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- Basic Spring Dependencies --> |
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-starter</artifactId> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>org.springframework.boot</groupId> |
|||
<artifactId>spring-boot-configuration-processor</artifactId> |
|||
<optional>true</optional> |
|||
</dependency> |
|||
|
|||
<!-- Utility Dependencies--> |
|||
<dependency> |
|||
<groupId>org.projectlombok</groupId> |
|||
<artifactId>lombok</artifactId> |
|||
<scope>provided</scope> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>io.vavr</groupId> |
|||
<artifactId>vavr</artifactId> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>org.jsoup</groupId> |
|||
<artifactId>jsoup</artifactId> |
|||
</dependency> |
|||
<dependency> |
|||
<groupId>com.squareup.okhttp3</groupId> |
|||
<artifactId>okhttp</artifactId> |
|||
</dependency> |
|||
|
|||
<!-- Template Compiler --> |
|||
<dependency> |
|||
<groupId>com.github.spullara.mustache.java</groupId> |
|||
<artifactId>compiler</artifactId> |
|||
<version>0.9.5</version> |
|||
</dependency> |
|||
</dependencies> |
|||
|
|||
<build> |
|||
<plugins> |
|||
<plugin> |
|||
<groupId>org.apache.maven.plugins</groupId> |
|||
<artifactId>maven-source-plugin</artifactId> |
|||
<executions> |
|||
<execution> |
|||
<id>attach-sources</id> |
|||
<phase>verify</phase> |
|||
<goals> |
|||
<goal>jar</goal> |
|||
</goals> |
|||
</execution> |
|||
</executions> |
|||
</plugin> |
|||
<plugin> |
|||
<groupId>org.apache.maven.plugins</groupId> |
|||
<artifactId>maven-javadoc-plugin</artifactId> |
|||
<executions> |
|||
<execution> |
|||
<id>attach-javadocs</id> |
|||
<phase>verify</phase> |
|||
<goals> |
|||
<goal>jar</goal> |
|||
</goals> |
|||
</execution> |
|||
</executions> |
|||
</plugin> |
|||
</plugins> |
|||
</build> |
|||
</project> |
@ -0,0 +1,46 @@ |
|||
package gr.thmmy.mthmmy.topicstarter.service; |
|||
|
|||
import io.vavr.control.Try; |
|||
import okhttp3.Cookie; |
|||
import okhttp3.CookieJar; |
|||
import okhttp3.HttpUrl; |
|||
import okhttp3.OkHttpClient; |
|||
import org.springframework.context.annotation.Bean; |
|||
import org.springframework.context.annotation.ComponentScan; |
|||
import org.springframework.context.annotation.Configuration; |
|||
import org.springframework.lang.NonNull; |
|||
|
|||
import java.util.ArrayList; |
|||
import java.util.concurrent.TimeUnit; |
|||
|
|||
@Configuration |
|||
@ComponentScan |
|||
public class TopicStarterServiceConfiguration { |
|||
|
|||
@Bean |
|||
public OkHttpClient getClient() { |
|||
|
|||
return Try |
|||
.of(() -> new CookieJar() { |
|||
private final java.util.List<Cookie> cookieStore = new ArrayList<>(); |
|||
|
|||
@Override |
|||
public void saveFromResponse(@NonNull HttpUrl url, @NonNull java.util.List<Cookie> cookies) { |
|||
cookieStore.addAll(cookies); |
|||
} |
|||
|
|||
@Override |
|||
public java.util.List<Cookie> loadForRequest(@NonNull HttpUrl url) { |
|||
return cookieStore; |
|||
} |
|||
} |
|||
).map(cookieJar -> new OkHttpClient.Builder() |
|||
.cookieJar(cookieJar) |
|||
.connectTimeout(30, TimeUnit.SECONDS) |
|||
.writeTimeout(30, TimeUnit.SECONDS) |
|||
.readTimeout(30, TimeUnit.SECONDS) |
|||
.retryOnConnectionFailure(true) |
|||
.build()) |
|||
.get(); |
|||
} |
|||
} |
@ -0,0 +1,14 @@ |
|||
package gr.thmmy.mthmmy.topicstarter.service.topic.starter; |
|||
|
|||
import gr.thmmy.mthmmy.topicstarter.entity.topic.TopicStarter; |
|||
import io.vavr.control.Try; |
|||
import org.springframework.data.domain.Page; |
|||
import org.springframework.data.domain.Pageable; |
|||
|
|||
public interface TopicStarterService { |
|||
|
|||
Try<Page<TopicStarter>> getWithFilters(String user, |
|||
String board, |
|||
String topic, |
|||
Pageable pageable); |
|||
} |
@ -0,0 +1,116 @@ |
|||
package gr.thmmy.mthmmy.topicstarter.service.topic.starter; |
|||
|
|||
import com.querydsl.core.types.dsl.BooleanExpression; |
|||
import com.querydsl.core.types.dsl.Expressions; |
|||
import gr.thmmy.mthmmy.topicstarter.entity.topic.QTopicStarter; |
|||
import gr.thmmy.mthmmy.topicstarter.entity.topic.TopicStarter; |
|||
import gr.thmmy.mthmmy.topicstarter.repository.topic.TopicStarterRepository; |
|||
import io.vavr.control.Option; |
|||
import io.vavr.control.Try; |
|||
import lombok.Data; |
|||
import lombok.RequiredArgsConstructor; |
|||
import org.springframework.data.domain.Page; |
|||
import org.springframework.data.domain.Pageable; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
import javax.annotation.Nonnull; |
|||
import javax.annotation.Nullable; |
|||
|
|||
import static java.util.Objects.requireNonNull; |
|||
|
|||
@RequiredArgsConstructor |
|||
@Data |
|||
@Service |
|||
public class TopicStarterServiceImpl implements TopicStarterService { |
|||
|
|||
private final TopicStarterRepository topicStarterRepository; |
|||
|
|||
@Override |
|||
public Try<Page<TopicStarter>> getWithFilters(final @Nullable String user, |
|||
final @Nullable String board, |
|||
final @Nullable String topic, |
|||
final Pageable pageable) { |
|||
requireNonNull(pageable, "pageable is null"); |
|||
|
|||
return Try |
|||
.of(() -> Expressions.asBoolean(true).isTrue()) |
|||
.flatMap(topicStarterPredicate -> Option |
|||
.of(user) |
|||
.map(userNotNull -> extractId(userNotNull) |
|||
.toTry() |
|||
.flatMap(this::getUserIdPredicate) |
|||
.orElse(getUsernamePredicate(userNotNull)) |
|||
.map(topicStarterPredicate::and)) |
|||
.getOrElse(Try.success(topicStarterPredicate)) |
|||
) |
|||
.flatMap(topicStarterPredicate -> Option |
|||
.of(board) |
|||
.map(boardNotNull -> extractId(boardNotNull) |
|||
.toTry() |
|||
.flatMap(this::getBoardIdPredicate) |
|||
.orElse(getBoardTitlePredicate(boardNotNull)) |
|||
.map(topicStarterPredicate::and)) |
|||
.getOrElse(Try.success(topicStarterPredicate)) |
|||
) |
|||
.flatMap(topicStarterPredicate -> Option |
|||
.of(topic) |
|||
.map(topicNotNull -> extractId(topicNotNull) |
|||
.toTry() |
|||
.flatMap(this::getTopicIdPredicate) |
|||
.orElse(getTopicSubjectPredicate(topicNotNull)) |
|||
.map(topicStarterPredicate::and)) |
|||
.getOrElse(Try.success(topicStarterPredicate)) |
|||
) |
|||
.map(topicStarterPredicate -> topicStarterRepository.findAll(topicStarterPredicate, pageable)); |
|||
} |
|||
|
|||
private Option<Long> extractId(final @Nonnull String input) { |
|||
|
|||
return Try |
|||
.of(() -> Long.parseLong(input)) |
|||
.recoverWith(throwable -> Try.success(null)) |
|||
.toOption(); |
|||
} |
|||
|
|||
private Try<BooleanExpression> getUsernamePredicate(final @Nonnull String username) { |
|||
|
|||
return Try |
|||
.of(() -> QTopicStarter.topicStarter) |
|||
.map(qTopicStarter -> qTopicStarter.starterUsername.like(username)); |
|||
} |
|||
|
|||
private Try<BooleanExpression> getUserIdPredicate(final @Nonnull Long userId) { |
|||
|
|||
return Try |
|||
.of(() -> QTopicStarter.topicStarter) |
|||
.map(qTopicStarter -> qTopicStarter.starterId.eq(userId)); |
|||
} |
|||
|
|||
private Try<BooleanExpression> getBoardTitlePredicate(final @Nonnull String boardTitle) { |
|||
|
|||
return Try |
|||
.of(() -> QTopicStarter.topicStarter) |
|||
.map(qTopicStarter -> qTopicStarter.boardTitle.like(boardTitle)); |
|||
} |
|||
|
|||
private Try<BooleanExpression> getBoardIdPredicate(final @Nonnull Long boardId) { |
|||
|
|||
return Try |
|||
.of(() -> QTopicStarter.topicStarter) |
|||
.map(qTopicStarter -> qTopicStarter.boardId.eq(boardId)); |
|||
} |
|||
|
|||
private Try<BooleanExpression> getTopicSubjectPredicate(final @Nonnull String topicSubject) { |
|||
|
|||
return Try |
|||
.of(() -> QTopicStarter.topicStarter) |
|||
.map(qTopicStarter -> qTopicStarter.topicSubject.like(topicSubject)); |
|||
} |
|||
|
|||
private Try<BooleanExpression> getTopicIdPredicate(final @Nonnull Long topicId) { |
|||
|
|||
return Try |
|||
.of(() -> QTopicStarter.topicStarter) |
|||
.map(qTopicStarter -> qTopicStarter.topicId.eq(topicId)); |
|||
} |
|||
} |
@ -0,0 +1,8 @@ |
|||
package gr.thmmy.mthmmy.topicstarter.service.topic.starter.parser; |
|||
|
|||
import io.vavr.control.Try; |
|||
|
|||
public interface TopicStarterParserService { |
|||
|
|||
Try<Void> parseTopicStarters(); |
|||
} |
@ -0,0 +1,241 @@ |
|||
package gr.thmmy.mthmmy.topicstarter.service.topic.starter.parser; |
|||
|
|||
import gr.thmmy.mthmmy.topicstarter.entity.topic.QTopicStarter; |
|||
import gr.thmmy.mthmmy.topicstarter.entity.topic.TopicStarter; |
|||
import gr.thmmy.mthmmy.topicstarter.repository.topic.TopicStarterRepository; |
|||
import gr.thmmy.mthmmy.topicstarter.service.topic.starter.parser.util.TopicParserUtils; |
|||
import io.vavr.Tuple; |
|||
import io.vavr.collection.List; |
|||
import io.vavr.control.Option; |
|||
import io.vavr.control.Try; |
|||
import lombok.Data; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import okhttp3.*; |
|||
import org.jsoup.Jsoup; |
|||
import org.jsoup.nodes.Document; |
|||
import org.jsoup.nodes.Element; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.core.env.Environment; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
import java.util.stream.IntStream; |
|||
|
|||
import static gr.thmmy.mthmmy.topicstarter.service.topic.starter.parser.util.BoardParserUtils.extractBoardIdFromUrl; |
|||
import static gr.thmmy.mthmmy.topicstarter.service.topic.starter.parser.util.BoardParserUtils.parseSubBoards; |
|||
import static java.util.Objects.requireNonNull; |
|||
|
|||
@Service |
|||
@Data |
|||
@Slf4j |
|||
public class TopicStarterParserServiceImpl implements TopicStarterParserService { |
|||
/* Constants */ |
|||
private static final String baseUrl = "https://www.thmmy.gr/smf/index.php?action=forum"; |
|||
private static final String RECYCLING_BIN_BOARD_ID = "244.0"; |
|||
private static final String USERNAME_ENV_VAR = "TOPIC_STARTERS_USERNAME"; |
|||
private static final String PASSWORD_ENV_VAR = "TOPIC_STARTERS_PASSWORD"; |
|||
private static final HttpUrl loginUrl = HttpUrl.parse("https://www.thmmy.gr/smf/index.php?action=login2"); |
|||
|
|||
private final OkHttpClient client; |
|||
private final TopicStarterRepository topicStarterRepository; |
|||
|
|||
@Autowired |
|||
private Environment environment; |
|||
|
|||
@Override |
|||
public Try<Void> parseTopicStarters() { |
|||
|
|||
return login() |
|||
.flatMap(ignored -> parseBoard(baseUrl)); |
|||
} |
|||
|
|||
private Try<Void> parseBoard(final String url) { |
|||
requireNonNull(url, "url is null"); |
|||
|
|||
return Try.of(Request.Builder::new) |
|||
// Builds and executes request
|
|||
.map(requestBuilder -> requestBuilder |
|||
.url(url) |
|||
.build()) |
|||
.mapTry(request -> client |
|||
.newCall(request) |
|||
.execute()) |
|||
.flatMap(this::getResponseString) |
|||
.map(Jsoup::parse) |
|||
.flatMap(document -> parseSubBoards(document) // Parses the sub boards
|
|||
.flatMap(subBoards -> Option |
|||
.of(subBoards) |
|||
.toTry() |
|||
.flatMap(subBoardsNotNull -> Try |
|||
.run(() -> subBoardsNotNull |
|||
.stream() |
|||
.filter(subBoard -> !subBoard |
|||
.attr("href") |
|||
.contains("board=" + RECYCLING_BIN_BOARD_ID)) |
|||
.forEach(subBoard -> parseBoard(subBoard.attr("href"))) |
|||
) |
|||
) |
|||
) |
|||
// Parses the topics
|
|||
.flatMap(ignored -> parseTopics(document, url)) |
|||
); |
|||
} |
|||
|
|||
private Try<Void> parseTopics(final Document document, |
|||
final String boardUrl) { |
|||
requireNonNull(document, "document is null"); |
|||
requireNonNull(boardUrl, "boardUrl is null"); |
|||
|
|||
return Try.of(() -> document // Finds the number of pages in this board
|
|||
.select("a.navPages") |
|||
.last()) |
|||
.map(pageNumber -> Option |
|||
.of(pageNumber) |
|||
.map(Element::text) |
|||
.map(Integer::parseInt) |
|||
.getOrElse(1)) |
|||
.flatMap(numberOfPages -> Try // Parses the board title
|
|||
.of(() -> document |
|||
.select("div.nav>b>a") |
|||
.last() |
|||
.text()) |
|||
.flatMap(boardTitle -> extractBoardIdFromUrl(boardUrl) // Parses topics of the current page
|
|||
.flatMap(boardId -> saveTopics(document, boardTitle, boardUrl, boardId) |
|||
.flatMap(ignored -> IntStream // Parses the topics from the rest of the pages
|
|||
.range(1, numberOfPages) |
|||
.boxed() |
|||
.map(page -> Try // Builds the URL of the board for each page
|
|||
.of(() -> String.join(".", |
|||
boardUrl.substring(0, boardUrl.lastIndexOf(".")), |
|||
String.valueOf(page * 20)) |
|||
).flatMap(pageUrl -> Try |
|||
.of(Request.Builder::new) |
|||
.map(requestBuilder -> requestBuilder |
|||
.url(pageUrl) |
|||
.build() |
|||
) |
|||
) |
|||
.mapTry(request -> client |
|||
.newCall(request) |
|||
.execute()) |
|||
.flatMap(this::getResponseString) |
|||
.map(Jsoup::parse) |
|||
.flatMap(pageDocument -> saveTopics( |
|||
pageDocument, |
|||
boardTitle, |
|||
boardUrl, |
|||
boardId))) |
|||
.collect(List.collector()) |
|||
.transform(Try::sequence) |
|||
) |
|||
) |
|||
).map(ignored -> null)); |
|||
} |
|||
|
|||
private Try<Void> saveTopics(final Document document, |
|||
final String boardTitle, |
|||
final String boardUrl, |
|||
final Long boardId) { |
|||
requireNonNull(document, "document is null"); |
|||
requireNonNull(boardTitle, "boardTitle is null"); |
|||
requireNonNull(boardUrl, "boardUrl is null"); |
|||
requireNonNull(boardId, "boardId is null"); |
|||
|
|||
return Try // Finds this page's topics
|
|||
.of(() -> document |
|||
.select("table.bordercolor tbody>tr:not([class])")) |
|||
.flatMap(topics -> Try |
|||
.run(() -> topics |
|||
.forEach(topicRow -> TopicParserUtils |
|||
.parseTopic(topicRow, boardTitle, boardUrl, boardId) |
|||
.map(topicStarter -> savedTopicStarter(topicStarter) |
|||
.flatMap(savedTopicStarter -> updateSavedTopicStarter(savedTopicStarter, topicStarter)) |
|||
.getOrElse(topicStarter) |
|||
) |
|||
.map(topicStarterRepository::save) |
|||
) |
|||
) |
|||
); |
|||
} |
|||
|
|||
private Option<TopicStarter> savedTopicStarter(final TopicStarter topicStarter) { |
|||
requireNonNull(topicStarter, "topicStarter is null"); |
|||
|
|||
return Option |
|||
.of(QTopicStarter.topicStarter) |
|||
.map(qTopicStarter -> qTopicStarter.topicId.eq(topicStarter.getTopicId())) |
|||
.map(topicStarterRepository::findOne) |
|||
.flatMap(Option::ofOptional); |
|||
} |
|||
|
|||
private Option<TopicStarter> updateSavedTopicStarter(final TopicStarter savedTopicStarter, final TopicStarter newTopicStarter) { |
|||
requireNonNull(savedTopicStarter, "savedTopicStarter is null"); |
|||
requireNonNull(newTopicStarter, "newTopicStarter is null"); |
|||
|
|||
savedTopicStarter.setTopicId(newTopicStarter.getTopicId()); |
|||
savedTopicStarter.setTopicUrl(newTopicStarter.getTopicUrl()); |
|||
savedTopicStarter.setStarterUsername(newTopicStarter.getStarterUsername()); |
|||
savedTopicStarter.setStarterUrl(newTopicStarter.getStarterUrl()); |
|||
savedTopicStarter.setStarterId(newTopicStarter.getStarterId()); |
|||
savedTopicStarter.setBoardTitle(newTopicStarter.getBoardTitle()); |
|||
savedTopicStarter.setBoardUrl(newTopicStarter.getBoardUrl()); |
|||
savedTopicStarter.setBoardId(newTopicStarter.getBoardId()); |
|||
savedTopicStarter.setTopicSubject(newTopicStarter.getTopicSubject()); |
|||
savedTopicStarter.setNumberOfReplies(newTopicStarter.getNumberOfReplies()); |
|||
savedTopicStarter.setNumberOfViews(newTopicStarter.getNumberOfViews()); |
|||
|
|||
return Option.of(savedTopicStarter); |
|||
} |
|||
|
|||
private Try<Void> login() { |
|||
|
|||
return Option |
|||
.of(environment.getProperty(USERNAME_ENV_VAR)) |
|||
.map(username -> Option |
|||
.of(environment.getProperty(PASSWORD_ENV_VAR)) |
|||
.map(password -> Tuple.of(username, password)) |
|||
.getOrElseThrow(() -> new RuntimeException("Password is null")) |
|||
).map(loginSecrets -> Option |
|||
.of(loginUrl) |
|||
.map(loginUrlNotNull -> Try |
|||
.of(FormBody.Builder::new) |
|||
.map(builder -> builder |
|||
.add("user", loginSecrets._1) |
|||
.add("passwrd", loginSecrets._2) |
|||
.add("cookielength", "-1") // -1 is forever
|
|||
.build() |
|||
).flatMap(formBody -> Try |
|||
.of(Request.Builder::new) |
|||
.map(builder -> builder |
|||
.url(loginUrlNotNull) |
|||
.post(formBody) |
|||
.build() |
|||
) |
|||
).mapTry(request -> client |
|||
.newCall(request) |
|||
.execute() |
|||
).flatMap(response -> Try |
|||
.run(() -> response |
|||
.body() |
|||
.close() |
|||
) |
|||
) |
|||
).getOrElseThrow(() -> new RuntimeException("Login URL is null.")) |
|||
).getOrElseThrow(() -> new RuntimeException("Username is null")); |
|||
} |
|||
|
|||
private Try<String> getResponseString(final Response response) { |
|||
requireNonNull(response, "response is null"); |
|||
|
|||
// Checks response for null and closes response body
|
|||
return Try |
|||
.of(response::body) |
|||
.flatMap(responseBody -> Option |
|||
.of(responseBody) |
|||
.toTry() |
|||
.mapTry(ResponseBody::string) |
|||
.flatMap(responseBodyString -> Try |
|||
.run(responseBody::close) |
|||
.map(ignored -> responseBodyString)) |
|||
); |
|||
} |
|||
} |
@ -0,0 +1,33 @@ |
|||
package gr.thmmy.mthmmy.topicstarter.service.topic.starter.parser.util; |
|||
|
|||
import io.vavr.control.Try; |
|||
import org.jsoup.nodes.Document; |
|||
import org.jsoup.select.Elements; |
|||
|
|||
import java.util.regex.Pattern; |
|||
|
|||
import static java.util.Objects.requireNonNull; |
|||
|
|||
public abstract class BoardParserUtils { |
|||
|
|||
public static Try<Long> extractBoardIdFromUrl(final String url) { |
|||
requireNonNull(url, "url is null"); |
|||
|
|||
return Try.success(".+?board=([0-9]+)") |
|||
.map(regex -> Pattern.compile(regex, Pattern.MULTILINE)) |
|||
.map(pattern -> pattern.matcher(url)) |
|||
.map(matcher -> Try |
|||
.of(matcher::find) |
|||
.filter(aBoolean -> aBoolean) |
|||
.map(ignored -> Long.parseLong(matcher.group(1))) |
|||
.getOrElse(-1L) |
|||
); |
|||
} |
|||
|
|||
public static Try<Elements> parseSubBoards(final Document document) { |
|||
requireNonNull(document, "document is null"); |
|||
|
|||
return Try.of(() -> document |
|||
.select("div.tborder tbody tr.windowbg2 td>b>a[name^=b]")); |
|||
} |
|||
} |
@ -0,0 +1,177 @@ |
|||
package gr.thmmy.mthmmy.topicstarter.service.topic.starter.parser.util; |
|||
|
|||
import gr.thmmy.mthmmy.topicstarter.entity.topic.TopicStarter; |
|||
import io.vavr.control.Option; |
|||
import io.vavr.control.Try; |
|||
import org.jsoup.nodes.Element; |
|||
import org.jsoup.select.Elements; |
|||
|
|||
import javax.annotation.Nullable; |
|||
import java.util.regex.Pattern; |
|||
|
|||
import static java.util.Objects.requireNonNull; |
|||
|
|||
public abstract class TopicParserUtils { |
|||
|
|||
public static Try<TopicStarter> parseTopic(final Element topicRow, |
|||
final String boardTitle, |
|||
final String boardUrl, |
|||
final Long boardId) { |
|||
requireNonNull(topicRow, "topicRow is null"); |
|||
requireNonNull(boardTitle, "boardTitle is null"); |
|||
requireNonNull(boardUrl, "boardUrl is null"); |
|||
requireNonNull(boardId, "boardId is null"); |
|||
|
|||
return Try.of(() -> topicRow |
|||
.select("td")) |
|||
.flatMap(topicColumns -> Try |
|||
.of(() -> topicColumns |
|||
.get(3) |
|||
.select("a") |
|||
.first()) |
|||
.flatMap(starterUrlElement -> Try |
|||
.of(TopicStarter::new) |
|||
.flatMap(topic -> parseTopicSubject(topicColumns) |
|||
.map(topicSubject -> { |
|||
topic.setTopicSubject(topicSubject); |
|||
|
|||
return topic; |
|||
}) |
|||
).flatMap(topic -> parseTopicUrl(topicColumns) |
|||
.flatMap(topicUrl -> extractTopicIdFromUrl(topicUrl) |
|||
.map(topicId -> { |
|||
topic.setTopicUrl(topicUrl); |
|||
topic.setTopicId(topicId); |
|||
|
|||
return topic; |
|||
}) |
|||
) |
|||
).flatMap(topic -> parseTopicStarterUsername(topicColumns) |
|||
.map(starterUsername -> { |
|||
topic.setStarterUsername(starterUsername); |
|||
|
|||
return topic; |
|||
}) |
|||
).flatMap(topic -> parseTopicStarterUrl(starterUrlElement) |
|||
.flatMap(topicStarterUrl -> extractTopicStarterIdFromUrl(topicStarterUrl) |
|||
.map(topicStarterId -> { |
|||
topic.setStarterUrl(topicStarterUrl); |
|||
topic.setStarterId(topicStarterId); |
|||
|
|||
return topic; |
|||
}) |
|||
) |
|||
).flatMap(topic -> parseTopicNumberOfReplies(topicColumns) |
|||
.map(numReplies -> { |
|||
topic.setNumberOfReplies(numReplies); |
|||
|
|||
return topic; |
|||
}) |
|||
).flatMap(topic -> parseTopicNumberOfViews(topicColumns) |
|||
.map(numViews -> { |
|||
topic.setNumberOfViews(numViews); |
|||
|
|||
return topic; |
|||
}) |
|||
).map(topic -> { |
|||
topic.setBoardTitle(boardTitle); |
|||
topic.setBoardId(boardId); |
|||
topic.setBoardUrl(boardUrl); |
|||
|
|||
return topic; |
|||
} |
|||
) |
|||
) |
|||
); |
|||
} |
|||
|
|||
private static Try<String> parseTopicSubject(final Elements topicColumns) { |
|||
requireNonNull(topicColumns, "topicColumns is null"); |
|||
|
|||
return Try |
|||
.of(() -> topicColumns |
|||
.get(2) |
|||
.select("span>a") |
|||
.first() |
|||
.text()); |
|||
} |
|||
|
|||
private static Try<String> parseTopicUrl(final Elements topicColumns) { |
|||
requireNonNull(topicColumns, "topicColumns is null"); |
|||
|
|||
return Try |
|||
.of(() -> topicColumns |
|||
.get(2) |
|||
.select("span>a") |
|||
.first() |
|||
.attr("href")); |
|||
} |
|||
|
|||
private static Try<String> parseTopicStarterUsername(final Elements topicColumns) { |
|||
requireNonNull(topicColumns, "topicColumns is null"); |
|||
|
|||
return Try |
|||
.of(() -> topicColumns |
|||
.get(3) |
|||
.text()); |
|||
} |
|||
|
|||
private static Try<String> parseTopicStarterUrl(final Element starterUrlEl) { |
|||
requireNonNull(starterUrlEl, "starterUrlEl is null"); |
|||
|
|||
return Try.of(() -> Option |
|||
.of(starterUrlEl) |
|||
.map(starterUrlElNotNull -> starterUrlElNotNull |
|||
.attr("href")) |
|||
.getOrElse(() -> null)); |
|||
} |
|||
|
|||
private static Try<Long> parseTopicNumberOfReplies(final Elements topicColumns) { |
|||
requireNonNull(topicColumns, "topicColumns is null"); |
|||
|
|||
return Try |
|||
.of(() -> topicColumns |
|||
.get(4) |
|||
.text()) |
|||
.map(Long::parseLong); |
|||
} |
|||
|
|||
private static Try<Long> parseTopicNumberOfViews(final Elements topicColumns) { |
|||
requireNonNull(topicColumns, "topicColumns is null"); |
|||
|
|||
return Try |
|||
.of(() -> topicColumns |
|||
.get(5) |
|||
.text()) |
|||
.map(Long::parseLong); |
|||
} |
|||
|
|||
private static Try<Long> extractTopicIdFromUrl(final String topicUrl) { |
|||
requireNonNull(topicUrl, "topicUrl is null"); |
|||
|
|||
return Try.success(".+?topic=([0-9]+)") |
|||
.map(regex -> Pattern.compile(regex, Pattern.MULTILINE)) |
|||
.map(pattern -> pattern.matcher(topicUrl)) |
|||
.map(matcher -> Try.of(matcher::find) |
|||
.filter(aBoolean -> aBoolean) |
|||
.map(ignored -> Long.parseLong(matcher.group(1))) |
|||
.getOrElse(-1L) |
|||
); |
|||
|
|||
} |
|||
|
|||
private static Try<Long> extractTopicStarterIdFromUrl(final @Nullable String topicStarterUrl) { |
|||
|
|||
return Option |
|||
.of(topicStarterUrl) |
|||
.map(topicStarterUrlNotNull -> Try |
|||
.of(() -> ".+?profile;u=([0-9]+)") |
|||
.map(regex -> Pattern.compile(regex, Pattern.MULTILINE)) |
|||
.map(pattern -> pattern.matcher(topicStarterUrlNotNull)) |
|||
.map(matcher -> Try.of(matcher::find) |
|||
.filter(aBoolean -> aBoolean) |
|||
.map(ignored -> Long.parseLong(matcher.group(1))) |
|||
.getOrElse(-1L)) |
|||
).getOrElse(Try.success(-1L)); |
|||
} |
|||
} |
Loading…
Reference in new issue