1

feat: первая рабочая версия

This commit is contained in:
2025-02-26 19:45:01 +03:00
parent 21412346c7
commit 93dfa59d40
26 changed files with 1218 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
package ru.di9.jdbc;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class AbstractEntity<T> {
protected T id;
}

View File

@@ -0,0 +1,19 @@
package ru.di9.jdbc;
import lombok.Getter;
@Getter
public class DataAccessException extends RuntimeException {
@SuppressWarnings("java:S1165")
private String sql;
public DataAccessException(String msg, Throwable cause) {
super(msg, cause);
}
public DataAccessException(String msg, String sql, Throwable cause) {
this(msg + " | " + sql, cause);
this.sql = sql;
}
}

View File

@@ -0,0 +1,36 @@
package ru.di9.jdbc;
import org.intellij.lang.annotations.Language;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
public interface JdbcTemplate {
void execute(@Language("GenericSQL") String sql) throws DataAccessException;
void execute(@Language("GenericSQL") String sql, PreparedStatementProcessor psp) throws DataAccessException;
<T> T query(@Language("GenericSQL") String sql, ResultSetExtractor<T> rse) throws DataAccessException;
<T> T query(@Language("GenericSQL") String sql, PreparedStatementProcessor psp, ResultSetExtractor<T> rse)
throws DataAccessException;
<T> Optional<T> queryOne(@Language("GenericSQL") String sql, ResultSetExtractor<T> rse)
throws DataAccessException;
<T> Optional<T> queryOne(@Language("GenericSQL") String sql, PreparedStatementProcessor psp,
ResultSetExtractor<T> rse) throws DataAccessException;
<T> List<T> queryList(@Language("GenericSQL") String sql,
final RowMapper<T> rowMapper) throws DataAccessException;
<T> List<T> queryList(@Language("GenericSQL") String sql, PreparedStatementProcessor psp,
final RowMapper<T> rowMapper) throws DataAccessException;
<T> T insert(@Language("GenericSQL") String sql, PreparedStatementProcessor psp,
ResultSetExtractor<T> processGeneratedKey) throws DataAccessException;
void transaction(Consumer<JdbcTemplate> consumer);
}

View File

