Skip to content

Commit 4d2feea

Browse files
committed
Form4 Final implementation
1 parent 7d52a99 commit 4d2feea

File tree

18 files changed

+2481
-103
lines changed

18 files changed

+2481
-103
lines changed

pom.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@
1717
<spring-cloud.version>2025.1.0</spring-cloud.version>
1818
</properties>
1919
<dependencies>
20+
<dependency>
21+
<groupId>jakarta.xml.bind</groupId>
22+
<artifactId>jakarta.xml.bind-api</artifactId>
23+
</dependency>
24+
<dependency>
25+
<groupId>org.glassfish.jaxb</groupId>
26+
<artifactId>jaxb-runtime</artifactId>
27+
</dependency>
2028
<dependency>
2129
<groupId>org.springframework.cloud</groupId>
2230
<artifactId>spring-cloud-starter-bootstrap</artifactId>
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
package org.jds.edgar4j.controller;
2+
3+
import java.text.ParseException;
4+
import java.text.SimpleDateFormat;
5+
import java.util.Date;
6+
import java.util.List;
7+
8+
import org.jds.edgar4j.model.Form4;
9+
import org.jds.edgar4j.service.Form4Service;
10+
import org.jds.edgar4j.service.Form4Service.InsiderStats;
11+
import org.springframework.data.domain.Page;
12+
import org.springframework.data.domain.PageRequest;
13+
import org.springframework.data.domain.Sort;
14+
import org.springframework.http.ResponseEntity;
15+
import org.springframework.web.bind.annotation.DeleteMapping;
16+
import org.springframework.web.bind.annotation.GetMapping;
17+
import org.springframework.web.bind.annotation.PathVariable;
18+
import org.springframework.web.bind.annotation.PostMapping;
19+
import org.springframework.web.bind.annotation.RequestBody;
20+
import org.springframework.web.bind.annotation.RequestMapping;
21+
import org.springframework.web.bind.annotation.RequestParam;
22+
import org.springframework.web.bind.annotation.RestController;
23+
24+
import lombok.RequiredArgsConstructor;
25+
import lombok.extern.slf4j.Slf4j;
26+
27+
/**
28+
* REST API for Form 4 insider trading filings.
29+
*/
30+
@Slf4j
31+
@RestController
32+
@RequestMapping("/api/form4")
33+
@RequiredArgsConstructor
34+
public class Form4Controller {
35+
36+
private final Form4Service form4Service;
37+
38+
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
39+
40+
/**
41+
* Get Form 4 by ID.
42+
*/
43+
@GetMapping("/{id}")
44+
public ResponseEntity<Form4> getById(@PathVariable String id) {
45+
return form4Service.findById(id)
46+
.map(ResponseEntity::ok)
47+
.orElse(ResponseEntity.notFound().build());
48+
}
49+
50+
/**
51+
* Get Form 4 by accession number.
52+
*/
53+
@GetMapping("/accession/{accessionNumber}")
54+
public ResponseEntity<Form4> getByAccessionNumber(@PathVariable String accessionNumber) {
55+
return form4Service.findByAccessionNumber(accessionNumber)
56+
.map(ResponseEntity::ok)
57+
.orElse(ResponseEntity.notFound().build());
58+
}
59+
60+
/**
61+
* Get Form 4 filings by trading symbol.
62+
*/
63+
@GetMapping("/symbol/{symbol}")
64+
public ResponseEntity<Page<Form4>> getBySymbol(
65+
@PathVariable String symbol,
66+
@RequestParam(defaultValue = "0") int page,
67+
@RequestParam(defaultValue = "20") int size) {
68+
69+
PageRequest pageRequest = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "transactionDate"));
70+
Page<Form4> results = form4Service.findByTradingSymbol(symbol.toUpperCase(), pageRequest);
71+
return ResponseEntity.ok(results);
72+
}
73+
74+
/**
75+
* Get Form 4 filings by CIK.
76+
*/
77+
@GetMapping("/cik/{cik}")
78+
public ResponseEntity<Page<Form4>> getByCik(
79+
@PathVariable String cik,
80+
@RequestParam(defaultValue = "0") int page,
81+
@RequestParam(defaultValue = "20") int size) {
82+
83+
PageRequest pageRequest = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "transactionDate"));
84+
Page<Form4> results = form4Service.findByCik(cik, pageRequest);
85+
return ResponseEntity.ok(results);
86+
}
87+
88+
/**
89+
* Search Form 4 filings by owner name.
90+
*/
91+
@GetMapping("/owner")
92+
public ResponseEntity<List<Form4>> searchByOwner(@RequestParam String name) {
93+
List<Form4> results = form4Service.findByOwnerName(name);
94+
return ResponseEntity.ok(results);
95+
}
96+
97+
/**
98+
* Get Form 4 filings within date range.
99+
*/
100+
@GetMapping("/date-range")
101+
public ResponseEntity<Page<Form4>> getByDateRange(
102+
@RequestParam String startDate,
103+
@RequestParam String endDate,
104+
@RequestParam(defaultValue = "0") int page,
105+
@RequestParam(defaultValue = "20") int size) {
106+
107+
try {
108+
Date start = parseDate(startDate);
109+
Date end = parseDate(endDate);
110+
PageRequest pageRequest = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "transactionDate"));
111+
Page<Form4> results = form4Service.findByDateRange(start, end, pageRequest);
112+
return ResponseEntity.ok(results);
113+
} catch (ParseException e) {
114+
log.warn("Invalid date format: {} or {}", startDate, endDate);
115+
return ResponseEntity.badRequest().build();
116+
}
117+
}
118+
119+
/**
120+
* Get Form 4 filings by symbol and date range.
121+
*/
122+
@GetMapping("/symbol/{symbol}/date-range")
123+
public ResponseEntity<Page<Form4>> getBySymbolAndDateRange(
124+
@PathVariable String symbol,
125+
@RequestParam String startDate,
126+
@RequestParam String endDate,
127+
@RequestParam(defaultValue = "0") int page,
128+
@RequestParam(defaultValue = "20") int size) {
129+
130+
try {
131+
Date start = parseDate(startDate);
132+
Date end = parseDate(endDate);
133+
PageRequest pageRequest = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "transactionDate"));
134+
Page<Form4> results = form4Service.findBySymbolAndDateRange(symbol.toUpperCase(), start, end, pageRequest);
135+
return ResponseEntity.ok(results);
136+
} catch (ParseException e) {
137+
log.warn("Invalid date format: {} or {}", startDate, endDate);
138+
return ResponseEntity.badRequest().build();
139+
}
140+
}
141+
142+
/**
143+
* Get recent Form 4 filings.
144+
*/
145+
@GetMapping("/recent")
146+
public ResponseEntity<List<Form4>> getRecentFilings(
147+
@RequestParam(defaultValue = "10") int limit) {
148+
List<Form4> results = form4Service.findRecentFilings(Math.min(limit, 100));
149+
return ResponseEntity.ok(results);
150+
}
151+
152+
/**
153+
* Get insider statistics for a symbol.
154+
*/
155+
@GetMapping("/symbol/{symbol}/stats")
156+
public ResponseEntity<InsiderStats> getInsiderStats(
157+
@PathVariable String symbol,
158+
@RequestParam String startDate,
159+
@RequestParam String endDate) {
160+
161+
try {
162+
Date start = parseDate(startDate);
163+
Date end = parseDate(endDate);
164+
InsiderStats stats = form4Service.getInsiderStats(symbol.toUpperCase(), start, end);
165+
return ResponseEntity.ok(stats);
166+
} catch (ParseException e) {
167+
log.warn("Invalid date format: {} or {}", startDate, endDate);
168+
return ResponseEntity.badRequest().build();
169+
}
170+
}
171+
172+
/**
173+
* Download and parse a Form 4 filing.
174+
*/
175+
@PostMapping("/download")
176+
public ResponseEntity<Form4> downloadAndParse(
177+
@RequestParam String cik,
178+
@RequestParam String accessionNumber,
179+
@RequestParam String primaryDocument) {
180+
181+
// Check if already exists
182+
if (form4Service.existsByAccessionNumber(accessionNumber)) {
183+
return form4Service.findByAccessionNumber(accessionNumber)
184+
.map(ResponseEntity::ok)
185+
.orElse(ResponseEntity.notFound().build());
186+
}
187+
188+
try {
189+
Form4 form4 = form4Service.downloadAndParseForm4(cik, accessionNumber, primaryDocument)
190+
.join();
191+
192+
if (form4 == null) {
193+
return ResponseEntity.badRequest().build();
194+
}
195+
196+
Form4 saved = form4Service.save(form4);
197+
return ResponseEntity.ok(saved);
198+
199+
} catch (Exception e) {
200+
log.error("Failed to download/parse Form 4: {}", accessionNumber, e);
201+
return ResponseEntity.internalServerError().build();
202+
}
203+
}
204+
205+
/**
206+
* Save a Form 4 filing (manual entry or re-processing).
207+
*/
208+
@PostMapping
209+
public ResponseEntity<Form4> save(@RequestBody Form4 form4) {
210+
Form4 saved = form4Service.save(form4);
211+
return ResponseEntity.ok(saved);
212+
}
213+
214+
/**
215+
* Delete a Form 4 filing.
216+
*/
217+
@DeleteMapping("/{id}")
218+
public ResponseEntity<Void> delete(@PathVariable String id) {
219+
if (form4Service.findById(id).isEmpty()) {
220+
return ResponseEntity.notFound().build();
221+
}
222+
form4Service.deleteById(id);
223+
return ResponseEntity.noContent().build();
224+
}
225+
226+
private Date parseDate(String dateStr) throws ParseException {
227+
synchronized (DATE_FORMAT) {
228+
return DATE_FORMAT.parse(dateStr);
229+
}
230+
}
231+
}

src/main/java/org/jds/edgar4j/integration/Form4Parser.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
import java.util.Date;
88
import java.util.List;
99

10-
import javax.xml.bind.JAXBContext;
11-
import javax.xml.bind.JAXBException;
12-
import javax.xml.bind.Unmarshaller;
10+
import jakarta.xml.bind.JAXBContext;
11+
import jakarta.xml.bind.JAXBException;
12+
import jakarta.xml.bind.Unmarshaller;
1313
import javax.xml.parsers.DocumentBuilder;
1414
import javax.xml.parsers.DocumentBuilderFactory;
1515

src/main/java/org/jds/edgar4j/integration/model/form4/DerivativeTable.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
import java.util.List;
44

5-
import javax.xml.bind.annotation.XmlAccessType;
6-
import javax.xml.bind.annotation.XmlAccessorType;
7-
import javax.xml.bind.annotation.XmlElement;
5+
import jakarta.xml.bind.annotation.XmlAccessType;
6+
import jakarta.xml.bind.annotation.XmlAccessorType;
7+
import jakarta.xml.bind.annotation.XmlElement;
88

99
import lombok.Data;
1010

src/main/java/org/jds/edgar4j/integration/model/form4/Issuer.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package org.jds.edgar4j.integration.model.form4;
22

3-
import javax.xml.bind.annotation.XmlAccessType;
4-
import javax.xml.bind.annotation.XmlAccessorType;
5-
import javax.xml.bind.annotation.XmlElement;
3+
import jakarta.xml.bind.annotation.XmlAccessType;
4+
import jakarta.xml.bind.annotation.XmlAccessorType;
5+
import jakarta.xml.bind.annotation.XmlElement;
66

