Showcase Mod

Data API

Access and manipulate share data programmatically with advanced storage options

Data API

MC 1.21.1MC 1.21.2MC 1.21.4MC 1.21.5MC 1.21.6

The Showcase Data API provides comprehensive access to share data, player statistics, and storage systems. It enables developers to build custom data providers, implement advanced analytics, and create sophisticated integrations.

Data Provider Interface

Custom Storage Implementation

public class DatabaseDataProvider implements DataProvider {

    private final DataSource dataSource;

    public DatabaseDataProvider(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Override
    public void saveShare(ShareData share) {
        try (Connection conn = dataSource.getConnection()) {
            // Save main share data
            saveShareMetadata(conn, share);

            // Save type-specific content
            switch (share.getType()) {
                case ITEM:
                    saveItemData(conn, share.getId(), share.getItem());
                    break;
                case INVENTORY:
                    saveInventoryData(conn, share.getId(), share.getItems());
                    break;
                case STATS:
                    saveStatsData(conn, share.getId(), share.getStats());
                    break;
                case CONTAINER:
                    saveContainerData(conn, share.getId(), share.getContainer());
                    break;
            }

            // Save receivers
            saveReceivers(conn, share.getId(), share.getReceivers());

        } catch (SQLException e) {
            throw new DataStorageException("Failed to save share: " + share.getId(), e);
        }
    }

    @Override
    public Optional<ShareData> loadShare(String shareId) {
        try (Connection conn = dataSource.getConnection()) {
            return loadShareFromDatabase(conn, shareId);
        } catch (SQLException e) {
            throw new DataStorageException("Failed to load share: " + shareId, e);
        }
    }

    @Override
    public List<ShareData> getPlayerShares(UUID playerId) {
        try (Connection conn = dataSource.getConnection()) {
            return loadPlayerShares(conn, playerId);
        } catch (SQLException e) {
            throw new DataStorageException("Failed to load player shares", e);
        }
    }

    @Override
    public void cleanup() {
        try (Connection conn = dataSource.getConnection()) {
            cleanupExpiredShares(conn);
        } catch (SQLException e) {
            throw new DataStorageException("Failed to cleanup shares", e);
        }
    }

    // Implementation details...
    private void saveShareMetadata(Connection conn, ShareData share) throws SQLException {
        PreparedStatement stmt = conn.prepareStatement(
            "INSERT INTO shares (id, creator_uuid, type, created_at, expires_at, description, view_count) " +
            "VALUES (?, ?, ?, ?, ?, ?, ?) " +
            "ON DUPLICATE KEY UPDATE " +
            "description = VALUES(description), expires_at = VALUES(expires_at), view_count = VALUES(view_count)"
        );

        stmt.setString(1, share.getId());
        stmt.setString(2, share.getCreator().getUuidAsString());
        stmt.setString(3, share.getType().name());
        stmt.setTimestamp(4, Timestamp.from(share.getCreatedAt()));
        stmt.setTimestamp(5, Timestamp.from(share.getExpiresAt()));
        stmt.setString(6, share.getDescription());
        stmt.setInt(7, share.getViewCount());

        stmt.executeUpdate();
    }
}

Redis Data Provider

public class RedisDataProvider implements DataProvider {

    private final RedisTemplate<String, Object> redisTemplate;
    private final ObjectMapper objectMapper;

    public RedisDataProvider(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
        this.objectMapper = new ObjectMapper();
    }

    @Override
    public void saveShare(ShareData share) {
        try {
            String key = "share:" + share.getId();
            ShareDataDTO dto = ShareDataDTO.fromShareData(share);
            String json = objectMapper.writeValueAsString(dto);

            // Store with expiration
            Duration ttl = Duration.between(Instant.now(), share.getExpiresAt());
            redisTemplate.opsForValue().set(key, json, ttl);

            // Add to player index
            String playerKey = "player_shares:" + share.getCreator().getUuidAsString();
            redisTemplate.opsForSet().add(playerKey, share.getId());

            // Add to type index
            String typeKey = "shares_by_type:" + share.getType().name();
            redisTemplate.opsForSet().add(typeKey, share.getId());

        } catch (JsonProcessingException e) {
            throw new DataStorageException("Failed to serialize share data", e);
        }
    }