@@ -0,0 +1,169 @@
package ru.di9.jdbc;
import org.intellij.lang.annotations.Language;
import javax.sql.DataSource;
import java.sql.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
public class JdbcTemplateImpl implements JdbcTemplate {
private final DataSource dataSource;
public JdbcTemplateImpl(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public void execute(@Language("GenericSQL") String sql) throws DataAccessException {
try (Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement()) {
statement.execute(sql);
} catch (SQLException e) {
throw throwDataAccessException(sql, e);
}
}
@Override
public void execute(@Language("GenericSQL") String sql, PreparedStatementProcessor psp) throws DataAccessException {
try (Connection connection = dataSource.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
psp.process(preparedStatement);
preparedStatement.execute();
} catch (SQLException e) {
throw throwDataAccessException(sql, e);
}
}
@Override
public <T> T query(@Language("GenericSQL") String sql, ResultSetExtractor<T> rse) throws DataAccessException {
try (Connection connection = dataSource.getConnection();
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(sql)) {
return rse.extractData(resultSet);
} catch (SQLException e) {
throw throwDataAccessException(sql, e);
}
}
@Override
public <T> T query(@Language("GenericSQL") String sql, PreparedStatementProcessor psp,
ResultSetExtractor<T> rse) throws DataAccessException {
try (Connection connection = dataSource.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
psp.process(preparedStatement);
try (ResultSet resultSet = preparedStatement.executeQuery()) {
return rse.extractData(resultSet);
}
} catch (SQLException e) {
throw throwDataAccessException(sql, e);
}
}
@Override
public <T> Optional<T> queryOne(@Language("GenericSQL") String sql,
ResultSetExtractor<T> rse) throws DataAccessException {
return query(sql, rs -> {
if (rs.next()) {
return Optional.ofNullable(rse.extractData(rs));
} else {
return Optional.empty();
}
});
}
@Override
public <T> Optional<T> queryOne(@Language("GenericSQL") String sql, PreparedStatementProcessor psp,
ResultSetExtractor<T> rse) throws DataAccessException {
return query(sql, psp, rs -> {
if (rs.next()) {
return Optional.ofNullable(rse.extractData(rs));
} else {
return Optional.empty();
}
});
}
@Override
public <T> List<T> queryList(@Language("GenericSQL") String sql,
final RowMapper<T> rowMapper) throws DataAccessException {
return query(sql, createResultSetExtractorList(rowMapper));
}
@Override
public <T> List<T> queryList(@Language("GenericSQL") String sql, PreparedStatementProcessor psp,
final RowMapper<T> rowMapper) throws DataAccessException {
return query(sql, psp, createResultSetExtractorList(rowMapper));
}
@Override
public <T> T insert(@Language("GenericSQL") String sql, PreparedStatementProcessor psp,
ResultSetExtractor<T> processGeneratedKey) throws DataAccessException {
try (Connection connection = dataSource.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
psp.process(preparedStatement);
preparedStatement.execute();
try (ResultSet generatedKeys = preparedStatement.getGeneratedKeys()) {
generatedKeys.next();
return processGeneratedKey.extractData(generatedKeys);
}
} catch (SQLException e) {
throw throwDataAccessException(sql, e);
}
}
@Override
public void transaction(Consumer<JdbcTemplate> consumer) {
Connection connection = null;
try {
connection = dataSource.getConnection();
connection.createStatement().execute("BEGIN TRANSACTION");
consumer.accept(new JdbcTemplateTransactional(connection));
connection.createStatement().execute("COMMIT");
} catch (SQLException e) {
if (connection != null) {
try {
connection.createStatement().execute("ROLLBACK");
throw new DataAccessException("Error transaction", e);
} catch (SQLException e1) {
DataAccessException exception = new DataAccessException("Error rollback", e1);
exception.addSuppressed(e1);
throw exception;
}
}
} finally {
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
throw new DataAccessException("Error close connection", e);
}
}
}
}
private <T> ResultSetExtractor<List<T>> createResultSetExtractorList(final RowMapper<T> rowMapper) {
return rs -> {
List<T> resultList;
int rowNum = 0;
if (rs.next()) {
resultList = new ArrayList<>();
do {
resultList.add(rowMapper.mapRow(rs, rowNum++));
} while (rs.next());
} else {
resultList = Collections.emptyList();
}
return resultList;
};
}
static DataAccessException throwDataAccessException(String sql, Exception e) {
return new DataAccessException("Error execute SQL", sql, e);
}
}

View File

@@ -0,0 +1,132 @@
package ru.di9.jdbc;
import lombok.RequiredArgsConstructor;
import java.sql.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import static ru.di9.jdbc.JdbcTemplateImpl.throwDataAccessException;
@RequiredArgsConstructor
public class JdbcTemplateTransactional implements JdbcTemplate, AutoCloseable {
private final Connection connection;
@Override
public void close() throws Exception {
connection.close();
}
@Override
public void execute(String sql) throws DataAccessException {
try (Statement statement = connection.createStatement()) {
statement.execute(sql);
} catch (SQLException e) {
throw throwDataAccessException(sql, e);
}
}
@Override
public void execute(String sql, PreparedStatementProcessor psp) throws DataAccessException {
try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
psp.process(preparedStatement);
preparedStatement.execute();
} catch (SQLException e) {
throw throwDataAccessException(sql, e);
}
}
@Override
public <T> T query(String sql, ResultSetExtractor<T> rse) throws DataAccessException {
try (Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(sql)) {
return rse.extractData(resultSet);
} catch (SQLException e) {
throw throwDataAccessException(sql, e);
}
}
@Override
public <T> T query(String sql, PreparedStatementProcessor psp, ResultSetExtractor<T> rse) throws DataAccessException {
try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
psp.process(preparedStatement);
try (ResultSet resultSet = preparedStatement.executeQuery()) {
return rse.extractData(resultSet);
}
} catch (SQLException e) {
throw throwDataAccessException(sql, e);
}
}
@Override
public <T> Optional<T> queryOne(String sql, ResultSetExtractor<T> rse) throws DataAccessException {
return query(sql, rs -> {
if (rs.next()) {
return Optional.ofNullable(rse.extractData(rs));
} else {
return Optional.empty();
}
});
}
@Override
public <T> Optional<T> queryOne(String sql, PreparedStatementProcessor psp, ResultSetExtractor<T> rse) throws DataAccessException {
return query(sql, psp, rs -> {
if (rs.next()) {
return Optional.ofNullable(rse.extractData(rs));
} else {
return Optional.empty();
}
});
}
@Override
public <T> List<T> queryList(String sql, RowMapper<T> rowMapper) throws DataAccessException {
return query(sql, createResultSetExtractorList(rowMapper));
}
@Override
public <T> List<T> queryList(String sql, PreparedStatementProcessor psp, RowMapper<T> rowMapper) throws DataAccessException {
return query(sql, psp, createResultSetExtractorList(rowMapper));
}
@Override
public <T> T insert(String sql, PreparedStatementProcessor psp, ResultSetExtractor<T> processGeneratedKey) throws DataAccessException {
try (PreparedStatement preparedStatement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
psp.process(preparedStatement);
preparedStatement.execute();
try (ResultSet generatedKeys = preparedStatement.getGeneratedKeys()) {
generatedKeys.next();
return processGeneratedKey.extractData(generatedKeys);
}
} catch (SQLException e) {
throw throwDataAccessException(sql, e);
}
}
@Override
public void transaction(Consumer<JdbcTemplate> consumer) {
consumer.accept(this);
}
private <T> ResultSetExtractor<List<T>> createResultSetExtractorList(final RowMapper<T> rowMapper) {
return rs -> {
List<T> resultList;
int rowNum = 0;
if (rs.next()) {
resultList = new ArrayList<>();
do {
resultList.add(rowMapper.mapRow(rs, rowNum++));
} while (rs.next());
} else {
resultList = Collections.emptyList();
}
return resultList;
};
}
}