77
import lombok.Data;
88

src/main/java/org/jds/edgar4j/integration/model/form4/NonDerivativeTable.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
import java.util.List;
44

5-
import javax.xml.bind.annotation.XmlAccessType;
6-
import javax.xml.bind.annotation.XmlAccessorType;
7-
import javax.xml.bind.annotation.XmlElement;
5+
import jakarta.xml.bind.annotation.XmlAccessType;
6+
import jakarta.xml.bind.annotation.XmlAccessorType;
7+
import jakarta.xml.bind.annotation.XmlElement;
88

99
import lombok.Data;
1010

src/main/java/org/jds/edgar4j/integration/model/form4/OwnershipDocument.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
import java.util.List;
44

5-
import javax.xml.bind.annotation.XmlAccessType;
6-
import javax.xml.bind.annotation.XmlAccessorType;
7-
import javax.xml.bind.annotation.XmlElement;
8-
import javax.xml.bind.annotation.XmlElementWrapper;
9-
import javax.xml.bind.annotation.XmlRootElement;
5+
import jakarta.xml.bind.annotation.XmlAccessType;
6+
import jakarta.xml.bind.annotation.XmlAccessorType;
7+
import jakarta.xml.bind.annotation.XmlElement;
8+
import jakarta.xml.bind.annotation.XmlElementWrapper;
9+
import jakarta.xml.bind.annotation.XmlRootElement;
1010

1111
import lombok.Data;
1212

src/main/java/org/jds/edgar4j/integration/model/form4/ReportingOwner.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package org.jds.edgar4j.integration.model.form4;
22

3-
import javax.xml.bind.annotation.XmlAccessType;
4-
import javax.xml.bind.annotation.XmlAccessorType;
5-
import javax.xml.bind.annotation.XmlElement;
3+
import jakarta.xml.bind.annotation.XmlAccessType;
4+
import jakarta.xml.bind.annotation.XmlAccessorType;
5+
import jakarta.xml.bind.annotation.XmlElement;
66

77
import lombok.Data;
88

src/main/java/org/jds/edgar4j/integration/model/form4/ValueWithFootnote.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package org.jds.edgar4j.integration.model.form4;
22

3-
import javax.xml.bind.annotation.XmlAccessType;
4-
import javax.xml.bind.annotation.XmlAccessorType;
5-
import javax.xml.bind.annotation.XmlAttribute;
6-
import javax.xml.bind.annotation.XmlElement;
7-
import javax.xml.bind.annotation.XmlValue;
3+
import jakarta.xml.bind.annotation.XmlAccessType;
4+
import jakarta.xml.bind.annotation.XmlAccessorType;
5+
import jakarta.xml.bind.annotation.XmlAttribute;
6+
import jakarta.xml.bind.annotation.XmlElement;
7+
import jakarta.xml.bind.annotation.XmlValue;
88

99
import lombok.Data;
1010

src/main/java/org/jds/edgar4j/model/Form4.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package org.jds.edgar4j.model;
22

33
import java.util.Date;
4+
import java.util.List;
5+
6+
import org.springframework.data.mongodb.core.index.CompoundIndex;
7+
import org.springframework.data.mongodb.core.index.CompoundIndexes;
8+
import org.springframework.data.mongodb.core.index.Indexed;
49

510
import org.springframework.data.annotation.Id;
611
import org.springframework.data.mongodb.core.mapping.Document;

0 commit comments

Comments
 (0)