GDPR Compliance Toolkit in Java

Overview

A comprehensive toolkit for implementing GDPR compliance features in Java applications, including data subject rights, consent management, data processing records, and privacy by design.

1. Core GDPR Data Models

import javax.persistence.*;
import javax.validation.constraints.*;
import java.time.LocalDateTime;
import java.util.*;
@Entity
@Table(name = "data_subjects")
public class DataSubject {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
@Column(unique = true)
private String subjectId; // External identifier
private String email;
private String phone;
@Embedded
private PersonalData personalData;
@OneToMany(mappedBy = "dataSubject", cascade = CascadeType.ALL)
private List<ConsentRecord> consents = new ArrayList<>();
@OneToMany(mappedBy = "dataSubject", cascade = CascadeType.ALL)
private List<DataAccessRequest> accessRequests = new ArrayList<>();
@OneToMany(mappedBy = "dataSubject", cascade = CascadeType.ALL)
private List<ProcessingActivity> processingActivities = new ArrayList<>();
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
// Getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getSubjectId() { return subjectId; }
public void setSubjectId(String subjectId) { this.subjectId = subjectId; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public PersonalData getPersonalData() { return personalData; }
public void setPersonalData(PersonalData personalData) { this.personalData = personalData; }
public List<ConsentRecord> getConsents() { return consents; }
public void setConsents(List<ConsentRecord> consents) { this.consents = consents; }
public List<DataAccessRequest> getAccessRequests() { return accessRequests; }
public void setAccessRequests(List<DataAccessRequest> accessRequests) { this.accessRequests = accessRequests; }
public List<ProcessingActivity> getProcessingActivities() { return processingActivities; }
public void setProcessingActivities(List<ProcessingActivity> processingActivities) { this.processingActivities = processingActivities; }
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
}
@Embeddable
public class PersonalData {
private String firstName;
private String lastName;
private LocalDate dateOfBirth;
private String address;
private String city;
private String country;
private String postalCode;
// Getters and setters
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
public LocalDate getDateOfBirth() { return dateOfBirth; }
public void setDateOfBirth(LocalDate dateOfBirth) { this.dateOfBirth = dateOfBirth; }
public String getAddress() { return address; }
public void setAddress(String address) { this.address = address; }
public String getCity() { return city; }
public void setCity(String city) { this.city = city; }
public String getCountry() { return country; }
public void setCountry(String country) { this.country = country; }
public String getPostalCode() { return postalCode; }
public void setPostalCode(String postalCode) { this.postalCode = postalCode; }
}
@Entity
@Table(name = "consent_records")
public class ConsentRecord {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "data_subject_id")
private DataSubject dataSubject;
@NotBlank
private String purpose;
@NotBlank
private String legalBasis; // CONSENT, CONTRACT, LEGAL_OBLIGATION, etc.
private boolean granted;
@Lob
private String consentText;
private String version;
private LocalDateTime grantedAt;
private LocalDateTime revokedAt;
private LocalDateTime expiresAt;
private String ipAddress;
private String userAgent;
// Getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public DataSubject getDataSubject() { return dataSubject; }
public void setDataSubject(DataSubject dataSubject) { this.dataSubject = dataSubject; }
public String getPurpose() { return purpose; }
public void setPurpose(String purpose) { this.purpose = purpose; }
public String getLegalBasis() { return legalBasis; }
public void setLegalBasis(String legalBasis) { this.legalBasis = legalBasis; }
public boolean isGranted() { return granted; }
public void setGranted(boolean granted) { this.granted = granted; }
public String getConsentText() { return consentText; }
public void setConsentText(String consentText) { this.consentText = consentText; }
public String getVersion() { return version; }
public void setVersion(String version) { this.version = version; }
public LocalDateTime getGrantedAt() { return grantedAt; }
public void setGrantedAt(LocalDateTime grantedAt) { this.grantedAt = grantedAt; }
public LocalDateTime getRevokedAt() { return revokedAt; }
public void setRevokedAt(LocalDateTime revokedAt) { this.revokedAt = revokedAt; }
public LocalDateTime getExpiresAt() { return expiresAt; }
public void setExpiresAt(LocalDateTime expiresAt) { this.expiresAt = expiresAt; }
public String getIpAddress() { return ipAddress; }
public void setIpAddress(String ipAddress) { this.ipAddress = ipAddress; }
public String getUserAgent() { return userAgent; }
public void setUserAgent(String userAgent) { this.userAgent = userAgent; }
}
@Entity
@Table(name = "data_access_requests")
public class DataAccessRequest {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "data_subject_id")
private DataSubject dataSubject;
@Enumerated(EnumType.STRING)
private RequestType type;
@Enumerated(EnumType.STRING)
private RequestStatus status = RequestStatus.PENDING;
private LocalDateTime requestedAt;
private LocalDateTime completedAt;
private LocalDateTime dueDate;
@Lob
private String requestDetails;
@Lob
private String responseData;
private String assignedTo;
// Getters and setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public DataSubject getDataSubject() { return dataSubject; }
public void setDataSubject(DataSubject dataSubject) { this.dataSubject = dataSubject; }
public RequestType getType() { return type; }
public void setType(RequestType type) { this.type = type; }
public RequestStatus getStatus() { return status; }
public void setStatus(RequestStatus status) { this.status = status; }
public LocalDateTime getRequestedAt() { return requestedAt; }
public void setRequestedAt(LocalDateTime requestedAt) { this.requestedAt = requestedAt; }
public LocalDateTime getCompletedAt() { return completedAt; }
public void setCompletedAt(LocalDateTime completedAt) { this.completedAt = completedAt; }
public LocalDateTime getDueDate() { return dueDate; }
public void setDueDate(LocalDateTime dueDate) { this.dueDate = dueDate; }
public String getRequestDetails() { return requestDetails; }
public void setRequestDetails(String requestDetails) { this.requestDetails = requestDetails; }
public String getResponseData() { return responseData; }
public void setResponseData(String responseData) { this.responseData = responseData; }
public String getAssignedTo() { return assignedTo; }
public void setAssignedTo(String assignedTo) { this.assignedTo = assignedTo; }
}
enum RequestType {
ACCESS,
RECTIFICATION,
ERASURE,
RESTRICTION,
PORTABILITY,
OBJECTION
}
enum RequestStatus {
PENDING,
IN_PROGRESS,
COMPLETED,
REJECTED,
CANCELLED
}
@Entity
@Table(name = "processing_activities")
public class ProcessingActivity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
private String name;
private String description;
@NotBlank
private String purpose;
@ElementCollection
@CollectionTable(name = "processing_categories", joinColumns = @JoinColumn(name = "activity_id"))
@Enumerated(EnumType.STRING)
private Set<DataCategory> dataCategories = new HashSet<>();
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "data_subject_id")
private DataSubject dataSubject;
@Enumerated(EnumType.STRING)
private LegalBasis legalBasis;
private LocalDateTime processingStart;
private LocalDateTime processingEnd;
@ElementCollection
@CollectionTable(name = "data_recipients", joinColumns = @JoinColumn(name = "activity_id"))
private Set<String> recipients = new HashSet<>();
private boolean thirdCountryTransfer;
private String safeguards;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
}
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
// Getters and setters
// ... (similar to previous entities)
}
enum DataCategory {
IDENTIFYING,
CONTACT,
FINANCIAL,
HEALTH,
BIOMETRIC,
LOCATION,
BEHAVIORAL,
DEMOGRAPHIC
}
enum LegalBasis {
CONSENT,
CONTRACT,
LEGAL_OBLIGATION,
VITAL_INTEREST,
PUBLIC_TASK,
LEGITIMATE_INTEREST
}