View File

@@ -0,0 +1,10 @@
package ru.di9.jdbc;
import java.sql.PreparedStatement;
import java.sql.SQLException;
@FunctionalInterface
public interface PreparedStatementProcessor {
void process(PreparedStatement ps) throws SQLException;
}

View File

@@ -0,0 +1,10 @@
package ru.di9.jdbc;
import java.sql.ResultSet;
import java.sql.SQLException;
@FunctionalInterface
public interface ResultSetExtractor<T> {
T extractData(ResultSet rs) throws SQLException, DataAccessException;
}

View File

@@ -0,0 +1,10 @@
package ru.di9.jdbc;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ReturnGeneratedKeyHelper {
public static final ResultSetExtractor<Long> RETURN_GENERATED_KEY_FIRST_LONG = rs -> rs.getLong(1);
public static final ResultSetExtractor<Integer> RETURN_GENERATED_KEY_FIRST_INT = rs -> rs.getInt(1);
}

View File

@@ -0,0 +1,9 @@
package ru.di9.jdbc;
import java.sql.ResultSet;
import java.sql.SQLException;
public interface RowMapper<T> {
T mapRow(ResultSet rs, int rowNum) throws SQLException;
}

View File

@@ -0,0 +1,100 @@
package ru.di9.ss14.extractor;
import com.github.luben.zstd.ZstdInputStream;
import org.sqlite.SQLiteDataSource;
import ru.di9.jdbc.JdbcTemplate;
import ru.di9.jdbc.JdbcTemplateImpl;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
public class ContentDbManager {
private final JdbcTemplate jdbcTemplate;
public ContentDbManager(String pathToDb) {
var dataSource = new SQLiteDataSource();
dataSource.setUrl("jdbc:sqlite:%s".formatted(pathToDb));
this.jdbcTemplate = new JdbcTemplateImpl(dataSource);
}
public List<String> getForkVersions() {
return jdbcTemplate.queryList(
"SELECT ForkVersion FROM ContentVersion",
(rs, rowNum) -> rs.getString(1).toUpperCase());
}
public ContentRec getPaths(String forkVersion) {
ContentRec root = new ContentRec(forkVersion);
jdbcTemplate.query("""
select cm.ContentId, cm.Path, c.Compression
from ContentManifest cm
inner join ContentVersion cv on cv.Id = cm.VersionId
inner join Content c on c.Id = cm.ContentId
where cv.ForkVersion like ?
order by cm.Path;
""",
ps -> ps.setString(1, forkVersion),
rs -> {
while (rs.next()) {
int contentId = rs.getInt(1);
String path = rs.getString(2);
boolean compressed = rs.getInt(3) > 0;
parseLine(contentId, path, compressed, root);
}
return null;
});
return root;
}
public void readContent(int contentId, OutputStream outputStream) {
jdbcTemplate.query("""
select Data, Compression
from Content
where Id = ?;
""",
ps -> ps.setInt(1, contentId),
rs -> {
rs.next();
boolean compressed = rs.getInt(2) > 0;
try(InputStream binaryStream = rs.getBinaryStream(1)) {
if (compressed) {
var zstdInputStream = new ZstdInputStream(binaryStream);
zstdInputStream.transferTo(outputStream);
} else {
binaryStream.transferTo(outputStream);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return null;
});
}
private void parseLine(int contentId, String path, boolean compressed, ContentRec contentRec) {
var idx = path.indexOf('/');
if (idx == -1) {
contentRec.getChildren().add(new ContentRec(path, contentId, compressed));
} else {
String folder = path.substring(0, idx);
var rec = contentRec.getChildren()
.stream()
.filter(r -> r.getName().equals(folder))
.findFirst()
.orElseGet(() -> {
var r = new ContentRec(folder);
contentRec.getChildren().add(r);
return r;
});
parseLine(contentId, path.substring(idx + 1), compressed, rec);
}
}
}

View File

@@ -0,0 +1,42 @@
package ru.di9.ss14.extractor;
import lombok.Getter;
import org.jetbrains.annotations.NotNull;
import java.util.SortedSet;
import java.util.TreeSet;
@Getter
public final class ContentRec implements Comparable<ContentRec> {
private final String name;
private final Integer id;
private final Boolean compressed;
private SortedSet<ContentRec> children;
public ContentRec(String folderName) {
this(folderName, null, null);
}
public ContentRec(String fileName, Integer id, Boolean compressed) {
this.name = fileName;
this.id = id;
this.compressed = compressed;
if (isFolder()) {
this.children = new TreeSet<>();
}
}
public boolean isFolder() {
return id == null;
}
@Override
public int compareTo(@NotNull ContentRec rec) {
if (this.isFolder() == rec.isFolder()) {
return this.getName().compareTo(rec.getName());
} else {
return this.isFolder() ? -1 : 1;
}
}
}

View File

@@ -0,0 +1,9 @@
package ru.di9.ss14.extractor;
import ru.di9.ss14.extractor.gui.AppFX;
public final class Launcher {
public static void main(String[] args) {
AppFX.main(args);
}
}

View File

@@ -0,0 +1,29 @@
package ru.di9.ss14.extractor.gui;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.util.Objects;
public class AppFX extends Application {
@Override
public void start(Stage stage) throws Exception {
var loader = new FXMLLoader(Objects.requireNonNull(getClass().getResource("/view/main.fxml")));
Parent root = loader.load();
var controller = (MainController)loader.getController();
controller.setStage(stage);
stage.setTitle("SS14: Content Extractor");
stage.setScene(new Scene(root, 640.0, 480.0));
stage.setResizable(true);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}

View File

@@ -0,0 +1,145 @@
package ru.di9.ss14.extractor.gui;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import lombok.Setter;
import ru.di9.ss14.extractor.ContentDbManager;
import ru.di9.ss14.extractor.ContentRec;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.SortedSet;
public class MainController implements Initializable {
private static final int STATE_NOT_LOADED = 0;
private static final int STATE_LOAD_IN_PROGRESS = 1;
private static final int STATE_LOADED = 2;
@FXML
public TreeView<String> treeView;
private final Map<TreeItem<String>, Integer> mapForkLoaded = new HashMap<>();
@Setter
private Stage stage;
private TreeItem<String> rootItem;
private ContentDbManager manager;
private File lastSaveDir;
@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
rootItem = new TreeItem<>("(root)");
treeView.setRoot(rootItem);
treeView.setShowRoot(false);
treeView.setCellFactory(ignore -> new TreeCellExt<>());
}
public void onClickOpenMenu() {
var fileChooser = new FileChooser();
fileChooser.setTitle("Открыть content.db");
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("content.db", "content.db"));
File file = fileChooser.showOpenDialog(stage);
manager = new ContentDbManager(file.getAbsolutePath());
rootItem.getChildren().clear();
mapForkLoaded.clear();
manager.getForkVersions().forEach(forkVersion -> {
var forkVersionItem = new TreeItem<>("\uD83D\uDCE6 " + forkVersion);
forkVersionItem.getChildren().add(new TreeItem<>("(загрузка...)"));
forkVersionItem.expandedProperty().addListener((observable, oldValue, newValue) -> {
if (!newValue || mapForkLoaded.get(forkVersionItem) != STATE_NOT_LOADED) {
return;
}
mapForkLoaded.put(forkVersionItem, STATE_LOAD_IN_PROGRESS);
var task = new Task<ContentRec>() {
@Override
protected ContentRec call() {
return manager.getPaths(forkVersion);
}
};
task.setOnSucceeded(event -> {
forkVersionItem.setExpanded(false);
forkVersionItem.getChildren().clear();
SortedSet<ContentRec> paths = ((ContentRec) event.getSource().getValue()).getChildren();
createTreeItems(forkVersionItem, paths);
mapForkLoaded.put(forkVersionItem, STATE_LOADED);
forkVersionItem.setExpanded(true);
});
var thread = new Thread(task);
thread.setDaemon(true);
thread.start();
});
mapForkLoaded.put(forkVersionItem, STATE_NOT_LOADED);
rootItem.getChildren().add(forkVersionItem);
});
}
private void createTreeItems(TreeItem<String> parentItem, SortedSet<ContentRec> sortedSet) {
sortedSet.forEach(contentRec -> {
var fileItem = new TreeItemExt<>(
(contentRec.isFolder()
? "\uD83D\uDCC2 "
: "\uD83D\uDCC4 ")
+ contentRec.getName());
fileItem.setContentRec(contentRec);
parentItem.getChildren().add(fileItem);
if (contentRec.isFolder()) {
createTreeItems(fileItem, contentRec.getChildren());
} else {
fileItem.setContextMenuBuilder(() -> createContextMenu(contentRec));
}
});
}
private ContextMenu createContextMenu(ContentRec contentRec) {
var menuItem = new MenuItem("\uD83D\uDCBE Сохранить %s в...".formatted(contentRec.getName()));
menuItem.setOnAction(event -> {
var fileChooser = new FileChooser();
fileChooser.setTitle("Сохранить " + contentRec.getName());
fileChooser.setInitialFileName(contentRec.getName());
if (lastSaveDir != null) {
fileChooser.setInitialDirectory(lastSaveDir);
}
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Все файлы (*.*)", "*.*"));
File file = fileChooser.showSaveDialog(stage);
if (file == null) {
return;
}
try (var out = new FileOutputStream(file)) {
manager.readContent(contentRec.getId(), out);
} catch (IOException e) {
throw new RuntimeException(e);
}
lastSaveDir = file.getParentFile();
});
var contextMenu = new ContextMenu();
contextMenu.getItems().add(menuItem);
return contextMenu;
}
}

View File

@@ -0,0 +1,24 @@
package ru.di9.ss14.extractor.gui;
import javafx.scene.control.TreeCell;
public class TreeCellExt<T> extends TreeCell<T> {
@Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
setText(getItem() == null ? "" : getItem().toString());
var treeItem = getTreeItem();
setGraphic(treeItem.getGraphic());
if (treeItem instanceof TreeItemExt<T> treeItemExt) {
setContextMenu(treeItemExt.getContextMenu());
}
}
}
}

View File

@@ -0,0 +1,32 @@
package ru.di9.ss14.extractor.gui;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.TreeItem;
import lombok.Getter;
import lombok.Setter;
import ru.di9.ss14.extractor.ContentRec;
import java.util.function.Supplier;
public class TreeItemExt<T> extends TreeItem<T> {
@Getter
@Setter
private ContentRec contentRec;
private ContextMenu contextMenu;
@Setter
private Supplier<ContextMenu> contextMenuBuilder;
public TreeItemExt(T t) {
super(t);
}
public ContextMenu getContextMenu() {
if (contextMenu == null && contextMenuBuilder != null) {
contextMenu = contextMenuBuilder.get();
}
return contextMenu;
}
}