    @Override
    public Optional<ShareData> loadShare(String shareId) {
        try {
            String key = "share:" + shareId;
            String json = (String) redisTemplate.opsForValue().get(key);

            if (json != null) {
                ShareDataDTO dto = objectMapper.readValue(json, ShareDataDTO.class);
                return Optional.of(dto.toShareData());
            }

            return Optional.empty();
        } catch (JsonProcessingException e) {
            throw new DataStorageException("Failed to deserialize share data", e);
        }
    }

    @Override
    public List<ShareData> getPlayerShares(UUID playerId) {
        String playerKey = "player_shares:" + playerId.toString();
        Set<Object> shareIds = redisTemplate.opsForSet().members(playerKey);

        return shareIds.stream()
            .map(id -> loadShare((String) id))
            .filter(Optional::isPresent)
            .map(Optional::get)
            .collect(Collectors.toList());
    }

    @Override
    public void cleanup() {
        // Redis automatically expires keys, but we can clean up indexes
        cleanupPlayerIndexes();
        cleanupTypeIndexes();
    }
}

Advanced Data Operations

Bulk Operations

public class BulkDataOperations {

    public void bulkSaveShares(List<ShareData> shares) {
        ShowcaseAPI api = ShowcaseAPI.getInstance();
        DataProvider provider = api.getDataProvider();

        if (provider instanceof BulkDataProvider) {
            // Use bulk operations if supported
            ((BulkDataProvider) provider).bulkSave(shares);
        } else {
            // Fallback to individual saves
            shares.parallelStream().forEach(provider::saveShare);
        }
    }

    public Map<String, ShareData> bulkLoadShares(List<String> shareIds) {
        ShowcaseAPI api = ShowcaseAPI.getInstance();
        DataProvider provider = api.getDataProvider();

        if (provider instanceof BulkDataProvider) {
            return ((BulkDataProvider) provider).bulkLoad(shareIds);
        } else {
            return shareIds.parallelStream()
                .collect(Collectors.toMap(
                    id -> id,
                    id -> provider.loadShare(id).orElse(null)
                ))
                .entrySet().stream()
                .filter(entry -> entry.getValue() != null)
                .collect(Collectors.toMap(
                    Map.Entry::getKey,
                    Map.Entry::getValue
                ));
        }
    }
}

public interface BulkDataProvider extends DataProvider {
    void bulkSave(List<ShareData> shares);
    Map<String, ShareData> bulkLoad(List<String> shareIds);
    void bulkDelete(List<String> shareIds);
}

Data Migration

public class DataMigration {

    public void migrateFromFileToDatabase() {
        ShowcaseAPI api = ShowcaseAPI.getInstance();

        // Get current provider (file-based)
        DataProvider oldProvider = api.getDataProvider();

        // Create new provider (database)
        DataProvider newProvider = new DatabaseDataProvider(dataSource);

        // Migrate all data
        try {
            migrateShares(oldProvider, newProvider);
            migratePlayerStats(oldProvider, newProvider);
            migrateConfiguration(oldProvider, newProvider);

            // Switch to new provider
            api.setDataProvider(newProvider);

            System.out.println("Migration completed successfully");
        } catch (Exception e) {
            System.err.println("Migration failed: " + e.getMessage());
            throw new RuntimeException("Data migration failed", e);
        }
    }

    private void migrateShares(DataProvider from, DataProvider to) {
        // This would need to be implemented based on the old provider's API
        // For example, if migrating from JSON files:
        List<ShareData> allShares = loadAllSharesFromFiles(from);

        for (ShareData share : allShares) {
            to.saveShare(share);
        }
    }
}

Data Serialization

Custom Serializers

public class ShareDataSerializer {

    public static class ShareDataDTO {
        public String id;
        public String creatorUuid;
        public String type;
        public String createdAt;
        public String expiresAt;
        public String description;
        public Set<String> receivers;
        public Object content; // Type-specific content
        public int viewCount;
        public List<String> viewers;

        public static ShareDataDTO fromShareData(ShareData share) {
            ShareDataDTO dto = new ShareDataDTO();
            dto.id = share.getId();
            dto.creatorUuid = share.getCreator().getUuidAsString();
            dto.type = share.getType().name();
            dto.createdAt = share.getCreatedAt().toString();
            dto.expiresAt = share.getExpiresAt().toString();
            dto.description = share.getDescription();
            dto.receivers = share.getReceivers().stream()
                .map(UUID::toString)
                .collect(Collectors.toSet());
            dto.viewCount = share.getViewCount();
            dto.viewers = share.getViewers().stream()
                .map(UUID::toString)
                .collect(Collectors.toList());

            // Serialize type-specific content
            dto.content = serializeContent(share);

            return dto;
        }

        public ShareData toShareData() {
            // Deserialize back to ShareData
            // Implementation depends on your ShareData constructor
            return ShareData.builder()
                .id(id)
                .creator(getPlayerByUuid(UUID.fromString(creatorUuid)))
                .type(ShareType.valueOf(type))
                .createdAt(Instant.parse(createdAt))
                .expiresAt(Instant.parse(expiresAt))
                .description(description)
                .receivers(receivers.stream().map(UUID::fromString).collect(Collectors.toSet()))
                .content(deserializeContent(content, ShareType.valueOf(type)))
                .viewCount(viewCount)
                .viewers(viewers.stream().map(UUID::fromString).collect(Collectors.toList()))
                .build();
        }

        private static Object serializeContent(ShareData share) {
            switch (share.getType()) {
                case ITEM:
                    return ItemStackSerializer.serialize(share.getItem());
                case INVENTORY:
                case HOTBAR:
                case ENDERCHEST:
                    return ItemStackSerializer.serializeArray(share.getItems());
                case STATS:
                    return PlayerStatsSerializer.serialize(share.getStats());
                case CONTAINER:
                    return ContainerDataSerializer.serialize(share.getContainer());
                default:
                    return null;
            }
        }
    }
}

NBT Data Handling

public class NBTDataHandler {

    public static NbtCompound serializeShareToNBT(ShareData share) {
        NbtCompound nbt = new NbtCompound();

        nbt.putString("id", share.getId());
        nbt.putUuid("creator", share.getCreator().getUuid());
        nbt.putString("type", share.getType().name());
        nbt.putLong("createdAt", share.getCreatedAt().toEpochMilli());
        nbt.putLong("expiresAt", share.getExpiresAt().toEpochMilli());

        if (share.getDescription() != null) {
            nbt.putString("description", share.getDescription());
        }

        // Serialize receivers
        NbtList receiversList = new NbtList();
        for (UUID receiver : share.getReceivers()) {
            receiversList.add(NbtHelper.fromUuid(receiver));
        }
        nbt.put("receivers", receiversList);

        // Serialize content based on type
        switch (share.getType()) {
            case ITEM:
                nbt.put("item", share.getItem().writeNbt(new NbtCompound()));
                break;
            case INVENTORY:
            case HOTBAR:
            case ENDERCHEST:
                NbtList itemsList = new NbtList();
                for (ItemStack item : share.getItems()) {
                    if (item != null && !item.isEmpty()) {
                        itemsList.add(item.writeNbt(new NbtCompound()));
                    }
                }
                nbt.put("items", itemsList);
                break;
            case STATS:
                nbt.put("stats", PlayerStatsSerializer.toNBT(share.getStats()));
                break;
            case CONTAINER:
                nbt.put("container", ContainerDataSerializer.toNBT(share.getContainer()));
                break;
        }

        return nbt;
    }

    public static ShareData deserializeShareFromNBT(NbtCompound nbt) {
        String id = nbt.getString("id");
        UUID creatorUuid = nbt.getUuid("creator");
        ShareType type = ShareType.valueOf(nbt.getString("type"));
        Instant createdAt = Instant.ofEpochMilli(nbt.getLong("createdAt"));
        Instant expiresAt = Instant.ofEpochMilli(nbt.getLong("expiresAt"));
        String description = nbt.contains("description") ? nbt.getString("description") : null;

        // Deserialize receivers
        Set<UUID> receivers = new HashSet<>();
        NbtList receiversList = nbt.getList("receivers", NbtElement.INT_ARRAY_TYPE);
        for (int i = 0; i < receiversList.size(); i++) {
            receivers.add(NbtHelper.toUuid(receiversList.get(i)));
        }

        // Get creator player
        ServerPlayerEntity creator = getPlayerByUuid(creatorUuid);

        // Build ShareData based on type
        ShareData.Builder builder = ShareData.builder()
            .id(id)
            .creator(creator)
            .type(type)
            .createdAt(createdAt)
            .expiresAt(expiresAt)
            .description(description)
            .receivers(receivers);

        // Deserialize content
        switch (type) {
            case ITEM:
                ItemStack item = ItemStack.fromNbt(nbt.getCompound("item"));
                builder.item(item);
                break;
            case INVENTORY:
            case HOTBAR:
            case ENDERCHEST:
                NbtList itemsList = nbt.getList("items", NbtElement.COMPOUND_TYPE);
                ItemStack[] items = new ItemStack[itemsList.size()];
                for (int i = 0; i < itemsList.size(); i++) {
                    items[i] = ItemStack.fromNbt(itemsList.getCompound(i));
                }
                builder.items(items);
                break;
            case STATS:
                PlayerStats stats = PlayerStatsSerializer.fromNBT(nbt.getCompound("stats"));
                builder.stats(stats);
                break;
            case CONTAINER:
                ContainerData container = ContainerDataSerializer.fromNBT(nbt.getCompound("container"));
                builder.container(container);
                break;
        }

        return builder.build();
    }
}

Analytics and Reporting

Data Analytics

public class ShareAnalytics {

    public AnalyticsReport generateReport(Duration period) {
        ShowcaseAPI api = ShowcaseAPI.getInstance();
        DataProvider provider = api.getDataProvider();

        Instant since = Instant.now().minus(period);
        List<ShareData> shares = getSharesSince(provider, since);

        return AnalyticsReport.builder()
            .period(period)
            .totalShares(shares.size())
            .sharesByType(groupByType(shares))
            .sharesByPlayer(groupByPlayer(shares))
            .topSharedItems(getTopSharedItems(shares))
            .averageViewsPerShare(calculateAverageViews(shares))
            .mostActiveUsers(getMostActiveUsers(shares))
            .peakSharingTimes(getPeakSharingTimes(shares))
            .build();
    }

    public Map<ShareType, Integer> groupByType(List<ShareData> shares) {
        return shares.stream()
            .collect(Collectors.groupingBy(
                ShareData::getType,
                Collectors.summingInt(share -> 1)
            ));
    }

    public Map<UUID, Integer> groupByPlayer(List<ShareData> shares) {
        return shares.stream()
            .collect(Collectors.groupingBy(
                share -> share.getCreator().getUuid(),
                Collectors.summingInt(share -> 1)
            ));
    }

    public List<String> getTopSharedItems(List<ShareData> shares) {
        return shares.stream()
            .filter(share -> share.getType() == ShareType.ITEM)
            .map(share -> share.getItem().getName().getString())
            .collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
            .entrySet().stream()
            .sorted(Map.Entry.<String, Long>comparingByValue().reversed())
            .limit(10)
            .map(Map.Entry::getKey)
            .collect(Collectors.toList());
    }

    public double calculateAverageViews(List<ShareData> shares) {
        return shares.stream()
            .mapToInt(ShareData::getViewCount)
            .average()
            .orElse(0.0);
    }
}

Performance Metrics

public class PerformanceMetrics {

    public void trackDataOperations() {
        ShowcaseAPI api = ShowcaseAPI.getInstance();
        MetricsCollector metrics = api.getMetricsCollector();

        // Track save operations
        Timer.Sample sample = Timer.start(metrics.getRegistry());
        try {
            // Perform save operation
            api.getDataProvider().saveShare(shareData);
            sample.stop(Timer.builder("showcase.data.save")
                .tag("operation", "save")
                .tag("type", shareData.getType().name())
                .register(metrics.getRegistry()));
        } catch (Exception e) {
            metrics.counter("showcase.data.errors",
                "operation", "save",
                "error", e.getClass().getSimpleName())
                .increment();
            throw e;
        }
    }

    public void generatePerformanceReport() {
        ShowcaseAPI api = ShowcaseAPI.getInstance();
        MetricsCollector metrics = api.getMetricsCollector();

        PerformanceReport report = PerformanceReport.builder()
            .averageSaveTime(getAverageTime(metrics, "showcase.data.save"))
            .averageLoadTime(getAverageTime(metrics, "showcase.data.load"))
            .errorRate(getErrorRate(metrics))
            .throughput(getThroughput(metrics))
            .cacheHitRate(getCacheHitRate(metrics))
            .build();

        System.out.println("Performance Report:");
        System.out.println("Average Save Time: " + report.getAverageSaveTime() + "ms");
        System.out.println("Average Load Time: " + report.getAverageLoadTime() + "ms");
        System.out.println("Error Rate: " + report.getErrorRate() + "%");
        System.out.println("Throughput: " + report.getThroughput() + " ops/sec");
        System.out.println("Cache Hit Rate: " + report.getCacheHitRate() + "%");
    }
}

Data Export/Import

Export Functionality

public class DataExportImport {

    public void exportAllData(Path exportPath) throws IOException {
        ShowcaseAPI api = ShowcaseAPI.getInstance();
        DataProvider provider = api.getDataProvider();

        ExportData exportData = new ExportData();

        // Export all shares
        exportData.shares = getAllShares(provider);

        // Export player statistics
        exportData.playerStats = getAllPlayerStats(provider);

        // Export configuration
        exportData.configuration = api.getConfig();

        // Export analytics data
        exportData.analytics = generateAnalyticsData();

        // Write to file
        ObjectMapper mapper = new ObjectMapper();
        mapper.writeValue(exportPath.toFile(), exportData);

        System.out.println("Data exported to: " + exportPath);
    }

    public void importData(Path importPath) throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        ExportData importData = mapper.readValue(importPath.toFile(), ExportData.class);

        ShowcaseAPI api = ShowcaseAPI.getInstance();
        DataProvider provider = api.getDataProvider();

        // Import shares
        for (ShareData share : importData.shares) {
            provider.saveShare(share);
        }

        // Import player statistics
        for (Map.Entry<UUID, PlayerStats> entry : importData.playerStats.entrySet()) {
            savePlayerStats(provider, entry.getKey(), entry.getValue());
        }

        System.out.println("Data imported from: " + importPath);
    }

    public static class ExportData {
        public List<ShareData> shares;
        public Map<UUID, PlayerStats> playerStats;
        public ShowcaseConfig configuration;
        public AnalyticsData analytics;
    }
}

Backup and Restore

public class BackupRestore {

    public void createBackup() {
        ShowcaseAPI api = ShowcaseAPI.getInstance();

        try {
            String timestamp = Instant.now().toString().replaceAll(":", "-");
            Path backupPath = Paths.get("backups", "showcase-backup-" + timestamp + ".json");

            Files.createDirectories(backupPath.getParent());

            DataExportImport exporter = new DataExportImport();
            exporter.exportAllData(backupPath);

            // Compress backup
            compressBackup(backupPath);

            System.out.println("Backup created: " + backupPath);
        } catch (IOException e) {
            System.err.println("Failed to create backup: " + e.getMessage());
        }
    }

    public void restoreBackup(Path backupPath) {
        try {
            // Decompress if needed
            if (backupPath.toString().endsWith(".gz")) {
                backupPath = decompressBackup(backupPath);
            }

            DataExportImport importer = new DataExportImport();
            importer.importData(backupPath);

            // Reload API to reflect changes
            ShowcaseAPI.getInstance().reload();

            System.out.println("Backup restored from: " + backupPath);
        } catch (IOException e) {
            System.err.println("Failed to restore backup: " + e.getMessage());
        }
    }

    public void scheduleAutomaticBackups() {
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

        // Create backup every 6 hours
        scheduler.scheduleAtFixedRate(this::createBackup, 0, 6, TimeUnit.HOURS);

        // Clean old backups every day
        scheduler.scheduleAtFixedRate(this::cleanOldBackups, 1, 24, TimeUnit.HOURS);
    }