2. Consent Management Service

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.*;
@Service
public class ConsentManagementService {
private final DataSubjectRepository dataSubjectRepository;
private final ConsentRecordRepository consentRecordRepository;
public ConsentManagementService(DataSubjectRepository dataSubjectRepository,
ConsentRecordRepository consentRecordRepository) {
this.dataSubjectRepository = dataSubjectRepository;
this.consentRecordRepository = consentRecordRepository;
}
@Transactional
public ConsentRecord grantConsent(String subjectId, ConsentRequest request, 
String ipAddress, String userAgent) {
DataSubject dataSubject = dataSubjectRepository.findBySubjectId(subjectId)
.orElseGet(() -> createDataSubject(subjectId, request.getEmail()));
// Revoke any existing consent for the same purpose
consentRecordRepository.findByDataSubjectAndPurposeAndGranted(dataSubject, 
request.getPurpose(), true)
.ifPresent(existing -> revokeConsent(existing.getId()));
ConsentRecord consent = new ConsentRecord();
consent.setDataSubject(dataSubject);
consent.setPurpose(request.getPurpose());
consent.setLegalBasis(request.getLegalBasis());
consent.setGranted(true);
consent.setConsentText(request.getConsentText());
consent.setVersion(request.getVersion());
consent.setGrantedAt(LocalDateTime.now());
consent.setExpiresAt(request.getExpiresAt());
consent.setIpAddress(ipAddress);
consent.setUserAgent(userAgent);
return consentRecordRepository.save(consent);
}
@Transactional
public void revokeConsent(Long consentId) {
consentRecordRepository.findById(consentId).ifPresent(consent -> {
consent.setGranted(false);
consent.setRevokedAt(LocalDateTime.now());
consentRecordRepository.save(consent);
});
}
@Transactional
public void revokeAllConsents(String subjectId) {
DataSubject dataSubject = dataSubjectRepository.findBySubjectId(subjectId)
.orElseThrow(() -> new RuntimeException("Data subject not found"));
consentRecordRepository.findByDataSubjectAndGranted(dataSubject, true)
.forEach(consent -> {
consent.setGranted(false);
consent.setRevokedAt(LocalDateTime.now());
consentRecordRepository.save(consent);
});
}
public boolean hasValidConsent(String subjectId, String purpose) {
return dataSubjectRepository.findBySubjectId(subjectId)
.flatMap(dataSubject -> 
consentRecordRepository.findByDataSubjectAndPurposeAndGranted(
dataSubject, purpose, true))
.filter(consent -> 
consent.getExpiresAt() == null || 
consent.getExpiresAt().isAfter(LocalDateTime.now()))
.isPresent();
}
public List<ConsentRecord> getConsentHistory(String subjectId) {
return dataSubjectRepository.findBySubjectId(subjectId)
.map(dataSubject -> consentRecordRepository.findByDataSubject(dataSubject))
.orElse(Collections.emptyList());
}
public ConsentAuditReport generateConsentAudit(String subjectId) {
DataSubject dataSubject = dataSubjectRepository.findBySubjectId(subjectId)
.orElseThrow(() -> new RuntimeException("Data subject not found"));
List<ConsentRecord> consents = consentRecordRepository.findByDataSubject(dataSubject);
ConsentAuditReport report = new ConsentAuditReport();
report.setDataSubject(dataSubject);
report.setTotalConsents(consents.size());
report.setActiveConsents(consents.stream()
.filter(ConsentRecord::isGranted)
.filter(c -> c.getExpiresAt() == null || c.getExpiresAt().isAfter(LocalDateTime.now()))
.count());
report.setConsentRecords(consents);
report.setGeneratedAt(LocalDateTime.now());
return report;
}
private DataSubject createDataSubject(String subjectId, String email) {
DataSubject dataSubject = new DataSubject();
dataSubject.setSubjectId(subjectId);
dataSubject.setEmail(email);
return dataSubjectRepository.save(dataSubject);
}
}
class ConsentRequest {
private String purpose;
private String legalBasis;
private String consentText;
private String version;
private LocalDateTime expiresAt;
private String email;
// Getters and setters
public String getPurpose() { return purpose; }
public void setPurpose(String purpose) { this.purpose = purpose; }
public String getLegalBasis() { return legalBasis; }
public void setLegalBasis(String legalBasis) { this.legalBasis = legalBasis; }
public String getConsentText() { return consentText; }
public void setConsentText(String consentText) { this.consentText = consentText; }
public String getVersion() { return version; }
public void setVersion(String version) { this.version = version; }
public LocalDateTime getExpiresAt() { return expiresAt; }
public void setExpiresAt(LocalDateTime expiresAt) { this.expiresAt = expiresAt; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
class ConsentAuditReport {
private DataSubject dataSubject;
private long totalConsents;
private long activeConsents;
private List<ConsentRecord> consentRecords;
private LocalDateTime generatedAt;
// Getters and setters
public DataSubject getDataSubject() { return dataSubject; }
public void setDataSubject(DataSubject dataSubject) { this.dataSubject = dataSubject; }
public long getTotalConsents() { return totalConsents; }
public void setTotalConsents(long totalConsents) { this.totalConsents = totalConsents; }
public long getActiveConsents() { return activeConsents; }
public void setActiveConsents(long activeConsents) { this.activeConsents = activeConsents; }
public List<ConsentRecord> getConsentRecords() { return consentRecords; }
public void setConsentRecords(List<ConsentRecord> consentRecords) { this.consentRecords = consentRecords; }
public LocalDateTime getGeneratedAt() { return generatedAt; }
public void setGeneratedAt(LocalDateTime generatedAt) { this.generatedAt = generatedAt; }
}

3. Data Subject Rights Service

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class DataSubjectRightsService {
private final DataSubjectRepository dataSubjectRepository;
private final DataAccessRequestRepository accessRequestRepository;
private final ProcessingActivityRepository processingActivityRepository;
private final DataErasureService dataErasureService;
private final DataPortabilityService dataPortabilityService;
public DataSubjectRightsService(DataSubjectRepository dataSubjectRepository,
DataAccessRequestRepository accessRequestRepository,
ProcessingActivityRepository processingActivityRepository,
DataErasureService dataErasureService,
DataPortabilityService dataPortabilityService) {
this.dataSubjectRepository = dataSubjectRepository;
this.accessRequestRepository = accessRequestRepository;
this.processingActivityRepository = processingActivityRepository;
this.dataErasureService = dataErasureService;
this.dataPortabilityService = dataPortabilityService;
}
@Transactional
public DataAccessRequest submitAccessRequest(String subjectId, RequestType type, 
String details) {
DataSubject dataSubject = dataSubjectRepository.findBySubjectId(subjectId)
.orElseGet(() -> createDataSubject(subjectId));
DataAccessRequest request = new DataAccessRequest();
request.setDataSubject(dataSubject);
request.setType(type);
request.setStatus(RequestStatus.PENDING);
request.setRequestedAt(LocalDateTime.now());
request.setDueDate(LocalDateTime.now().plusDays(30)); // GDPR requires response within 30 days
request.setRequestDetails(details);
return accessRequestRepository.save(request);
}
@Transactional
public DataAccessRequest processAccessRequest(Long requestId, String assignedTo) {
DataAccessRequest request = accessRequestRepository.findById(requestId)
.orElseThrow(() -> new RuntimeException("Request not found"));
request.setStatus(RequestStatus.IN_PROGRESS);
request.setAssignedTo(assignedTo);
return accessRequestRepository.save(request);
}
@Transactional
public DataAccessRequest completeAccessRequest(Long requestId, String responseData) {
DataAccessRequest request = accessRequestRepository.findById(requestId)
.orElseThrow(() -> new RuntimeException("Request not found"));
request.setStatus(RequestStatus.COMPLETED);
request.setCompletedAt(LocalDateTime.now());
request.setResponseData(responseData);
return accessRequestRepository.save(request);
}
public String generateDataPortabilityReport(String subjectId) {
DataSubject dataSubject = dataSubjectRepository.findBySubjectId(subjectId)
.orElseThrow(() -> new RuntimeException("Data subject not found"));
return dataPortabilityService.generatePortableData(dataSubject);
}
@Transactional
public ErasureResult processRightToErasure(String subjectId, String reason) {
DataSubject dataSubject = dataSubjectRepository.findBySubjectId(subjectId)
.orElseThrow(() -> new RuntimeException("Data subject not found"));
// Submit erasure request
DataAccessRequest request = submitAccessRequest(subjectId, RequestType.ERASURE, reason);
// Process erasure
ErasureResult result = dataErasureService.erasePersonalData(dataSubject);
// Update request status
request.setStatus(RequestStatus.COMPLETED);
request.setCompletedAt(LocalDateTime.now());
request.setResponseData("Erasure completed: " + result.getSummary());
accessRequestRepository.save(request);
return result;
}
public DataSubjectRightsSummary getRightsSummary(String subjectId) {
DataSubject dataSubject = dataSubjectRepository.findBySubjectId(subjectId)
.orElseThrow(() -> new RuntimeException("Data subject not found"));
List<DataAccessRequest> requests = accessRequestRepository.findByDataSubject(dataSubject);
List<ProcessingActivity> activities = processingActivityRepository.findByDataSubject(dataSubject);
DataSubjectRightsSummary summary = new DataSubjectRightsSummary();
summary.setDataSubject(dataSubject);
summary.setTotalRequests(requests.size());
summary.setPendingRequests(requests.stream()
.filter(r -> r.getStatus() == RequestStatus.PENDING)
.count());
summary.setActiveProcessingActivities(activities.size());
summary.setLastRequest(requests.stream()
.map(DataAccessRequest::getRequestedAt)
.max(LocalDateTime::compareTo)
.orElse(null));
return summary;
}
private DataSubject createDataSubject(String subjectId) {
DataSubject dataSubject = new DataSubject();
dataSubject.setSubjectId(subjectId);
return dataSubjectRepository.save(dataSubject);
}
}
class DataSubjectRightsSummary {
private DataSubject dataSubject;
private long totalRequests;
private long pendingRequests;
private long activeProcessingActivities;
private LocalDateTime lastRequest;
// Getters and setters
public DataSubject getDataSubject() { return dataSubject; }
public void setDataSubject(DataSubject dataSubject) { this.dataSubject = dataSubject; }
public long getTotalRequests() { return totalRequests; }
public void setTotalRequests(long totalRequests) { this.totalRequests = totalRequests; }
public long getPendingRequests() { return pendingRequests; }
public void setPendingRequests(long pendingRequests) { this.pendingRequests = pendingRequests; }
public long getActiveProcessingActivities() { return activeProcessingActivities; }
public void setActiveProcessingActivities(long activeProcessingActivities) { this.activeProcessingActivities = activeProcessingActivities; }
public LocalDateTime getLastRequest() { return lastRequest; }
public void setLastRequest(LocalDateTime lastRequest) { this.lastRequest = lastRequest; }
}

4. Data Erasure Service

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
@Service
public class DataErasureService {
private final DataSubjectRepository dataSubjectRepository;
private final ConsentRecordRepository consentRecordRepository;
private final ProcessingActivityRepository processingActivityRepository;
private final List<DataErasureHandler> erasureHandlers;
public DataErasureService(DataSubjectRepository dataSubjectRepository,
ConsentRecordRepository consentRecordRepository,
ProcessingActivityRepository processingActivityRepository,
List<DataErasureHandler> erasureHandlers) {
this.dataSubjectRepository = dataSubjectRepository;
this.consentRecordRepository = consentRecordRepository;
this.processingActivityRepository = processingActivityRepository;
this.erasureHandlers = erasureHandlers;
}
@Transactional
public ErasureResult erasePersonalData(DataSubject dataSubject) {
List<ErasureStep> steps = new ArrayList<>();
// Step 1: Anonymize personal data in the main entity
steps.add(anonymizeDataSubject(dataSubject));
// Step 2: Revoke all consents
steps.add(revokeAllConsents(dataSubject));
// Step 3: Stop processing activities
steps.add(stopProcessingActivities(dataSubject));
// Step 4: Call custom erasure handlers
steps.addAll(executeCustomErasureHandlers(dataSubject));
// Step 5: Create erasure audit trail
steps.add(createErasureAudit(dataSubject));
return new ErasureResult(dataSubject.getSubjectId(), steps);
}
private ErasureStep anonymizeDataSubject(DataSubject dataSubject) {
try {
// Anonymize personal data
dataSubject.setEmail(generateAnonymousIdentifier());
dataSubject.setPhone(null);
if (dataSubject.getPersonalData() != null) {
PersonalData personalData = dataSubject.getPersonalData();
personalData.setFirstName("ANONYMIZED");
personalData.setLastName("ANONYMIZED");
personalData.setDateOfBirth(null);
personalData.setAddress(null);
personalData.setCity(null);
personalData.setPostalCode(null);
}
dataSubjectRepository.save(dataSubject);
return new ErasureStep("ANONYMIZE_SUBJECT", "SUCCESS", 
"Personal data anonymized successfully");
} catch (Exception e) {
return new ErasureStep("ANONYMIZE_SUBJECT", "FAILED", e.getMessage());
}
}
private ErasureStep revokeAllConsents(DataSubject dataSubject) {
try {
List<ConsentRecord> consents = consentRecordRepository.findByDataSubject(dataSubject);
consents.forEach(consent -> {
consent.setGranted(false);
consent.setRevokedAt(LocalDateTime.now());
});
consentRecordRepository.saveAll(consents);
return new ErasureStep("REVOKE_CONSENTS", "SUCCESS", 
"Revoked " + consents.size() + " consent records");
} catch (Exception e) {
return new ErasureStep("REVOKE_CONSENTS", "FAILED", e.getMessage());
}
}
private ErasureStep stopProcessingActivities(DataSubject dataSubject) {
try {
List<ProcessingActivity> activities = 
processingActivityRepository.findByDataSubject(dataSubject);
activities.forEach(activity -> 
activity.setProcessingEnd(LocalDateTime.now()));
processingActivityRepository.saveAll(activities);
return new ErasureStep("STOP_PROCESSING", "SUCCESS", 
"Stopped " + activities.size() + " processing activities");
} catch (Exception e) {
return new ErasureStep("STOP_PROCESSING", "FAILED", e.getMessage());
}
}
private List<ErasureStep> executeCustomErasureHandlers(DataSubject dataSubject) {
List<ErasureStep> steps = new ArrayList<>();
for (DataErasureHandler handler : erasureHandlers) {
try {
ErasureStep step = handler.eraseData(dataSubject);
steps.add(step);
} catch (Exception e) {
steps.add(new ErasureStep(handler.getHandlerName(), "FAILED", e.getMessage()));
}
}
return steps;
}
private ErasureStep createErasureAudit(DataSubject dataSubject) {
// Create an audit record of the erasure
// This could be stored in a separate audit table
return new ErasureStep("CREATE_AUDIT", "SUCCESS", 
"Erasure audit trail created for subject: " + dataSubject.getSubjectId());
}
private String generateAnonymousIdentifier() {
return "anon_" + UUID.randomUUID().toString().substring(0, 8);
}
}
interface DataErasureHandler {
ErasureStep eraseData(DataSubject dataSubject);
String getHandlerName();
}
@Service
class UserProfileErasureHandler implements DataErasureHandler {
private final UserProfileRepository userProfileRepository;
public UserProfileErasureHandler(UserProfileRepository userProfileRepository) {
this.userProfileRepository = userProfileRepository;
}
@Override
public ErasureStep eraseData(DataSubject dataSubject) {
try {
userProfileRepository.findBySubjectId(dataSubject.getSubjectId())
.ifPresent(profile -> {
profile.setAvatarUrl(null);
profile.setBiography(null);
profile.setPreferences("{}");
userProfileRepository.save(profile);
});
return new ErasureStep("ERASE_PROFILE", "SUCCESS", 
"User profile data erased");
} catch (Exception e) {
return new ErasureStep("ERASE_PROFILE", "FAILED", e.getMessage());
}
}
@Override
public String getHandlerName() {
return "USER_PROFILE_ERASURE";
}
}
class ErasureResult {
private String subjectId;
private List<ErasureStep> steps;
private LocalDateTime completedAt;
private String summary;
public ErasureResult(String subjectId, List<ErasureStep> steps) {
this.subjectId = subjectId;
this.steps = steps;
this.completedAt = LocalDateTime.now();
this.summary = generateSummary();
}
private String generateSummary() {
long successful = steps.stream()
.filter(step -> "SUCCESS".equals(step.getStatus()))
.count();
return String.format("Erasure completed: %d/%d steps successful", 
successful, steps.size());
}
// Getters
public String getSubjectId() { return subjectId; }
public List<ErasureStep> getSteps() { return steps; }
public LocalDateTime getCompletedAt() { return completedAt; }
public String getSummary() { return summary; }
}
class ErasureStep {
private String operation;
private String status;
private String message;
private LocalDateTime timestamp;
public ErasureStep(String operation, String status, String message) {
this.operation = operation;
this.status = status;
this.message = message;
this.timestamp = LocalDateTime.now();
}
// Getters
public String getOperation() { return operation; }
public String getStatus() { return status; }
public String getMessage() { return message; }
public LocalDateTime getTimestamp() { return timestamp; }
}

5. Data Portability Service

import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import java.util.*;
@Service
public class DataPortabilityService {
private final DataSubjectRepository dataSubjectRepository;
private final ConsentRecordRepository consentRecordRepository;
private final ProcessingActivityRepository processingActivityRepository;
private final ObjectMapper objectMapper;
private final List<DataPortabilityExporter> exporters;
public DataPortabilityService(DataSubjectRepository dataSubjectRepository,
ConsentRecordRepository consentRecordRepository,
ProcessingActivityRepository processingActivityRepository,
List<DataPortabilityExporter> exporters) {
this.dataSubjectRepository = dataSubjectRepository;
this.consentRecordRepository = consentRecordRepository;
this.processingActivityRepository = processingActivityRepository;
this.exporters = exporters;
this.objectMapper = new ObjectMapper();
this.objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
}
public String generatePortableData(DataSubject dataSubject) {
try {
PortableData portableData = new PortableData();
portableData.setSubjectId(dataSubject.getSubjectId());
portableData.setGeneratedAt(LocalDateTime.now());
portableData.setFormatVersion("1.0");
// Basic subject information (anonymized if needed)
portableData.setBasicInformation(extractBasicInformation(dataSubject));
// Consent history
portableData.setConsents(extractConsentHistory(dataSubject));
// Processing activities
portableData.setProcessingActivities(extractProcessingActivities(dataSubject));
// Custom data from exporters
portableData.setCustomData(extractCustomData(dataSubject));
return objectMapper.writeValueAsString(portableData);
} catch (Exception e) {
throw new RuntimeException("Failed to generate portable data: " + e.getMessage(), e);
}
}
private BasicInformation extractBasicInformation(DataSubject dataSubject) {
BasicInformation info = new BasicInformation();
info.setEmail(dataSubject.getEmail());
info.setCreatedAt(dataSubject.getCreatedAt());
info.setUpdatedAt(dataSubject.getUpdatedAt());
return info;
}
private List<PortableConsent> extractConsentHistory(DataSubject dataSubject) {
return consentRecordRepository.findByDataSubject(dataSubject).stream()
.map(consent -> {
PortableConsent pc = new PortableConsent();
pc.setPurpose(consent.getPurpose());
pc.setLegalBasis(consent.getLegalBasis());
pc.setGranted(consent.isGranted());
pc.setGrantedAt(consent.getGrantedAt());
pc.setRevokedAt(consent.getRevokedAt());
pc.setExpiresAt(consent.getExpiresAt());
return pc;
})
.collect(Collectors.toList());
}
private List<PortableProcessingActivity> extractProcessingActivities(DataSubject dataSubject) {
return processingActivityRepository.findByDataSubject(dataSubject).stream()
.map(activity -> {
PortableProcessingActivity ppa = new PortableProcessingActivity();
ppa.setName(activity.getName());
ppa.setPurpose(activity.getPurpose());
ppa.setLegalBasis(activity.getLegalBasis().name());
ppa.setProcessingStart(activity.getProcessingStart());
ppa.setProcessingEnd(activity.getProcessingEnd());
ppa.setDataCategories(activity.getDataCategories().stream()
.map(Enum::name)
.collect(Collectors.toList()));
return ppa;
})
.collect(Collectors.toList());
}
private Map<String, Object> extractCustomData(DataSubject dataSubject) {
Map<String, Object> customData = new HashMap<>();
for (DataPortabilityExporter exporter : exporters) {
try {
customData.put(exporter.getExporterName(), 
exporter.exportData(dataSubject));
} catch (Exception e) {
customData.put(exporter.getExporterName(), 
Map.of("error", "Failed to export: " + e.getMessage()));
}
}
return customData;
}
}
interface DataPortabilityExporter {
Object exportData(DataSubject dataSubject);
String getExporterName();
}
@Service
class UserActivityExporter implements DataPortabilityExporter {
private final UserActivityRepository userActivityRepository;
public UserActivityExporter(UserActivityRepository userActivityRepository) {
this.userActivityRepository = userActivityRepository;
}
@Override
public Object exportData(DataSubject dataSubject) {
List<UserActivity> activities = 
userActivityRepository.findBySubjectId(dataSubject.getSubjectId());
return activities.stream()
.map(activity -> Map.of(
"type", activity.getType(),
"timestamp", activity.getTimestamp(),
"details", activity.getDetails()
))
.collect(Collectors.toList());
}
@Override
public String getExporterName() {
return "user_activities";
}
}
class PortableData {
private String subjectId;
private LocalDateTime generatedAt;
private String formatVersion;
private BasicInformation basicInformation;
private List<PortableConsent> consents;
private List<PortableProcessingActivity> processingActivities;
private Map<String, Object> customData;
// Getters and setters
public String getSubjectId() { return subjectId; }
public void setSubjectId(String subjectId) { this.subjectId = subjectId; }
public LocalDateTime getGeneratedAt() { return generatedAt; }
public void setGeneratedAt(LocalDateTime generatedAt) { this.generatedAt = generatedAt; }
public String getFormatVersion() { return formatVersion; }
public void setFormatVersion(String formatVersion) { this.formatVersion = formatVersion; }
public BasicInformation getBasicInformation() { return basicInformation; }
public void setBasicInformation(BasicInformation basicInformation) { this.basicInformation = basicInformation; }
public List<PortableConsent> getConsents() { return consents; }
public void setConsents(List<PortableConsent> consents) { this.consents = consents; }
public List<PortableProcessingActivity> getProcessingActivities() { return processingActivities; }
public void setProcessingActivities(List<PortableProcessingActivity> processingActivities) { this.processingActivities = processingActivities; }
public Map<String, Object> getCustomData() { return customData; }
public void setCustomData(Map<String, Object> customData) { this.customData = customData; }
}
class BasicInformation {
private String email;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// Getters and setters
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
}
class PortableConsent {
private String purpose;
private String legalBasis;
private boolean granted;
private LocalDateTime grantedAt;
private LocalDateTime revokedAt;
private LocalDateTime expiresAt;
// Getters and setters
// ... (similar to above)
}
class PortableProcessingActivity {
private String name;
private String purpose;
private String legalBasis;
private LocalDateTime processingStart;
private LocalDateTime processingEnd;
private List<String> dataCategories;
// Getters and setters
// ... (similar to above)
}

6. REST Controllers

import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import org.springframework.http.HttpStatus;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.util.*;
@RestController
@RequestMapping("/api/gdpr")
public class GDPRController {
private final ConsentManagementService consentService;
private final DataSubjectRightsService rightsService;
public GDPRController(ConsentManagementService consentService,
DataSubjectRightsService rightsService) {
this.consentService = consentService;
this.rightsService = rightsService;
}
@PostMapping("/consent")
public ResponseEntity<ApiResponse<ConsentRecord>> grantConsent(
@Valid @RequestBody ConsentRequest request,
@RequestHeader("X-Subject-ID") String subjectId,
HttpServletRequest httpRequest) {
String ipAddress = httpRequest.getRemoteAddr();
String userAgent = httpRequest.getHeader("User-Agent");
ConsentRecord consent = consentService.grantConsent(
subjectId, request, ipAddress, userAgent);
return ResponseEntity.status(HttpStatus.CREATED)
.body(new ApiResponse<>("Consent granted successfully", consent));
}
@DeleteMapping("/consent/{consentId}")
public ResponseEntity<ApiResponse<Void>> revokeConsent(
@PathVariable Long consentId) {
consentService.revokeConsent(consentId);
return ResponseEntity.ok(new ApiResponse<>("Consent revoked successfully", null));
}
@GetMapping("/consent/history")
public ResponseEntity<ApiResponse<List<ConsentRecord>>> getConsentHistory(
@RequestHeader("X-Subject-ID") String subjectId) {
List<ConsentRecord> history = consentService.getConsentHistory(subjectId);
return ResponseEntity.ok(new ApiResponse<>("Consent history retrieved", history));
}
@PostMapping("/rights/request")
public ResponseEntity<ApiResponse<DataAccessRequest>> submitRightsRequest(
@RequestHeader("X-Subject-ID") String subjectId,
@RequestParam RequestType type,
@RequestBody(required = false) String details) {
DataAccessRequest request = rightsService.submitAccessRequest(subjectId, type, details);
return ResponseEntity.status(HttpStatus.CREATED)
.body(new ApiResponse<>("Rights request submitted", request));
}
@GetMapping("/rights/portability")
public ResponseEntity<ApiResponse<String>> getPortableData(
@RequestHeader("X-Subject-ID") String subjectId) {
String portableData = rightsService.generateDataPortabilityReport(subjectId);
return ResponseEntity.ok(new ApiResponse<>("Portable data generated", portableData));
}
@PostMapping("/rights/erasure")
public ResponseEntity<ApiResponse<ErasureResult>> requestErasure(
@RequestHeader("X-Subject-ID") String subjectId,
@RequestBody(required = false) String reason) {
ErasureResult result = rightsService.processRightToErasure(subjectId, reason);
return ResponseEntity.ok(new ApiResponse<>("Erasure request processed", result));
}
@GetMapping("/rights/summary")
public ResponseEntity<ApiResponse<DataSubjectRightsSummary>> getRightsSummary(
@RequestHeader("X-Subject-ID") String subjectId) {
DataSubjectRightsSummary summary = rightsService.getRightsSummary(subjectId);
return ResponseEntity.ok(new ApiResponse<>("Rights summary retrieved", summary));
}
@GetMapping("/consent/audit")
public ResponseEntity<ApiResponse<ConsentAuditReport>> getConsentAudit(
@RequestHeader("X-Subject-ID") String subjectId) {
ConsentAuditReport audit = consentService.generateConsentAudit(subjectId);
return ResponseEntity.ok(new ApiResponse<>("Consent audit generated", audit));
}
}
class ApiResponse<T> {
private String message;
private T data;
private LocalDateTime timestamp;
public ApiResponse(String message, T data) {
this.message = message;
this.data = data;
this.timestamp = LocalDateTime.now();
}
// Getters and setters
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public T getData() { return data; }
public void setData(T data) { this.data = data; }
public LocalDateTime getTimestamp() { return timestamp; }
public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; }
}

7. Security and Configuration

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/gdpr/**").authenticated()
.anyRequest().permitAll()
)
.httpBasic(withDefaults());
return http.build();
}
}
@Configuration
@ConfigurationProperties(prefix = "gdpr")
public class GDPRConfig {
private int defaultRetentionYears = 7;
private int requestResponseDays = 30;
private boolean requireExplicitConsent = true;
private List<String> allowedDataCategories = Arrays.asList(
"IDENTIFYING", "CONTACT", "DEMOGRAPHIC"
);
// Getters and setters
public int getDefaultRetentionYears() { return defaultRetentionYears; }
public void setDefaultRetentionYears(int defaultRetentionYears) { this.defaultRetentionYears = defaultRetentionYears; }
public int getRequestResponseDays() { return requestResponseDays; }
public void setRequestResponseDays(int requestResponseDays) { this.requestResponseDays = requestResponseDays; }
public boolean isRequireExplicitConsent() { return requireExplicitConsent; }
public void setRequireExplicitConsent(boolean requireExplicitConsent) { this.requireExplicitConsent = requireExplicitConsent; }
public List<String> getAllowedDataCategories() { return allowedDataCategories; }
public void setAllowedDataCategories(List<String> allowedDataCategories) { this.allowedDataCategories = allowedDataCategories; }
}

Key Features

  1. Consent Management: Track, manage, and audit user consents
  2. Data Subject Rights: Implement all GDPR rights (access, erasure, portability, etc.)
  3. Data Erasure: Secure and comprehensive data deletion
  4. Data Portability: Export user data in standardized formats
  5. Processing Activities: Record and manage data processing activities
  6. Audit Trail: Comprehensive logging of all GDPR-related activities
  7. Security: Secure API endpoints with proper authentication
  8. Extensibility: Plugin architecture for custom data handlers

This toolkit provides a solid foundation for GDPR compliance in Java applications, with extensible architecture to accommodate specific business requirements.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper