Issues (44)

java/fr/arakne/swfmaploader/swf/SwfFileLoader.java (14 issues)

1
/*
2
 * This file is part of Swf Map Loader.
3
 *
4
 * Swf Map Loader is free software: you can redistribute it and/or modify
5
 * it under the terms of the GNU Lesser General Public License as published by
6
 * the Free Software Foundation, either version 3 of the License, or
7
 * (at your option) any later version.
8
 *
9
 * Swf Map Loader is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 * GNU Lesser General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU Lesser General Public License
15
 * along with Swf Map Loader.  If not, see <https://www.gnu.org/licenses/>.
16
 *
17
 * Copyright (c) 2020-2020 Vincent Quatrevieux
18
 */
19
20
package fr.arakne.swfmaploader.swf;
21
22
import com.jpexs.decompiler.flash.AbortRetryIgnoreHandler;
23
import com.jpexs.decompiler.flash.SWF;
24
import com.jpexs.decompiler.flash.exporters.modes.ScriptExportMode;
25
import com.jpexs.decompiler.flash.exporters.settings.ScriptExportSettings;
26
import org.apache.commons.lang3.StringUtils;
27
28
import java.io.*;
0 ignored issues
show
Comprehensibility introduced by
Instead of using wildcard imports, consider only importing the actual classes needed.
Loading history...
29
import java.net.URL;
30
import java.nio.file.*;
0 ignored issues
show
Comprehensibility introduced by
Instead of using wildcard imports, consider only importing the actual classes needed.
Loading history...
31
import java.nio.file.attribute.BasicFileAttributes;
32
import java.util.HashMap;
33
import java.util.List;
34
import java.util.Map;
35
36
/**
37
 * Load and parse swf map file
38
 *
39
 * Note: the load method is synchronized, so it not scale on multithreading application.
40
 */
41
final public class SwfFileLoader {
42
    @FunctionalInterface
43
    interface Setter<T> {
0 ignored issues
show
Drop this interface in favor of "java.util.function.BiConsumer<SwfMapStructure,T>".
Loading history...
44
        public void set(SwfMapStructure map, T value);
0 ignored issues
show
The modifier "public" is redundant in this context.
Loading history...
45
    }
46
47
    final private String baseUrl;
48
    final private String tempDir;
49
50 1
    final private Map<String, Setter<String>> fields = new HashMap<>();
51
52
    /**
53
     * @param baseUrl The base URL to load map data (must contains the protocol, host and path to maps directory)
54
     * @param tempDir Directory to use for store decompiled action script files
55
     */
56 1
    public SwfFileLoader(String baseUrl, String tempDir) {
57 1
        this.baseUrl = baseUrl;
58 1
        this.tempDir = tempDir;
59
60 1
        declareFields();
61 1
    }
62
63
    /**
64
     * Load a map file from CDN
65
     *
66
     * @param id The map id
67
     * @param version The map version string
68
     * @param key The encryption key
69
     *
70
     * @return The loaded map structure
71
     *
72
     * @throws IOException When cannot load swf file
73
     * @throws InterruptedException When an interruption occurs during loading the swf file
74
     * @throws IllegalArgumentException When the swf file do not contains a valid map structure
75
     */
76
    public SwfMapStructure load(int id, String version, String key) throws IOException, InterruptedException {
0 ignored issues
show
Refactor this method to throw at most one checked exception instead of: java.io.IOException, java.lang.InterruptedException
Loading history...
77 1
        SwfMapStructure structure = load(new URL(baseUrl + "/" + id + "_" + version + (key == null || key.isEmpty() ? "" : "X") + ".swf"));
78
79 1
        structure.setVersion(version);
80
81 1
        if (structure.id() != id) {
82 1
            throw new IllegalArgumentException("Invalid loaded map id : expected " + id + " but get " + structure.id());
83
        }
84
85 1
        return structure;
86
    }
87
88
    /**
89
     * Load a map file from an URL
90
     *
91
     * @param mapFileUrl The file URL
92
     *
93
     * @return The loaded map structure
94
     *
95
     * @throws IOException When cannot load swf file
96
     * @throws InterruptedException When an interruption occurs during loading the swf file
97
     * @throws IllegalArgumentException When the swf file do not contains a valid map structure
98
     */
99
    synchronized public SwfMapStructure load(URL mapFileUrl) throws IOException, InterruptedException {
0 ignored issues
show
Refactor this method to throw at most one checked exception instead of: java.io.IOException, java.lang.InterruptedException
Loading history...
100 1
        File outdir = new File(tempDir + File.separator + StringUtils.substringAfterLast(mapFileUrl.getPath(), "/"));
101 1
        outdir.mkdirs();
102
103 1
        try (InputStream stream = mapFileUrl.openStream()) {
104 1
            SWF swf = new SWF(stream, false);
105
106 1
            List<File> sources = swf.exportActionScript(
107 1
                new AbortRetryIgnoreHandler() {
108
                    @Override
109
                    public int handle(Throwable throwable) {
110
                        return AbortRetryIgnoreHandler.ABORT;
111
                    }
112
113
                    @Override
114
                    public AbortRetryIgnoreHandler getNewInstance() {
115
                        return this;
116
                    }
117
                },
118 1
                outdir.getAbsolutePath(),
119
                new ScriptExportSettings(ScriptExportMode.AS, false),
120
                false,
121
                null
122
            );
123
124 1
            if (sources.size() != 1 || !"DoAction.as".equalsIgnoreCase(sources.get(0).getName())) {
125 1
                throw new IllegalArgumentException("Invalid SWF file : missing the ActionScript file");
126
            }
127
128 1
            return parseActionScript(new FileInputStream(sources.get(0)));
129
        } finally {
130 1
            clearTemp(outdir);
131
        }
132
    }
133
134
    /**
135
     * Parse an action script file
136
     *
137
     * @param stream The file stream
138
     *
139
     * @return The map structure
140
     *
141
     * @throws IOException When cannot load swf file
142
     * @throws IllegalArgumentException When the swf file do not contains a valid map structure
143
     */
144
    public SwfMapStructure parseActionScript(InputStream stream) throws IOException {
145 1
        SwfMapStructure structure = new SwfMapStructure();
146
147 1
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) {
0 ignored issues
show
Classes and methods like InputStreamReader(InputStream) that rely on the default system encoding should not be used.
Loading history...
148 1
            for (String line = reader.readLine(); line != null; line = reader.readLine()) {
149 1
                parseLine(structure, line);
150
            }
151
        }
152
153 1
        if (!structure.valid()) {
154 1
            throw new IllegalArgumentException("Invalid data");
155
        }
156
157 1
        return structure;
158
    }
159
160
    /**
161
     * Parse a single action script line
162
     *
163
     * @param structure The map structure to fill
164
     * @param line The line to parse
165
     */
166
    private void parseLine(SwfMapStructure structure, String line) {
167 1
        String[] parts = StringUtils.split(line, "=", 2);
0 ignored issues
show
Comprehensibility introduced by
Consider assigning this magic number 2 to a constant.

Using constants for hard-coded numbers is a best practice. A constant’s name can explain the rationale behind this magic number. It is also easier to find if you ever need to change it.

Loading history...
168
169 1
        if (parts.length != 2) {
0 ignored issues
show
Comprehensibility introduced by
Consider assigning this magic number 2 to a constant.

Using constants for hard-coded numbers is a best practice. A constant’s name can explain the rationale behind this magic number. It is also easier to find if you ever need to change it.

Loading history...
170 1
            return;
171
        }
172
173 1
        if (!parts[1].endsWith(";")) {
174 1
            throw new IllegalArgumentException();
175
        }
176
177 1
        String varName = parts[0].trim();
178 1
        String rawValue = StringUtils.substring(parts[1], 0, -1).trim();
0 ignored issues
show
Move the declaration of "rawValue" closer to the code that uses it.
Loading history...
179
180 1
        if (!fields.containsKey(varName)) {
181
            return;
182
        }
183
184 1
        fields.get(varName).set(structure, rawValue);
185 1
    }
186
187
    /**
188
     * Declare file structure
189
     */
190
    private void declareFields() {
191 1
        integer("id", SwfMapStructure::setId);
192 1
        integer("width", SwfMapStructure::setWidth);
193 1
        integer("height", SwfMapStructure::setHeight);
194 1
        integer("backgroundNum", SwfMapStructure::setBackgroundNum);
195 1
        integer("ambianceId", SwfMapStructure::setAmbianceId);
196 1
        integer("musicId", SwfMapStructure::setMusicId);
197 1
        bool("bOutdoor", SwfMapStructure::setOutdoor);
198 1
        integer("capabilities", SwfMapStructure::setCapabilities);
199 1
        string("mapData", SwfMapStructure::setMapData);
200 1
    }
201
202
    /**
203
     * Declare an integer field
204
     *
205
     * @param name Field name
206
     * @param setter The setter
207
     */
208
    private void integer(String name, Setter<Integer> setter) {
209 1
        fields.put(name, (map, value) -> setter.set(map, Integer.parseInt(value)));
0 ignored issues
show
Specify a type for: 'map', 'value'
Loading history...
210 1
    }
211
212
    /**
213
     * Declare an boolean field
214
     *
215
     * @param name Field name
216
     * @param setter The setter
217
     */
218
    private void bool(String name, Setter<Boolean> setter) {
219 1
        fields.put(name, (map, value) -> setter.set(map, "true".equals(value) || "1".equals(value)));
0 ignored issues
show
Specify a type for: 'map', 'value'
Loading history...
220 1
    }
221
222
    /**
223
     * Declare an string field
224
     *
225
     * @param name Field name
226
     * @param setter The setter
227
     */
228
    private void string(String name, Setter<String> setter) {
229 1
        fields.put(name, (map, value) -> {
0 ignored issues
show
Specify a type for: 'map', 'value'
Loading history...
230 1
            if (value.charAt(0) != '"' || value.charAt(value.length() - 1) != '"') {
231 1
                throw new IllegalArgumentException();
232
            }
233
234 1
            setter.set(map, value.substring(1, value.length() - 1));
235 1
        });
236 1
    }
237
238
    private void clearTemp(File tempdir) throws IOException {
0 ignored issues
show
Comprehensibility introduced by
Private methods that do not access instance data should be made static. This makes the code more readable and may enable the compiler to optimize your code. Consider making clearTempstatic.
Loading history...
239 1
        if (!tempdir.exists()) {
240
            return;
241
        }
242
243 1
        Files.walkFileTree(tempdir.toPath(), new SimpleFileVisitor<Path>() {
244
            @Override
245
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
246 1
                Files.delete(dir);
247
248 1
                return FileVisitResult.CONTINUE;
249
            }
250
251
            @Override
252
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
253 1
                Files.delete(file);
254
255 1
                return FileVisitResult.CONTINUE;
256
            }
257
        });
258 1
    }
259
}
260