    private void cleanOldBackups() {
        try {
            Path backupDir = Paths.get("backups");
            if (!Files.exists(backupDir)) return;

            Instant cutoff = Instant.now().minus(Duration.ofDays(7)); // Keep 7 days

            Files.list(backupDir)
                .filter(path -> path.getFileName().toString().startsWith("showcase-backup-"))
                .filter(path -> {
                    try {
                        BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
                        return attrs.creationTime().toInstant().isBefore(cutoff);
                    } catch (IOException e) {
                        return false;
                    }
                })
                .forEach(path -> {
                    try {
                        Files.delete(path);
                        System.out.println("Deleted old backup: " + path.getFileName());
                    } catch (IOException e) {
                        System.err.println("Failed to delete backup: " + path.getFileName());
                    }
                });
        } catch (IOException e) {
            System.err.println("Failed to clean old backups: " + e.getMessage());
        }
    }
}

Data Validation

Integrity Checks

public class DataIntegrityChecker {

    public ValidationResult validateDataIntegrity() {
        ShowcaseAPI api = ShowcaseAPI.getInstance();
        DataProvider provider = api.getDataProvider();

        ValidationResult result = new ValidationResult();

        // Check for orphaned data
        result.orphanedShares = findOrphanedShares(provider);

        // Check for corrupted shares
        result.corruptedShares = findCorruptedShares(provider);

        // Check for expired shares that weren't cleaned up
        result.expiredShares = findExpiredShares(provider);

        // Check data consistency
        result.inconsistencies = findDataInconsistencies(provider);

        return result;
    }

    private List<String> findOrphanedShares(DataProvider provider) {
        // Find shares that reference non-existent players
        return getAllShareIds(provider).stream()
            .filter(id -> {
                Optional<ShareData> shareOpt = provider.loadShare(id);
                if (shareOpt.isPresent()) {
                    ShareData share = shareOpt.get();
                    return getPlayerByUuid(share.getCreator().getUuid()) == null;
                }
                return false;
            })
            .collect(Collectors.toList());
    }

    private List<String> findCorruptedShares(DataProvider provider) {
        return getAllShareIds(provider).stream()
            .filter(id -> {
                try {
                    Optional<ShareData> shareOpt = provider.loadShare(id);
                    if (shareOpt.isPresent()) {
                        ShareData share = shareOpt.get();
                        return !validateShareData(share);
                    }
                    return true; // If it can't be loaded, it's corrupted
                } catch (Exception e) {
                    return true; // Exception during loading indicates corruption
                }
            })
            .collect(Collectors.toList());
    }

    private boolean validateShareData(ShareData share) {
        // Validate share has required fields
        if (share.getId() == null || share.getCreator() == null || share.getType() == null) {
            return false;
        }

        // Validate timestamps
        if (share.getCreatedAt().isAfter(share.getExpiresAt())) {
            return false;
        }

        // Validate type-specific content
        switch (share.getType()) {
            case ITEM:
                return share.getItem() != null && !share.getItem().isEmpty();
            case INVENTORY:
            case HOTBAR:
            case ENDERCHEST:
                return share.getItems() != null && share.getItems().length > 0;
            case STATS:
                return share.getStats() != null;
            case CONTAINER:
                return share.getContainer() != null;
            default:
                return true;
        }
    }

    public void repairDataIntegrity(ValidationResult result) {
        ShowcaseAPI api = ShowcaseAPI.getInstance();
        DataProvider provider = api.getDataProvider();

        // Remove orphaned shares
        for (String shareId : result.orphanedShares) {
            provider.deleteShare(shareId);
            System.out.println("Removed orphaned share: " + shareId);
        }

        // Remove corrupted shares
        for (String shareId : result.corruptedShares) {
            provider.deleteShare(shareId);
            System.out.println("Removed corrupted share: " + shareId);
        }

        // Clean up expired shares
        provider.cleanup();

        System.out.println("Data integrity repair completed");
    }

    public static class ValidationResult {
        public List<String> orphanedShares = new ArrayList<>();
        public List<String> corruptedShares = new ArrayList<>();
        public List<String> expiredShares = new ArrayList<>();
        public List<String> inconsistencies = new ArrayList<>();

        public boolean hasIssues() {
            return !orphanedShares.isEmpty() || !corruptedShares.isEmpty() ||
                   !expiredShares.isEmpty() || !inconsistencies.isEmpty();
        }
    }
}