001/*
002 * PlotSquared, a land and world management plugin for Minecraft.
003 * Copyright (C) IntellectualSites <https://intellectualsites.com>
004 * Copyright (C) IntellectualSites team and contributors
005 *
006 * This program is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 *
011 * This program is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
014 * GNU General Public License for more details.
015 *
016 * You should have received a copy of the GNU General Public License
017 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
018 */
019package com.plotsquared.core.command;
020
021import com.google.inject.Inject;
022import com.plotsquared.core.PlotSquared;
023import com.plotsquared.core.configuration.caption.StaticCaption;
024import com.plotsquared.core.configuration.caption.TranslatableCaption;
025import com.plotsquared.core.configuration.file.YamlConfiguration;
026import com.plotsquared.core.database.DBFunc;
027import com.plotsquared.core.database.Database;
028import com.plotsquared.core.database.MySQL;
029import com.plotsquared.core.database.SQLManager;
030import com.plotsquared.core.database.SQLite;
031import com.plotsquared.core.inject.annotations.WorldConfig;
032import com.plotsquared.core.listener.PlotListener;
033import com.plotsquared.core.player.PlotPlayer;
034import com.plotsquared.core.plot.Plot;
035import com.plotsquared.core.plot.PlotArea;
036import com.plotsquared.core.plot.PlotId;
037import com.plotsquared.core.plot.world.PlotAreaManager;
038import com.plotsquared.core.plot.world.SinglePlotArea;
039import com.plotsquared.core.util.EventDispatcher;
040import com.plotsquared.core.util.FileUtils;
041import com.plotsquared.core.util.WorldUtil;
042import com.plotsquared.core.util.query.PlotQuery;
043import com.plotsquared.core.util.task.TaskManager;
044import net.kyori.adventure.text.Component;
045import net.kyori.adventure.text.minimessage.tag.Tag;
046import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
047import org.apache.logging.log4j.LogManager;
048import org.apache.logging.log4j.Logger;
049import org.checkerframework.checker.nullness.qual.NonNull;
050
051import java.io.File;
052import java.io.IOException;
053import java.nio.file.Files;
054import java.nio.file.Path;
055import java.sql.SQLException;
056import java.util.ArrayList;
057import java.util.Arrays;
058import java.util.HashMap;
059import java.util.List;
060import java.util.Map.Entry;
061
062@CommandDeclaration(command = "database",
063        aliases = {"convert"},
064        category = CommandCategory.ADMINISTRATION,
065        permission = "plots.database",
066        requiredType = RequiredType.CONSOLE,
067        usage = "/plot database [area] <sqlite | mysql | import>")
068public class DatabaseCommand extends SubCommand {
069
070    private static final Logger LOGGER = LogManager.getLogger("PlotSquared/" + DatabaseCommand.class.getSimpleName());
071
072    private final PlotAreaManager plotAreaManager;
073    private final EventDispatcher eventDispatcher;
074    private final PlotListener plotListener;
075    private final YamlConfiguration worldConfiguration;
076
077    @Inject
078    public DatabaseCommand(
079            final @NonNull PlotAreaManager plotAreaManager,
080            final @NonNull EventDispatcher eventDispatcher,
081            final @NonNull PlotListener plotListener,
082            @WorldConfig final @NonNull YamlConfiguration worldConfiguration
083    ) {
084        this.plotAreaManager = plotAreaManager;
085        this.eventDispatcher = eventDispatcher;
086        this.plotListener = plotListener;
087        this.worldConfiguration = worldConfiguration;
088    }
089
090    public static void insertPlots(
091            final SQLManager manager, final List<Plot> plots,
092            final PlotPlayer<?> player
093    ) {
094        TaskManager.runTaskAsync(() -> {
095            try {
096                ArrayList<Plot> ps = new ArrayList<>(plots);
097                player.sendMessage(TranslatableCaption.of("database.starting_conversion"));
098                manager.createPlotsAndData(ps, () -> {
099                    player.sendMessage(TranslatableCaption.of("database.conversion_done"));
100                    manager.close();
101                });
102            } catch (Exception e) {
103                player.sendMessage(TranslatableCaption.of("database.conversion_failed"));
104                LOGGER.error("Database conversion failed", e);
105            }
106        });
107    }
108
109    @Override
110    public boolean onCommand(final PlotPlayer<?> player, String[] args) {
111        if (args.length < 1) {
112            player.sendMessage(
113                    TranslatableCaption.of("commandconfig.command_syntax"),
114                    TagResolver.resolver(
115                            "value",
116                            Tag.inserting(Component.text("/plot database [area] <sqlite | mysql | import>"))
117                    )
118            );
119            return false;
120        }
121        List<Plot> plots;
122        PlotArea area = this.plotAreaManager.getPlotAreaByString(args[0]);
123        if (area != null) {
124            plots = PlotSquared.get().sortPlotsByTemp(area.getPlots());
125            args = Arrays.copyOfRange(args, 1, args.length);
126        } else {
127            plots = PlotSquared.get().sortPlotsByTemp(PlotQuery.newQuery().allPlots().asList());
128        }
129        if (args.length < 1) {
130            player.sendMessage(
131                    TranslatableCaption.of("commandconfig.command_syntax"),
132                    TagResolver.resolver("value", Tag.inserting(Component.text("/plot database [area] <sqlite|mysql|import>")))
133            );
134            player.sendMessage(TranslatableCaption.of("database.arg"));
135            return false;
136        }
137        try {
138            Database implementation;
139            String prefix = "";
140            switch (args[0].toLowerCase()) {
141                case "import" -> {
142                    if (args.length < 2) {
143                        player.sendMessage(
144                                TranslatableCaption.of("commandconfig.command_syntax"),
145                                TagResolver.resolver(
146                                        "value",
147                                        Tag.inserting(Component.text("/plot database import <sqlite file> [prefix]"))
148                                )
149                        );
150                        return false;
151                    }
152                    File file = FileUtils.getFile(
153                            PlotSquared.platform().getDirectory(),
154                            args[1].endsWith(".db") ? args[1] : args[1] + ".db"
155                    );
156                    if (!file.exists()) {
157                        player.sendMessage(
158                                TranslatableCaption.of("database.does_not_exist"),
159                                TagResolver.resolver("value", Tag.inserting(Component.text(file.toString())))
160                        );
161                        return false;
162                    }
163                    player.sendMessage(TranslatableCaption.of("database.starting_conversion"));
164                    implementation = new SQLite(file);
165                    SQLManager manager = new SQLManager(implementation, args.length == 3 ? args[2] : "",
166                            this.eventDispatcher, this.plotListener, this.worldConfiguration
167                    );
168                    HashMap<String, HashMap<PlotId, Plot>> map = manager.getPlots();
169                    Path worldContainer = PlotSquared.platform().worldContainer().toPath();
170                    plots = new ArrayList<>();
171                    for (Entry<String, HashMap<PlotId, Plot>> entry : map.entrySet()) {
172                        String areaName = entry.getKey();
173                        PlotArea pa = this.plotAreaManager.getPlotAreaByString(areaName);
174                        if (pa != null) {
175                            for (Entry<PlotId, Plot> entry2 : entry.getValue().entrySet()) {
176                                Plot plot = entry2.getValue();
177                                if (pa.getOwnedPlotAbs(plot.getId()) != null) {
178                                    if (pa instanceof SinglePlotArea) {
179                                        Plot newPlot = pa.getNextFreePlot(null, plot.getId());
180                                        if (newPlot != null) {
181                                            PlotId newId = newPlot.getId();
182                                            PlotId id = plot.getId();
183                                            Path worldPath = (WorldUtil.isModernServerLevelStructure() ?
184                                                    worldContainer.resolve("dimensions").resolve("minecraft") :
185                                                    worldContainer
186                                            ).resolve(id.toCommaSeparatedString());
187                                            if (Files.exists(worldPath)) {
188                                                Path newPath = (WorldUtil.isModernServerLevelStructure() ?
189                                                        worldContainer.resolve("dimensions").resolve("minecraft") :
190                                                        worldContainer
191                                                ).resolve(newId.toCommaSeparatedString());
192                                                try {
193                                                    Files.move(worldPath, newPath);
194                                                } catch (IOException e) {
195                                                    LOGGER.error("Failed to rename world entry", e);
196                                                }
197                                            }
198                                            plot.setId(newId);
199                                            plot.setArea(pa);
200                                            plots.add(plot);
201                                            continue;
202                                        }
203                                    }
204                                    player.sendMessage(
205                                            TranslatableCaption.of("database.skipping_duplicated_plot"),
206                                            TagResolver.builder()
207                                                    .tag("plot", Tag.inserting(Component.text(plot.toString())))
208                                                    .tag("id", Tag.inserting(Component.text(plot.temp)))
209                                                    .build()
210                                    );
211                                    continue;
212                                }
213                                plot.setArea(pa);
214                                plots.add(plot);
215                            }
216                        } else {
217                            HashMap<PlotId, Plot> plotMap = PlotSquared.get().plots_tmp
218                                    .computeIfAbsent(areaName, k -> new HashMap<>());
219                            plotMap.putAll(entry.getValue());
220                        }
221                    }
222                    DBFunc.createPlotsAndData(
223                            plots,
224                            () -> player.sendMessage(TranslatableCaption.of("database.conversion_done"))
225                    );
226                    return true;
227                }
228                case "mysql" -> {
229                    if (args.length < 6) {
230                        player.sendMessage(StaticCaption.of(
231                                "/plot database mysql [host] [port] [username] [password] [database] {prefix}"));
232                        return false;
233                    }
234                    String host = args[1];
235                    String port = args[2];
236                    String username = args[3];
237                    String password = args[4];
238                    String database = args[5];
239                    if (args.length > 6) {
240                        prefix = args[6];
241                    }
242                    implementation = new MySQL(host, port, database, username, password);
243                }
244                case "sqlite" -> {
245                    if (args.length < 2) {
246                        player.sendMessage(StaticCaption.of("/plot database sqlite [file]"));
247                        return false;
248                    }
249                    File sqliteFile =
250                            FileUtils.getFile(PlotSquared.platform().getDirectory(), args[1] + ".db");
251                    implementation = new SQLite(sqliteFile);
252                }
253                default -> {
254                    player.sendMessage(StaticCaption.of("/plot database [sqlite/mysql]"));
255                    return false;
256                }
257            }
258            try {
259                SQLManager manager = new SQLManager(
260                        implementation,
261                        prefix,
262                        this.eventDispatcher,
263                        this.plotListener,
264                        this.worldConfiguration
265                );
266                DatabaseCommand.insertPlots(manager, plots, player);
267                return true;
268            } catch (ClassNotFoundException | SQLException e) {
269                player.sendMessage(TranslatableCaption.of("database.failed_to_save_plots"));
270                player.sendMessage(TranslatableCaption.of("errors.stacktrace_begin"));
271                LOGGER.error("Inserting plots failed", e);
272                player.sendMessage(TranslatableCaption.of("errors.stacktrace_end"));
273                player.sendMessage(TranslatableCaption.of("database.invalid_args"));
274                return false;
275            }
276        } catch (ClassNotFoundException | SQLException e) {
277            player.sendMessage(TranslatableCaption.of("database.failed_to_open"));
278            player.sendMessage(TranslatableCaption.of("errors.stacktrace_begin"));
279            LOGGER.error("Opening database connection failed", e);
280            player.sendMessage(TranslatableCaption.of("errors.stacktrace_end"));
281            player.sendMessage(TranslatableCaption.of("database.invalid_args"));
282            return false;
283        }
284    }
285
286}