diff --git a/.gitignore b/.gitignore index 84170b2..2ed74a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +testFile +testDir + *.class # Mobile Tools for Java (J2ME) diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..74676ef --- /dev/null +++ b/build.gradle @@ -0,0 +1,14 @@ +group 'com.github.nizshee' +version '1.0-SNAPSHOT' + +apply plugin: 'java' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +dependencies { + testCompile group: 'junit', name: 'junit', version: '4.11' +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..446cf15 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Oct 21 16:04:59 MSK 2016 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.13-bin.zip diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..27309d9 --- /dev/null +++ b/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f6d5974 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..bbfb9ad --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'torrent' + diff --git a/src/main/java/com/github/nizshee/client/ClientGui.java b/src/main/java/com/github/nizshee/client/ClientGui.java new file mode 100644 index 0000000..c70c80b --- /dev/null +++ b/src/main/java/com/github/nizshee/client/ClientGui.java @@ -0,0 +1,143 @@ +package com.github.nizshee.client; + + +import com.github.nizshee.server.util.FileDescriptor; +import javafx.application.Application; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.Scene; +import javafx.scene.control.*; +import javafx.scene.layout.*; +import javafx.scene.text.Text; +import javafx.stage.Stage; + +import java.util.Optional; + + +public class ClientGui extends Application { + private static TorrentClient torrentClient; + private static Thread serverThread; + private static ListView listView; + + + public static void main(String[] args) throws Exception { + short port = Short.parseShort(args[0]); + torrentClient = new TorrentClient(port, fp -> { + if (listView != null) listView.refresh(); + return null; + }); + serverThread = new Thread(() -> { + try { + while (!Thread.interrupted()) { + torrentClient.runNow(); + } + } catch (Exception e) { + e.printStackTrace(); + } + }); + serverThread.start(); + launch(args); + } + + @Override + public void start(Stage primaryStage) throws Exception { + + listView = new ListView<>(); + + ObservableList list = FXCollections.observableArrayList(); + listView.setCellFactory(param -> new FileCell()); + listView.setItems(list); + + primaryStage.setTitle("Torrent Client"); + Button btn = new Button(); + btn.setText("Refresh list"); + btn.setOnAction(event -> updateList()); + + Button btnInput = new Button(); + btnInput.setText("Add file"); + btnInput.setOnAction(event -> { + TextInputDialog dialog = new TextInputDialog(""); + dialog.setTitle("Upload file"); + dialog.setHeaderText(null); + dialog.setContentText("Enter file name:"); + Optional result = dialog.showAndWait(); + result.ifPresent(name -> { + try { + torrentClient.upload(name, identifier -> { + Platform.runLater(ClientGui::updateList); + return null; + }); + } catch (Exception e) { + e.printStackTrace(); + } + }); + }); + + StackPane root = new StackPane(); + VBox vbox = new VBox(); + vbox.getChildren().addAll(btn, btnInput, listView); + root.getChildren().add(vbox); + primaryStage.setScene(new Scene(root, 300, 250)); + primaryStage.show(); + } + + @Override + public void stop() throws Exception { + super.stop(); + torrentClient.close(); + serverThread.interrupt(); + serverThread.join(); + } + + private static void updateList() { + try { + torrentClient.list(files -> { + Platform.runLater(() -> listView.setItems(FXCollections.observableList(files))); + return null; + }); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private static class FileCell extends ListCell { + private final HBox hbox; + private final Text name; + private final Text progress; + private final Button btn; + + FileCell() { + hbox = new HBox(); + name = new Text(); + progress = new Text(); + btn = new Button("download"); + Pane pane = new Pane(); + hbox.getChildren().addAll(name, pane, progress, btn); + HBox.setHgrow(pane, Priority.ALWAYS); + } + + @Override + protected void updateItem(FileDescriptor item, boolean empty) { + super.updateItem(item, empty); + if (empty) { + setGraphic(null); + } else { + int info = torrentClient.info(item.id); + boolean isDownloading = info != -1; + int p = (int) (1 - ((double) info / (item.size / (ClientKeeperImpl.PART_SIZE)))) * 100; + name.setText(item.name + " (" + item.size + "b)"); + progress.setText("" + Math.max(0, Math.min(100, p))); + progress.setManaged(isDownloading); + progress.setVisible(isDownloading); + btn.setManaged(!isDownloading); + btn.setVisible(!isDownloading); + btn.setOnAction(event -> { + torrentClient.get(item.id); + listView.refresh(); + }); + setGraphic(hbox); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/github/nizshee/client/ClientKeeper.java b/src/main/java/com/github/nizshee/client/ClientKeeper.java new file mode 100644 index 0000000..10c0f16 --- /dev/null +++ b/src/main/java/com/github/nizshee/client/ClientKeeper.java @@ -0,0 +1,13 @@ +package com.github.nizshee.client; + + +import com.github.nizshee.client.util.FilePart; + +import java.util.Set; + +public interface ClientKeeper { + + Set stat(int identifier); + + byte[] get(FilePart part); +} diff --git a/src/main/java/com/github/nizshee/client/ClientKeeperImpl.java b/src/main/java/com/github/nizshee/client/ClientKeeperImpl.java new file mode 100644 index 0000000..f16f519 --- /dev/null +++ b/src/main/java/com/github/nizshee/client/ClientKeeperImpl.java @@ -0,0 +1,121 @@ +package com.github.nizshee.client; + + +import com.github.nizshee.client.util.FilePart; +import com.github.nizshee.server.util.FileDescriptor; + +import java.io.RandomAccessFile; +import java.io.Serializable; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +@SuppressWarnings("all") +public class ClientKeeperImpl implements ClientKeeper, Serializable { + + public static final int PART_SIZE = 1024; + + private Map info = new HashMap<>(); + private Map> downloaded = new HashMap<>(); + private Map> toDownload = new HashMap<>(); + + + @Override + public Set stat(int identifier) { + return downloaded.keySet().stream() + .filter(id -> id == identifier) + .findFirst() + .map(downloaded::get) + .orElse(new HashSet<>()); + } + + @Override + public byte[] get(FilePart filePart) { + Optional optional = getListItem(filePart.identifier); + if (!optional.isPresent()) return new byte[0]; + FileDescriptor item = optional.get(); + try (RandomAccessFile raf = new RandomAccessFile(item.name, "rw")) { + if (raf.length() != item.size) throw new Exception("file has different size"); + if (partCount(item.size) < filePart.part) return new byte[0]; + long seek = (long) filePart.part * PART_SIZE; + raf.seek(seek); + byte[] bytes = new byte[partSize(item.size, filePart.part)]; + raf.read(bytes); + return bytes; + } catch (Exception e) { + e.printStackTrace(); + return new byte[0]; + } + } + + public List update() { + return downloaded.keySet().stream() + .filter(k -> !downloaded.get(k).isEmpty()) + .collect(Collectors.toList()); + } + + public void upload(FileDescriptor item) { + info.put(item.id, item); + downloaded.put(item.id, IntStream.range(0, partCount(item.size)).mapToObj(i -> i).collect(Collectors.toSet())); + toDownload.put(item.id, new HashSet<>()); + } + + public void put(FilePart filePart, byte[] bytes) { + Optional optional = getListItem(filePart.identifier); + if (!optional.isPresent()) { + System.err.println("can't find id"); + return; + } + FileDescriptor item = optional.get(); + if (filePart.part > partCount(item.size)) { + System.err.println("wrong size"); + return; + } + try (RandomAccessFile raf = new RandomAccessFile(item.name, "rw")) { + if (raf.length() != item.size) throw new Exception("file has different size"); + long seek = (long) filePart.part * PART_SIZE; + raf.seek(seek); + raf.write(bytes); + toDownload.get(filePart.identifier).remove(filePart.part); + downloaded.get(filePart.identifier).add(filePart.part); + if (toDownload.get(filePart.identifier).isEmpty()) { + System.out.println("file " + filePart.identifier + " is ready"); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void get(FileDescriptor item) { + try (RandomAccessFile raf = new RandomAccessFile(item.name, "rw")) { + for (int i = 0; i < partCount(item.size); ++i) { + byte[] bytes = new byte[partSize(item.size, i)]; + raf.write(bytes); + } + } catch (Exception e) { + e.printStackTrace(); + } + info.put(item.id, item); + downloaded.put(item.id, new HashSet<>()); + toDownload.put(item.id, IntStream.range(0, partCount(item.size)).mapToObj(i -> i).collect(Collectors.toSet())); + } + + public Map> getToDownload() { + return new HashMap<>(toDownload); + } + + private Optional getListItem(int identifier) { + return info.containsKey(identifier) ? Optional.of(info.get(identifier)) : Optional.empty(); + } + + private int partCount(long size) { + return (int) (size / PART_SIZE) + (size % PART_SIZE == 0 ? 0 : 1); + } + + private int partSize(long size, int number) { + if (number < partCount(size) - 1) return PART_SIZE; + else { + return size % PART_SIZE == 0 ? PART_SIZE : (int) (size % PART_SIZE); + } + } +} diff --git a/src/main/java/com/github/nizshee/client/ClientMain.java b/src/main/java/com/github/nizshee/client/ClientMain.java new file mode 100644 index 0000000..9d062ec --- /dev/null +++ b/src/main/java/com/github/nizshee/client/ClientMain.java @@ -0,0 +1,63 @@ +package com.github.nizshee.client; + +import java.io.*; + + +public class ClientMain { + + public static void main(String[] args) throws IOException, ClassNotFoundException { + + BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); + TorrentClient torrentClient = new TorrentClient(Short.parseShort(args[0]), e -> null); + + label: + while (true) { + if (in.ready()) { + String input = in.readLine(); + try { + String[] parts = input.split(" "); + switch (parts[0]) { + case "exit": + break label; + case "list": { + torrentClient.list(list -> { + System.out.println("list:"); + list.forEach(System.out::println); + return null; + }); + break; + } + case "upload": { + String name = parts[1]; + torrentClient.upload(name, identifier -> { + System.out.println("file added with id " + identifier); + return null; + }); + break; + } + case "get": { + int identifier = Integer.parseInt(parts[1]); + torrentClient.get(identifier); + break; + } + case "sources": { + int identifier = Integer.parseInt(parts[1]); + torrentClient.sources(identifier, sources -> { + System.out.println("sources:"); + sources.forEach(System.out::println); + return null; + }); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + torrentClient.runNow(); + + } + + torrentClient.close(); + } + +} diff --git a/src/main/java/com/github/nizshee/client/TorrentClient.java b/src/main/java/com/github/nizshee/client/TorrentClient.java new file mode 100644 index 0000000..7348261 --- /dev/null +++ b/src/main/java/com/github/nizshee/client/TorrentClient.java @@ -0,0 +1,142 @@ +package com.github.nizshee.client; + + +import com.github.nizshee.client.procedure.ClientRegistry; +import com.github.nizshee.client.util.ConnectionWrapper; +import com.github.nizshee.client.util.Downloader; +import com.github.nizshee.client.util.FilePart; +import com.github.nizshee.server.procedure.ServerRegistry; +import com.github.nizshee.server.util.*; +import com.github.nizshee.server.util.FileDescriptor; +import com.github.nizshee.shared.Client; +import com.github.nizshee.shared.Server; +import com.github.nizshee.shared.exception.WrongDataException; + +import java.io.*; +import java.net.InetSocketAddress; +import java.time.LocalDateTime; +import java.util.List; + +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; + +@SuppressWarnings("all") +public class TorrentClient { + + private final short port; + + private final ClientKeeperImpl clientKeeper; + private final Server clientServer; + private final Client serverConnection1 = new Client(); + + private final Downloader downloader; + + private final ConnectionWrapper serverConnection = new ConnectionWrapper(serverConnection1); + + private LocalDateTime updateTime; + + public TorrentClient(short port, Function onComplete) throws ClassNotFoundException, IOException { + this.port = port; + + clientKeeper = restoreKeeper(); + ClientRegistry clientRegistry = new ClientRegistry(clientKeeper); + clientServer = new Server(clientRegistry); + downloader = new Downloader(onComplete); + + clientServer.start(new InetSocketAddress("localhost", port)); + serverConnection1.connect(new InetSocketAddress("localhost", 8081)); + updateTime = LocalDateTime.now(); + } + + public synchronized void close() throws IOException { + downloader.clear(); + clientServer.stop(); + serverConnection.disconnect(); + + dumpKeeper(clientKeeper); + } + + public synchronized void runNow() throws IOException { + if (updateTime.isBefore(LocalDateTime.now())) { + downloader.clear(); + update(clientKeeper, serverConnection, port); + updateTime = LocalDateTime.now().plusMinutes(4); + } + + downloader.runNow(clientKeeper, serverConnection); + serverConnection.runNow(); + clientServer.runNow(); + } + + public synchronized void list(Function, Void> func) throws WrongDataException { + serverConnection.add(ServerRegistry.LISTW, new Object(), list -> { + downloader.updateList(list); + func.apply(list); + return null; + }); + } + + public synchronized void upload(String name, Function func) throws Exception { + if (!new File(name).exists()) throw new Exception("File not exists."); + long size = (new File(name)).length(); + serverConnection.add(ServerRegistry.UPLOADW, new UploadItem(name, size), identifier -> { + clientKeeper.upload(new FileDescriptor(identifier, name, size)); + update(clientKeeper, serverConnection, port); + func.apply(identifier); + return null; + }); + } + + public synchronized void get(int identifier) { + Optional optional = downloader.resolve(identifier); + if (!optional.isPresent()) { + System.out.println("Maybe you should get list first?"); + } else { + clientKeeper.get(optional.get()); + downloader.clear(); + } + } + + public synchronized int info(int identifier) { + Map> m = clientKeeper.getToDownload(); + if(!m.containsKey(identifier)) return -1; + return m.get(identifier).size(); + } + + public synchronized void sources(int identifier, Function, Void> func) throws WrongDataException { + serverConnection.add(ServerRegistry.SOURCESW, identifier, sources -> { + func.apply(sources); + return null; + }); + } + + private static void update(ClientKeeperImpl client, ConnectionWrapper server, short port) { + try { + List ids = client.update(); + server.add(ServerRegistry.UPDATEW, new UpdateItem(port, ids), result -> { + if (!result) update(client, server, port); + return null; + }); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private static ClientKeeperImpl restoreKeeper() throws IOException, ClassNotFoundException { + try (FileInputStream fis = new FileInputStream(".client-keeper")) { + ObjectInputStream ois = new ObjectInputStream(fis); + return (ClientKeeperImpl) ois.readObject(); + } catch (FileNotFoundException ignore) { + return new ClientKeeperImpl(); + } + } + + private static void dumpKeeper(ClientKeeperImpl keeper) throws IOException { + FileOutputStream fos = new FileOutputStream(".client-keeper"); + ObjectOutputStream oos = new ObjectOutputStream(fos); + oos.writeObject(keeper); + fos.close(); + } +} diff --git a/src/main/java/com/github/nizshee/client/procedure/ClientRegistry.java b/src/main/java/com/github/nizshee/client/procedure/ClientRegistry.java new file mode 100644 index 0000000..7848a8a --- /dev/null +++ b/src/main/java/com/github/nizshee/client/procedure/ClientRegistry.java @@ -0,0 +1,43 @@ +package com.github.nizshee.client.procedure; + + +import com.github.nizshee.client.ClientKeeper; +import com.github.nizshee.client.util.FilePart; +import com.github.nizshee.shared.procedure.ProcedureRegistry; +import com.github.nizshee.shared.procedure.ProcedureWrapper; +import com.github.nizshee.shared.procedure.RemoteProcedure; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + + +@SuppressWarnings("all") +public class ClientRegistry extends ProcedureRegistry { + + public ClientRegistry(ClientKeeper clientKeeper) { + super(clientKeeper); + } + + public static final byte STAT_ID = 1; + public static final byte GET_ID = 2; + + private final static StatProcedure STAT = new StatProcedure(); + private final static GetProcedure GET = new GetProcedure(); + + public static final ProcedureWrapper> STATW = new ProcedureWrapper<>(STAT_ID, STAT); + public static final ProcedureWrapper GETW = new ProcedureWrapper<>(GET_ID, GET); + + private final static Map> procedures; + + static { + procedures = new HashMap<>(); + procedures.put(STAT_ID, STAT); + procedures.put(GET_ID, GET); + } + + @Override + protected Map> procedures() { + return procedures; + } +} diff --git a/src/main/java/com/github/nizshee/client/procedure/GetProcedure.java b/src/main/java/com/github/nizshee/client/procedure/GetProcedure.java new file mode 100644 index 0000000..49d6b6a --- /dev/null +++ b/src/main/java/com/github/nizshee/client/procedure/GetProcedure.java @@ -0,0 +1,71 @@ +package com.github.nizshee.client.procedure; + + +import com.github.nizshee.client.ClientKeeper; +import com.github.nizshee.client.util.FilePart; +import com.github.nizshee.shared.util.Dumper; +import com.github.nizshee.shared.exception.WrongDataException; +import com.github.nizshee.shared.procedure.RemoteProcedure; + +import java.io.*; + +@SuppressWarnings("all") +public class GetProcedure extends RemoteProcedure { + + private final Dumper request = new Dumper() { + @Override + public byte[] dump(FilePart filePart) throws WrongDataException { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); + DataOutputStream dos = new DataOutputStream(baos); + dos.writeInt(filePart.identifier); + dos.writeInt(filePart.part); + return baos.toByteArray(); + } catch (IOException e) { + e.printStackTrace(); + throw new WrongDataException("Can't write get."); + } + } + + @Override + public FilePart restore(byte[] bytes) throws WrongDataException { + try { + ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + DataInputStream dis = new DataInputStream(bais); + int identifier = dis.readInt(); + int part = dis.readInt(); + return new FilePart(identifier, part); + } catch (IOException e) { + e.printStackTrace(); + throw new WrongDataException("Can't read get."); + } + } + }; + + private final Dumper response = new Dumper() { + @Override + public byte[] dump(byte[] bytes) throws WrongDataException { + return bytes; + } + + @Override + public byte[] restore(byte[] bytes) throws WrongDataException { + return bytes; + } + }; + + @Override + protected Dumper request() { + return request; + } + + @Override + protected Dumper response() { + return response; + } + + @Override + protected byte[] execute(byte[] ip, ClientKeeper clientKeeper, FilePart filePart) { + return clientKeeper.get(filePart); + } +} diff --git a/src/main/java/com/github/nizshee/client/procedure/StatProcedure.java b/src/main/java/com/github/nizshee/client/procedure/StatProcedure.java new file mode 100644 index 0000000..eae4362 --- /dev/null +++ b/src/main/java/com/github/nizshee/client/procedure/StatProcedure.java @@ -0,0 +1,72 @@ +package com.github.nizshee.client.procedure; + + +import com.github.nizshee.client.ClientKeeper; +import com.github.nizshee.shared.util.Dumper; +import com.github.nizshee.shared.exception.WrongDataException; +import com.github.nizshee.shared.procedure.RemoteProcedure; +import com.github.nizshee.shared.util.IntDumper; + +import java.io.*; +import java.util.HashSet; +import java.util.Set; + + +@SuppressWarnings("all") +public class StatProcedure extends RemoteProcedure> { + + private final Dumper request = new IntDumper(); + + private final Dumper> response = new Dumper>() { + @Override + public byte[] dump(Set identifiers) throws WrongDataException { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); + DataOutputStream dos = new DataOutputStream(baos); + dos.writeInt(identifiers.size()); + for (int identifier: identifiers) { + dos.writeInt(identifier); + } + return baos.toByteArray(); + } catch (IOException e) { + e.printStackTrace(); + throw new WrongDataException("Can't write stat."); + } + } + + @Override + public Set restore(byte[] bytes) throws WrongDataException { + try { + ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + DataInputStream dis = new DataInputStream(bais); + + int count = dis.readInt(); + Set identifiers = new HashSet<>(count); + for (int i = 0; i < count; ++i) { + int identifier = dis.readInt(); + identifiers.add(identifier); + } + return identifiers; + } catch (IOException e) { + e.printStackTrace(); + throw new WrongDataException("Can't read stat."); + } + } + }; + + + @Override + protected Dumper request() { + return request; + } + + @Override + protected Dumper> response() { + return response; + } + + @Override + protected Set execute(byte[] ip, ClientKeeper clientKeeper, Integer integer) { + return clientKeeper.stat(integer); + } +} diff --git a/src/main/java/com/github/nizshee/client/util/ConnectionWrapper.java b/src/main/java/com/github/nizshee/client/util/ConnectionWrapper.java new file mode 100644 index 0000000..4967513 --- /dev/null +++ b/src/main/java/com/github/nizshee/client/util/ConnectionWrapper.java @@ -0,0 +1,57 @@ +package com.github.nizshee.client.util; + + +import com.github.nizshee.shared.Client; +import com.github.nizshee.shared.exception.WrongDataException; +import com.github.nizshee.shared.procedure.ProcedureWrapper; + +import java.io.IOException; +import java.util.LinkedList; +import java.util.function.Function; + +public class ConnectionWrapper { + + private final Client client; + + private final LinkedList data; + private final LinkedList> callbacks; + private final boolean isWaiting; + + public ConnectionWrapper(Client client) { + this.client = client; + data = new LinkedList<>(); + callbacks = new LinkedList<>(); + isWaiting = false; + } + + public void add(ProcedureWrapper procedure, Request request, + Function callback) + throws WrongDataException { + byte[] bytesRequest = procedure.dump(request); + data.add(bytesRequest); + callbacks.add(bytesResponse -> { + try { + Response response = procedure.restore(bytesResponse); + callback.apply(response); + } catch (WrongDataException e) { + e.printStackTrace(); + } + return null; + }); + } + + public void runNow() throws IOException { + if (!isWaiting && !data.isEmpty()) { + client.write(data.pollFirst()); + } + LinkedList d = client.runNow(); + while (!d.isEmpty()) { + Function callback = callbacks.pollFirst(); + callback.apply(d.pollFirst()); + } + } + + public void disconnect() throws IOException { + client.disconnect(); + } +} diff --git a/src/main/java/com/github/nizshee/client/util/Downloader.java b/src/main/java/com/github/nizshee/client/util/Downloader.java new file mode 100644 index 0000000..20249d7 --- /dev/null +++ b/src/main/java/com/github/nizshee/client/util/Downloader.java @@ -0,0 +1,97 @@ +package com.github.nizshee.client.util; + + +import com.github.nizshee.client.ClientKeeperImpl; +import com.github.nizshee.server.procedure.ServerRegistry; +import com.github.nizshee.server.util.ClientItem; +import com.github.nizshee.server.util.FileDescriptor; +import com.github.nizshee.shared.Client; +import com.github.nizshee.shared.exception.WrongDataException; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.*; +import java.util.function.Function; + + +/** + * Naive downloader. + */ +public class Downloader { + + private List list = new LinkedList<>(); + private boolean isClear = true; + private Map peers = new HashMap<>(); + private final Function onComplete; + + public Downloader(Function onComplete) { + this.onComplete = onComplete; + } + + + public void runNow(ClientKeeperImpl client, ConnectionWrapper server) { + if (isClear) { + prepare(client, server); + isClear = false; + } else if (client.getToDownload().isEmpty()) { + clear(); + } else { + for (Iterator> it = peers.entrySet().iterator(); it.hasNext();) { + Map.Entry entry = it.next(); + try { + entry.getValue().runNow(); + } catch (IOException e) { + e.printStackTrace(); + it.remove(); + } + } + } + + } + + public void clear() { + peers.values().forEach(Peer::disconnect); + peers.clear(); + isClear = true; + } + + @SuppressWarnings("all") + private void prepare(ClientKeeperImpl client, ConnectionWrapper server) { + for (int id: client.getToDownload().keySet()) { + try { + server.add(ServerRegistry.SOURCESW, id, clientItems -> { + for (ClientItem item: clientItems) { + if (!peers.containsKey(item)) { + Client client1 = new Client(); + byte[] ip = {item.ip0, item.ip1, item.ip2, item.ip3}; + try { + client1.connect(new InetSocketAddress(InetAddress.getByAddress(ip), item.port)); + ConnectionWrapper wrapper = new ConnectionWrapper(client1); + Peer peer = new Peer(client, wrapper); + peer.resolve(id, client.getToDownload().get(id), onComplete); + peers.put(item, peer); + } catch (Exception e) { + e.printStackTrace(); + try { + client1.disconnect(); + } catch (IOException ignore) {} + } + } + } + return null; + }); + } catch (WrongDataException ignore) { + } + } + } + + public void updateList(List list) { + this.list = list; + } + + public Optional resolve(int identifier) { + return list.stream().filter(i -> i.id == identifier).findAny(); + } + +} diff --git a/src/main/java/com/github/nizshee/client/util/FilePart.java b/src/main/java/com/github/nizshee/client/util/FilePart.java new file mode 100644 index 0000000..4699651 --- /dev/null +++ b/src/main/java/com/github/nizshee/client/util/FilePart.java @@ -0,0 +1,26 @@ +package com.github.nizshee.client.util; + + +public class FilePart { + public final int identifier; + public final int part; + + public FilePart(int identifier, int part) { + this.identifier = identifier; + this.part = part; + } + + @Override + public int hashCode() { + return identifier + part; + } + + @Override + public boolean equals(Object o) { + if (o instanceof FilePart) { + FilePart other = (FilePart) o; + return identifier == other.identifier && part == other.part; + } + return false; + } +} diff --git a/src/main/java/com/github/nizshee/client/util/Peer.java b/src/main/java/com/github/nizshee/client/util/Peer.java new file mode 100644 index 0000000..cd14e2a --- /dev/null +++ b/src/main/java/com/github/nizshee/client/util/Peer.java @@ -0,0 +1,64 @@ +package com.github.nizshee.client.util; + + +import com.github.nizshee.client.ClientKeeperImpl; +import com.github.nizshee.client.procedure.ClientRegistry; +import com.github.nizshee.shared.exception.WrongDataException; + +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + + +@SuppressWarnings("all") +public class Peer { + private final ConnectionWrapper connection; + private final Map> parts = new HashMap<>(); + private final ClientKeeperImpl client; + + public Peer(ClientKeeperImpl client, ConnectionWrapper connection) { + this.connection = connection; + this.client = client; + } + + public void resolve(int identifier, Set toDownload, Function onComplete) { + if (!parts.containsKey(identifier)) { + parts.put(identifier, new HashSet<>()); + try { + connection.add(ClientRegistry.STATW, identifier, ps -> { + parts.put(identifier, ps); + ps.retainAll(toDownload); + for (int part: ps) { + FilePart fp = new FilePart(identifier, part); + try { + connection.add(ClientRegistry.GETW, fp, bytes -> { + onComplete.apply(fp); + client.put(fp, bytes); + return null; + }); + } catch (WrongDataException e) { + e.printStackTrace(); + } + } + return null; + }); + } catch (WrongDataException ignore) { + } + } + } + + public void runNow() throws IOException { + connection.runNow(); + } + + public void disconnect() { + try { + connection.disconnect(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/com/github/nizshee/server/ServerKeeper.java b/src/main/java/com/github/nizshee/server/ServerKeeper.java new file mode 100644 index 0000000..2343043 --- /dev/null +++ b/src/main/java/com/github/nizshee/server/ServerKeeper.java @@ -0,0 +1,20 @@ +package com.github.nizshee.server; + + +import com.github.nizshee.server.util.ClientItem; +import com.github.nizshee.server.util.FileDescriptor; +import com.github.nizshee.server.util.UpdateItem; +import com.github.nizshee.server.util.UploadItem; + +import java.util.List; + +public interface ServerKeeper { + + List list(); + + int upload(UploadItem item); + + List sources(int identifier); + + boolean update(byte[] ip, UpdateItem item); +} diff --git a/src/main/java/com/github/nizshee/server/ServerKeeperImpl.java b/src/main/java/com/github/nizshee/server/ServerKeeperImpl.java new file mode 100644 index 0000000..d445f05 --- /dev/null +++ b/src/main/java/com/github/nizshee/server/ServerKeeperImpl.java @@ -0,0 +1,57 @@ +package com.github.nizshee.server; + + +import com.github.nizshee.server.util.*; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +@SuppressWarnings("all") +public class ServerKeeperImpl implements ServerKeeper, Serializable { + + private int identifierCounter = 0; + private List files = new LinkedList<>(); + private Map clients = new HashMap<>(); + private Map> clientIds = new HashMap<>(); + + @Override + public List list() { + return files; + } + + @Override + public int upload(UploadItem item) { + files.add(new FileDescriptor(identifierCounter, item.name, item.size)); + return identifierCounter++; + } + + @Override + public List sources(int identifier) { + return clientIds.keySet().stream() + .filter(id -> clientIds.get(id) + .contains(identifier)) + .collect(Collectors.toList()); + + } + + @Override + public boolean update(byte[] ip, UpdateItem item) { + ClientItem id = new ClientItem(ip[0], ip[1], ip[2], ip[3], item.port); + clients.put(id, LocalDateTime.now().plusMinutes(5)); + clientIds.put(id, item.identifiers); + return true; + } + + public void clearOld(LocalDateTime current) { + Set set = clients.keySet().stream() + .filter(id -> clients.get(id).isBefore(current)) + .collect(Collectors.toSet()); + + set.forEach(id -> { + clients.remove(id); + clientIds.remove(id); + }); + } +} diff --git a/src/main/java/com/github/nizshee/server/ServerMain.java b/src/main/java/com/github/nizshee/server/ServerMain.java new file mode 100644 index 0000000..d11a06d --- /dev/null +++ b/src/main/java/com/github/nizshee/server/ServerMain.java @@ -0,0 +1,54 @@ +package com.github.nizshee.server; + + +import com.github.nizshee.server.procedure.ServerRegistry; +import com.github.nizshee.shared.Server; + +import java.io.*; +import java.net.InetSocketAddress; +import java.time.LocalDateTime; + +public class ServerMain { + + public static void main(String[] args) throws IOException, ClassNotFoundException { + + ServerKeeperImpl serverKeeper = restoreKeeper(); + ServerRegistry serverRegistry = new ServerRegistry(serverKeeper); + Server server = new Server(serverRegistry); + + server.start(new InetSocketAddress("localhost", 8081)); + + BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); + + while (true) { + if (in.ready()) { + String input = in.readLine(); + if (input.equals("exit")) { + break; + } else { + System.out.println("input 'exit'"); + } + } + server.runNow(); + serverKeeper.clearOld(LocalDateTime.now()); + } + + dumpKeeper(serverKeeper); + } + + private static ServerKeeperImpl restoreKeeper() throws IOException, ClassNotFoundException { + try (FileInputStream fis = new FileInputStream(".server-keeper")) { + ObjectInputStream ois = new ObjectInputStream(fis); + return (ServerKeeperImpl) ois.readObject(); + } catch (FileNotFoundException ignore) { + return new ServerKeeperImpl(); + } + } + + private static void dumpKeeper(ServerKeeperImpl keeper) throws IOException { + FileOutputStream fos = new FileOutputStream(".server-keeper"); + ObjectOutputStream oos = new ObjectOutputStream(fos); + oos.writeObject(keeper); + fos.close(); + } +} diff --git a/src/main/java/com/github/nizshee/server/procedure/ListProcedure.java b/src/main/java/com/github/nizshee/server/procedure/ListProcedure.java new file mode 100644 index 0000000..7e87472 --- /dev/null +++ b/src/main/java/com/github/nizshee/server/procedure/ListProcedure.java @@ -0,0 +1,86 @@ +package com.github.nizshee.server.procedure; + + +import com.github.nizshee.server.util.FileDescriptor; +import com.github.nizshee.server.ServerKeeper; +import com.github.nizshee.shared.util.Dumper; +import com.github.nizshee.shared.procedure.RemoteProcedure; +import com.github.nizshee.shared.exception.WrongDataException; + +import java.io.*; +import java.util.ArrayList; +import java.util.List; + +@SuppressWarnings("all") +public class ListProcedure extends RemoteProcedure> { + + private Dumper request = new Dumper() { + @Override + public byte[] dump(Object o) throws WrongDataException { + return new byte[0]; + } + + @Override + public Object restore(byte[] bytes) throws WrongDataException { + return new Object(); + } + }; + + private Dumper> response = new Dumper>() { + @Override + public byte[] dump(List items) throws WrongDataException { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); + DataOutputStream dos = new DataOutputStream(baos); + dos.writeInt(items.size()); + for (FileDescriptor item: items) { + dos.writeInt(item.id); + dos.writeUTF(item.name); + dos.writeLong(item.size); + } + return baos.toByteArray(); + } catch (IOException e) { + e.printStackTrace(); + throw new WrongDataException("Can't write list."); + } + } + + @Override + public List restore(byte[] bytes) throws WrongDataException { + try { + ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + DataInputStream dis = new DataInputStream(bais); + + int count = dis.readInt(); + List list = new ArrayList<>(count); + for (int i = 0; i < count; ++i) { + int id = dis.readInt(); + String name = dis.readUTF(); + long size = dis.readLong(); + list.add(i, new FileDescriptor(id, name, size)); + } + return list; + } catch (IOException e) { + e.printStackTrace(); + throw new WrongDataException("Can't read list."); + } + } + }; + + @Override + protected Dumper request() { + return request; + } + + @Override + protected Dumper> response() { + return response; + } + + @Override + protected List execute(byte[] ip, ServerKeeper keeper, Object o) { + return keeper.list(); + } + + +} diff --git a/src/main/java/com/github/nizshee/server/procedure/ServerRegistry.java b/src/main/java/com/github/nizshee/server/procedure/ServerRegistry.java new file mode 100644 index 0000000..5b7a744 --- /dev/null +++ b/src/main/java/com/github/nizshee/server/procedure/ServerRegistry.java @@ -0,0 +1,53 @@ +package com.github.nizshee.server.procedure; + + +import com.github.nizshee.server.ServerKeeper; +import com.github.nizshee.server.util.ClientItem; +import com.github.nizshee.server.util.FileDescriptor; +import com.github.nizshee.server.util.UpdateItem; +import com.github.nizshee.server.util.UploadItem; +import com.github.nizshee.shared.procedure.ProcedureRegistry; +import com.github.nizshee.shared.procedure.ProcedureWrapper; +import com.github.nizshee.shared.procedure.RemoteProcedure; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ServerRegistry extends ProcedureRegistry { + + public ServerRegistry(ServerKeeper keeper) { + super(keeper); + } + + private static final byte LIST_ID = 1; + private static final byte UPLOAD_ID = 2; + private static final byte SOURCES_ID = 3; + private static final byte UPDATE_ID = 4; + + private static final ListProcedure LIST = new ListProcedure(); + private static final UploadProcedure UPLOAD = new UploadProcedure(); + private static final SourcesProcedure SOURCES = new SourcesProcedure(); + private static final UpdateProcedure UPDATE = new UpdateProcedure(); + + public static final ProcedureWrapper> LISTW = new ProcedureWrapper<>(LIST_ID, LIST); + public static final ProcedureWrapper UPLOADW = new ProcedureWrapper<>(UPLOAD_ID, UPLOAD); + public static final ProcedureWrapper> SOURCESW = new ProcedureWrapper<>(SOURCES_ID, SOURCES); + public static final ProcedureWrapper UPDATEW = new ProcedureWrapper<>(UPDATE_ID, UPDATE); + + + private final static Map> procedures; + + static { + procedures = new HashMap<>(); + procedures.put(LIST_ID, LIST); + procedures.put(UPLOAD_ID, UPLOAD); + procedures.put(SOURCES_ID, SOURCES); + procedures.put(UPDATE_ID, UPDATE); + } + + @Override + protected Map> procedures() { + return procedures; + } +} diff --git a/src/main/java/com/github/nizshee/server/procedure/SourcesProcedure.java b/src/main/java/com/github/nizshee/server/procedure/SourcesProcedure.java new file mode 100644 index 0000000..51a92fa --- /dev/null +++ b/src/main/java/com/github/nizshee/server/procedure/SourcesProcedure.java @@ -0,0 +1,79 @@ +package com.github.nizshee.server.procedure; + + +import com.github.nizshee.server.ServerKeeper; +import com.github.nizshee.server.util.ClientItem; +import com.github.nizshee.shared.util.IntDumper; +import com.github.nizshee.shared.util.Dumper; +import com.github.nizshee.shared.procedure.RemoteProcedure; +import com.github.nizshee.shared.exception.WrongDataException; + +import java.io.*; +import java.util.ArrayList; +import java.util.List; + +@SuppressWarnings("all") +public class SourcesProcedure extends RemoteProcedure> { + + private final Dumper request = new IntDumper(); + + private final Dumper> response = new Dumper>() { + @Override + public byte[] dump(List items) throws WrongDataException { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); + DataOutputStream dos = new DataOutputStream(baos); + dos.writeInt(items.size()); + for (ClientItem item: items) { + dos.writeByte(item.ip0); + dos.writeByte(item.ip1); + dos.writeByte(item.ip2); + dos.writeByte(item.ip3); + dos.writeShort(item.port); + } + return baos.toByteArray(); + } catch (IOException e) { + e.printStackTrace(); + throw new WrongDataException("Can't write sources."); + } + } + + @Override + public List restore(byte[] bytes) throws WrongDataException { + try { + ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + DataInputStream dis = new DataInputStream(bais); + + int count = dis.readInt(); + List list = new ArrayList<>(count); + for (int i = 0; i < count; ++i) { + byte ip0 = dis.readByte(); + byte ip1 = dis.readByte(); + byte ip2 = dis.readByte(); + byte ip3 = dis.readByte(); + short port = dis.readShort(); + list.add(i, new ClientItem(ip0, ip1, ip2, ip3, port)); + } + return list; + } catch (IOException e) { + e.printStackTrace(); + throw new WrongDataException("Can't read sources."); + } + } + }; + + @Override + protected Dumper request() { + return request; + } + + @Override + protected Dumper> response() { + return response; + } + + @Override + protected List execute(byte[] ip, ServerKeeper keeper, Integer integer) { + return keeper.sources(integer); + } +} diff --git a/src/main/java/com/github/nizshee/server/procedure/UpdateProcedure.java b/src/main/java/com/github/nizshee/server/procedure/UpdateProcedure.java new file mode 100644 index 0000000..094cae2 --- /dev/null +++ b/src/main/java/com/github/nizshee/server/procedure/UpdateProcedure.java @@ -0,0 +1,73 @@ +package com.github.nizshee.server.procedure; + + +import com.github.nizshee.server.ServerKeeper; +import com.github.nizshee.server.util.UpdateItem; +import com.github.nizshee.shared.util.Dumper; +import com.github.nizshee.shared.procedure.RemoteProcedure; +import com.github.nizshee.shared.exception.WrongDataException; +import com.github.nizshee.shared.util.BooleanDumper; + +import java.io.*; +import java.util.ArrayList; +import java.util.List; + +@SuppressWarnings("all") +public class UpdateProcedure extends RemoteProcedure { + + private final Dumper request = new Dumper() { + @Override + public byte[] dump(UpdateItem item) throws WrongDataException { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); + DataOutputStream dos = new DataOutputStream(baos); + dos.writeShort(item.port); + dos.writeInt(item.identifiers.size()); + for (int identifier: item.identifiers) { + dos.writeInt(identifier); + } + return baos.toByteArray(); + } catch (IOException e) { + e.printStackTrace(); + throw new WrongDataException("Can't write list."); + } + } + + @Override + public UpdateItem restore(byte[] bytes) throws WrongDataException { + try { + ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + DataInputStream dis = new DataInputStream(bais); + + short port = dis.readShort(); + int count = dis.readInt(); + List identifiers = new ArrayList<>(count); + for (int i = 0; i < count; ++i) { + int identifier = dis.readInt(); + identifiers.add(i, identifier); + } + return new UpdateItem(port, identifiers); + } catch (IOException e) { + e.printStackTrace(); + throw new WrongDataException("Can't read list."); + } + } + }; + + private final Dumper response = new BooleanDumper(); + + @Override + protected Dumper request() { + return request; + } + + @Override + protected Dumper response() { + return response; + } + + @Override + protected Boolean execute(byte[] ip, ServerKeeper keeper, UpdateItem updateItem) { + return keeper.update(ip, updateItem); + } +} diff --git a/src/main/java/com/github/nizshee/server/procedure/UploadProcedure.java b/src/main/java/com/github/nizshee/server/procedure/UploadProcedure.java new file mode 100644 index 0000000..3ddfc61 --- /dev/null +++ b/src/main/java/com/github/nizshee/server/procedure/UploadProcedure.java @@ -0,0 +1,61 @@ +package com.github.nizshee.server.procedure; + + +import com.github.nizshee.server.ServerKeeper; +import com.github.nizshee.shared.util.IntDumper; +import com.github.nizshee.server.util.UploadItem; +import com.github.nizshee.shared.util.Dumper; +import com.github.nizshee.shared.procedure.RemoteProcedure; +import com.github.nizshee.shared.exception.WrongDataException; + +import java.io.*; + +@SuppressWarnings("all") +public class UploadProcedure extends RemoteProcedure{ + private final Dumper request = new Dumper() { + @Override + public byte[] dump(UploadItem item) throws WrongDataException { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); + DataOutputStream dos = new DataOutputStream(baos); + dos.writeUTF(item.name); + dos.writeLong(item.size); + return baos.toByteArray(); + } catch (IOException e) { + e.printStackTrace(); + throw new WrongDataException("Can't write upload."); + } + } + + @Override + public UploadItem restore(byte[] bytes) throws WrongDataException { + try { + ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + DataInputStream dis = new DataInputStream(bais); + String name = dis.readUTF(); + long size = dis.readLong(); + return new UploadItem(name, size); + } catch (IOException e) { + e.printStackTrace(); + throw new WrongDataException("Can't read list."); + } + } + }; + + private final Dumper response = new IntDumper(); + + @Override + public Dumper request() { + return request; + } + + @Override + public Dumper response() { + return response; + } + + @Override + protected Integer execute(byte[] ip, ServerKeeper keeper, UploadItem uploadItem) { + return keeper.upload(uploadItem); + } +} diff --git a/src/main/java/com/github/nizshee/server/util/ClientItem.java b/src/main/java/com/github/nizshee/server/util/ClientItem.java new file mode 100644 index 0000000..3a63045 --- /dev/null +++ b/src/main/java/com/github/nizshee/server/util/ClientItem.java @@ -0,0 +1,39 @@ +package com.github.nizshee.server.util; + + +import java.io.Serializable; + +public class ClientItem implements Serializable { + public final byte ip0; + public final byte ip1; + public final byte ip2; + public final byte ip3; + public final short port; + + public ClientItem(byte ip0, byte ip1, byte ip2, byte ip3, short port) { + this.ip0 = ip0; + this.ip1 = ip1; + this.ip2 = ip2; + this.ip3 = ip3; + this.port = port; + } + + @Override + public int hashCode() { + return Byte.hashCode(ip0) + Byte.hashCode(ip1) + Byte.hashCode(ip2) + Byte.hashCode(ip3) + Short.hashCode(port); + } + + @Override + public boolean equals(Object o) { + if (o instanceof ClientItem) { + ClientItem other = (ClientItem) o; + return ip0 == other.ip0 && ip1 == other.ip1 && ip2 == other.ip2 && ip3 == other.ip3 && port == other.port; + } + return false; + } + + @Override + public String toString() { + return ip0 + "." + ip1 + "." + ip2 + "." + ip3 + ":" + port; + } +} diff --git a/src/main/java/com/github/nizshee/server/util/FileDescriptor.java b/src/main/java/com/github/nizshee/server/util/FileDescriptor.java new file mode 100644 index 0000000..3319ea1 --- /dev/null +++ b/src/main/java/com/github/nizshee/server/util/FileDescriptor.java @@ -0,0 +1,35 @@ +package com.github.nizshee.server.util; + + +import java.io.Serializable; + +public class FileDescriptor implements Serializable { + public final int id; + public final String name; + public final long size; + + public FileDescriptor(int id, String name, long size) { + this.id = id; + this.name = name; + this.size = size; + } + + @Override + public int hashCode() { + return id + name.hashCode() + Long.hashCode(size); + } + + @Override + public boolean equals(Object o) { + if (o instanceof FileDescriptor) { + FileDescriptor other = (FileDescriptor) o; + return id == other.id && name.equals(other.name) && size == other.size; + } + return false; + } + + @Override + public String toString() { + return "" + id + " " + name + " " + size + "b"; + } +} diff --git a/src/main/java/com/github/nizshee/server/util/UpdateItem.java b/src/main/java/com/github/nizshee/server/util/UpdateItem.java new file mode 100644 index 0000000..56863ee --- /dev/null +++ b/src/main/java/com/github/nizshee/server/util/UpdateItem.java @@ -0,0 +1,29 @@ +package com.github.nizshee.server.util; + + +import java.util.Collections; +import java.util.List; + +public class UpdateItem { + public final short port; + public final List identifiers; + + public UpdateItem(short port, List identifiers) { + this.port = port; + this.identifiers = Collections.unmodifiableList(identifiers); + } + + @Override + public int hashCode() { + return Short.hashCode(port) + identifiers.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o instanceof UpdateItem) { + UpdateItem other = (UpdateItem) o; + return port == other.port && identifiers.equals(other.identifiers); + } + return false; + } +} diff --git a/src/main/java/com/github/nizshee/server/util/UploadItem.java b/src/main/java/com/github/nizshee/server/util/UploadItem.java new file mode 100644 index 0000000..e0abe60 --- /dev/null +++ b/src/main/java/com/github/nizshee/server/util/UploadItem.java @@ -0,0 +1,26 @@ +package com.github.nizshee.server.util; + + +public class UploadItem { + public final String name; + public final long size; + + public UploadItem(String name, long size) { + this.name = name; + this.size = size; + } + + @Override + public int hashCode() { + return name.hashCode() + Long.hashCode(size); + } + + @Override + public boolean equals(Object o) { + if (o instanceof UploadItem) { + UploadItem other = (UploadItem) o; + return name.equals(other.name) && size == other.size; + } + return false; + } +} diff --git a/src/main/java/com/github/nizshee/shared/Client.java b/src/main/java/com/github/nizshee/shared/Client.java new file mode 100644 index 0000000..840ecd6 --- /dev/null +++ b/src/main/java/com/github/nizshee/shared/Client.java @@ -0,0 +1,86 @@ +package com.github.nizshee.shared; + + +import com.github.nizshee.shared.exception.ConnectionClosedException; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.util.Iterator; +import java.util.LinkedList; + +public class Client { + + private SocketChannel socketChannel = null; + private ConnectionContext context = null; + private Selector selector = null; + private volatile boolean isRunning = false; + private ByteBuffer byteBuffer = ByteBuffer.allocate(2048); + + public void connect(InetSocketAddress address) throws IOException { + socketChannel = SocketChannel.open(); + socketChannel.connect(address); + socketChannel.configureBlocking(false); + selector = Selector.open(); + socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE); + context = new ConnectionContext(); + isRunning = true; + } + + public void disconnect() throws IOException { + if (selector != null) selector.close(); + if (socketChannel != null) socketChannel.close(); + + isRunning = false; + context = null; + selector = null; + socketChannel = null; + } + + public void write(byte[] bytes) throws IOException { + if (!isRunning) throw new IOException("TorrentClient not connected."); + context.write(bytes); + } + + public LinkedList runNow() throws IOException { + if (!isRunning) throw new IOException("TorrentClient not connected."); + + selector.selectNow(); + final Iterator iterator = selector.selectedKeys().iterator(); + LinkedList result = new LinkedList<>(); + + while (iterator.hasNext()) { + final SelectionKey key = iterator.next(); + + if (key.isValid() && key.isReadable()) { + try { + long count = socketChannel.read(byteBuffer); + if (count == -1) throw new ConnectionClosedException(); + byteBuffer.flip(); + result.addAll(context.read(byteBuffer)); + byteBuffer.flip(); + } catch (ConnectionClosedException | IOException e) { + disconnect(); + } finally { + byteBuffer.clear(); + } + } + if (key.isValid() && key.isWritable()) { + try { + context.writeTo(socketChannel); + } catch (ConnectionClosedException | IOException e) { + disconnect(); + } + } + iterator.remove(); + } + + return result; + } + + + +} diff --git a/src/main/java/com/github/nizshee/shared/ConnectionContext.java b/src/main/java/com/github/nizshee/shared/ConnectionContext.java new file mode 100644 index 0000000..2b7657d --- /dev/null +++ b/src/main/java/com/github/nizshee/shared/ConnectionContext.java @@ -0,0 +1,108 @@ +package com.github.nizshee.shared; + + +import com.github.nizshee.shared.exception.ConnectionClosedException; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.GatheringByteChannel; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +@SuppressWarnings("all") +public class ConnectionContext { + private LinkedList toWrite; + private Buffer current; + + public ConnectionContext() { + toWrite = new LinkedList<>(); + current = new Buffer(); + } + + public List read(ByteBuffer bb) { + LinkedList list = new LinkedList<>(); + while (bb.hasRemaining()) { + current.read(bb); + if (current.isComplete()) { + list.add(current.getBytes()); + current = new Buffer(); + } + } + return list; + } + + public boolean writeTo(GatheringByteChannel bc) throws IOException, ConnectionClosedException { + if (toWrite.isEmpty()) return true; + ByteBuffer[] bbs = new ByteBuffer[toWrite.size()]; + long count = bc.write(toWrite.toArray(bbs)); + if (count == -1) throw new ConnectionClosedException(); + Iterator i = toWrite.iterator(); + while (i.hasNext()) { + ByteBuffer bb = i.next(); + if (bb.hasRemaining()) { + return false; + } else { + i.remove(); + } + } + return toWrite.isEmpty(); + } + + public void write(byte[] bytes) { + byte[] w = new byte[bytes.length + 4]; + w[0] = (byte)(bytes.length >>> 24); + w[1] = (byte)(bytes.length >>> 16); + w[2] = (byte)(bytes.length >>> 8); + w[3] = (byte)(bytes.length); + System.arraycopy(bytes, 0, w, 4, bytes.length); + toWrite.add(ByteBuffer.wrap(w)); + } + + + private final class Buffer { + private short sizeCount; + private int size; + private byte[] bytes; + private int read; + + Buffer() { + this.size = 0; + this.sizeCount = 0; + this.bytes = null; + this.read = 0; + } + + boolean isComplete() { + return sizeCount > 3 && read == size; + } + + void read(ByteBuffer bb) { + while (bb.hasRemaining()) { + if (sizeCount < 4) { + int signedByte = bb.get() & 0xff; + if (sizeCount == 0) { + size += signedByte << 24; + } else if (sizeCount == 1) { + size += signedByte << 16; + } else if (sizeCount == 2) { + size += signedByte << 8; + } else if (sizeCount == 3) { + size += signedByte; + bytes = new byte[size]; + } + sizeCount += 1; + } else if (read < size) { + int toRead = Math.min(bb.remaining(), size - read); + bb.get(bytes, read, toRead); + read += toRead; + if (read == size) return; + } + } + } + + byte[] getBytes() { + return bytes; + } + } +} diff --git a/src/main/java/com/github/nizshee/shared/Server.java b/src/main/java/com/github/nizshee/shared/Server.java new file mode 100644 index 0000000..f46420e --- /dev/null +++ b/src/main/java/com/github/nizshee/shared/Server.java @@ -0,0 +1,115 @@ +package com.github.nizshee.shared; + + +import com.github.nizshee.shared.exception.ConnectionClosedException; +import com.github.nizshee.shared.exception.WrongDataException; +import com.github.nizshee.shared.procedure.Registry; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; + +public class Server { + + private ServerSocketChannel ssc; + private Selector selector; + private volatile boolean isRunning; + private final ByteBuffer byteBuffer; + private final Registry registry; + + public Server(Registry registry) throws IOException { + isRunning = false; + byteBuffer = ByteBuffer.allocate(2048); + this.registry = registry; + } + + public final void start(InetSocketAddress address) throws IOException { + + ssc = ServerSocketChannel.open(); + ssc.socket().bind(address); + ssc.configureBlocking(false); + selector = Selector.open(); + ssc.register(selector, SelectionKey.OP_ACCEPT); + + isRunning = true; + + } + + public void runNow() throws IOException { + if (!isRunning) throw new IOException("Server not running."); + + selector.selectNow(); + final Iterator iterator = selector.selectedKeys().iterator(); + + while (iterator.hasNext()) { + final SelectionKey key = iterator.next(); + + if (key.isAcceptable()) { + SocketChannel socketChannel = ssc.accept(); + socketChannel.configureBlocking(false); + SelectionKey sKey = socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE); + sKey.attach(new ConnectionContext()); + } + if (key.isValid() && key.isReadable()) { + ConnectionContext context = (ConnectionContext) key.attachment(); + SocketChannel sc = (SocketChannel) key.channel(); + + try { + long count = sc.read(byteBuffer); + if (count == -1) throw new ConnectionClosedException(); + byteBuffer.flip(); + List commands = context.read(byteBuffer); + byteBuffer.flip(); + handle(sc.socket().getInetAddress().getAddress(), context, commands); + } catch (ConnectionClosedException | IOException | WrongDataException e) { + e.printStackTrace(); + sc.close(); + } finally { + byteBuffer.clear(); + } + } + if (key.isValid() && key.isWritable()) { + ConnectionContext context = (ConnectionContext) key.attachment(); + SocketChannel sc = (SocketChannel) key.channel(); + try { + context.writeTo(sc); + } catch (ConnectionClosedException | IOException e) { + sc.close(); + e.printStackTrace(); + } + } + iterator.remove(); + } + } + + public final void stop() { + try { + selector.close(); + } catch (IOException ignore) { + } finally { + selector = null; + ssc = null; + isRunning = false; + } + + } + + private void handle(byte[] ip, ConnectionContext context, List commands) throws WrongDataException { + for (byte[] bytes: commands) { + Optional res = registry.executeDump(ip, bytes); + if (res.isPresent()) { + context.write(res.get()); + } else { + throw new WrongDataException("Wrong command."); + } + } + } + +} diff --git a/src/main/java/com/github/nizshee/shared/exception/ConnectionClosedException.java b/src/main/java/com/github/nizshee/shared/exception/ConnectionClosedException.java new file mode 100644 index 0000000..f074bd1 --- /dev/null +++ b/src/main/java/com/github/nizshee/shared/exception/ConnectionClosedException.java @@ -0,0 +1,8 @@ +package com.github.nizshee.shared.exception; + + +public class ConnectionClosedException extends Exception { + public ConnectionClosedException() { + super("Connection closed."); + } +} diff --git a/src/main/java/com/github/nizshee/shared/exception/WrongDataException.java b/src/main/java/com/github/nizshee/shared/exception/WrongDataException.java new file mode 100644 index 0000000..3872b9e --- /dev/null +++ b/src/main/java/com/github/nizshee/shared/exception/WrongDataException.java @@ -0,0 +1,8 @@ +package com.github.nizshee.shared.exception; + + +public class WrongDataException extends Exception { + public WrongDataException(String s) { + super(s); + } +} diff --git a/src/main/java/com/github/nizshee/shared/procedure/ProcedureRegistry.java b/src/main/java/com/github/nizshee/shared/procedure/ProcedureRegistry.java new file mode 100644 index 0000000..f04abdd --- /dev/null +++ b/src/main/java/com/github/nizshee/shared/procedure/ProcedureRegistry.java @@ -0,0 +1,55 @@ +package com.github.nizshee.shared.procedure; + + +import com.github.nizshee.shared.exception.WrongDataException; + + +import java.util.Map; +import java.util.Optional; + +@SuppressWarnings("all") +public abstract class ProcedureRegistry implements Registry { + + private final Keeper keeper; + + public ProcedureRegistry(Keeper keeper) { + this.keeper = keeper; + } + + @Override + public Optional executeDump(byte[] ip, byte[] bytes) { + try { + byte identifier = getIdentifier(bytes); + if (!procedures().containsKey(identifier)) { + return Optional.empty(); + } + RemoteProcedure remoteProcedure = procedures().get(identifier); + byte[] rawBytes = getRawData(bytes); + return Optional.of(remoteProcedure.executeDump(ip, keeper, rawBytes)); + } catch (WrongDataException e) { + e.printStackTrace(); + return Optional.empty(); + } + } + + protected abstract Map> procedures(); + + protected static byte[] addIdentifier(byte identifier, byte[] bytes) throws WrongDataException { + byte[] nBytes = new byte[bytes.length + 1]; + nBytes[0] = identifier; + System.arraycopy(bytes, 0, nBytes, 1, bytes.length); + return nBytes; + } + + private static byte getIdentifier(byte[] bytes) throws WrongDataException { + if (bytes.length < 1) throw new WrongDataException("Empty bytes"); + return bytes[0]; + } + + private static byte[] getRawData(byte[] bytes) throws WrongDataException { + if (bytes.length < 1) throw new WrongDataException("Empty bytes"); + byte[] nBytes = new byte[bytes.length - 1]; + System.arraycopy(bytes, 1, nBytes, 0, bytes.length - 1); + return nBytes; + } +} diff --git a/src/main/java/com/github/nizshee/shared/procedure/ProcedureWrapper.java b/src/main/java/com/github/nizshee/shared/procedure/ProcedureWrapper.java new file mode 100644 index 0000000..b7e66a3 --- /dev/null +++ b/src/main/java/com/github/nizshee/shared/procedure/ProcedureWrapper.java @@ -0,0 +1,24 @@ +package com.github.nizshee.shared.procedure; + + +import com.github.nizshee.shared.exception.WrongDataException; + +import static com.github.nizshee.shared.procedure.ProcedureRegistry.addIdentifier; + +public class ProcedureWrapper { + private final byte id; + private final RemoteProcedure procedure; + + public ProcedureWrapper(byte id, RemoteProcedure procedure) { + this.id = id; + this.procedure = procedure; + } + + public byte[] dump(Request request) throws WrongDataException { + return addIdentifier(id, procedure.request2Byes(request)); + } + + public Response restore(byte[] bytes) throws WrongDataException { + return procedure.bytes2Response(bytes); + } +} diff --git a/src/main/java/com/github/nizshee/shared/procedure/Registry.java b/src/main/java/com/github/nizshee/shared/procedure/Registry.java new file mode 100644 index 0000000..982c996 --- /dev/null +++ b/src/main/java/com/github/nizshee/shared/procedure/Registry.java @@ -0,0 +1,9 @@ +package com.github.nizshee.shared.procedure; + + +import java.util.Optional; + +public interface Registry { + + Optional executeDump(byte[] ip, byte[] bytes); +} diff --git a/src/main/java/com/github/nizshee/shared/procedure/RemoteProcedure.java b/src/main/java/com/github/nizshee/shared/procedure/RemoteProcedure.java new file mode 100644 index 0000000..d0cd995 --- /dev/null +++ b/src/main/java/com/github/nizshee/shared/procedure/RemoteProcedure.java @@ -0,0 +1,28 @@ +package com.github.nizshee.shared.procedure; + + +import com.github.nizshee.shared.util.Dumper; +import com.github.nizshee.shared.exception.WrongDataException; + +public abstract class RemoteProcedure { + + public final byte[] executeDump(byte[] ip, Keeper keeper, byte[] bytes) throws WrongDataException { + Request f = request().restore(bytes); + Response t = execute(ip, keeper, f); + return response().dump(t); + } + + public final byte[] request2Byes(Request request) throws WrongDataException { + return request().dump(request); + } + + public final Response bytes2Response(byte[] bytes) throws WrongDataException { + return response().restore(bytes); + } + + protected abstract Dumper request(); + + protected abstract Dumper response(); + + protected abstract Response execute(byte[] ip, Keeper keeper, Request request); +} diff --git a/src/main/java/com/github/nizshee/shared/util/BooleanDumper.java b/src/main/java/com/github/nizshee/shared/util/BooleanDumper.java new file mode 100644 index 0000000..0e44c33 --- /dev/null +++ b/src/main/java/com/github/nizshee/shared/util/BooleanDumper.java @@ -0,0 +1,21 @@ +package com.github.nizshee.shared.util; + + +import com.github.nizshee.shared.exception.WrongDataException; + +public class BooleanDumper implements Dumper { + @Override + public byte[] dump(Boolean aBoolean) throws WrongDataException { + byte[] bytes = new byte[1]; + bytes[0] = aBoolean ? (byte) 1 : (byte) 0; + return bytes; + } + + @Override + public Boolean restore(byte[] bytes) throws WrongDataException { + if (bytes.length < 1) throw new WrongDataException("Can't read Boolean."); + if (bytes[0] == (short) 0) return false; + if (bytes[0] == (short) 1) return true; + throw new WrongDataException("Can't read Boolean."); + } +} diff --git a/src/main/java/com/github/nizshee/shared/util/Dumper.java b/src/main/java/com/github/nizshee/shared/util/Dumper.java new file mode 100644 index 0000000..3cdb7bd --- /dev/null +++ b/src/main/java/com/github/nizshee/shared/util/Dumper.java @@ -0,0 +1,11 @@ +package com.github.nizshee.shared.util; + + +import com.github.nizshee.shared.exception.WrongDataException; + +public interface Dumper { + + byte[] dump(Value value) throws WrongDataException; + + Value restore(byte[] bytes) throws WrongDataException; +} diff --git a/src/main/java/com/github/nizshee/shared/util/IntDumper.java b/src/main/java/com/github/nizshee/shared/util/IntDumper.java new file mode 100644 index 0000000..8a01274 --- /dev/null +++ b/src/main/java/com/github/nizshee/shared/util/IntDumper.java @@ -0,0 +1,22 @@ +package com.github.nizshee.shared.util; + + +import com.github.nizshee.shared.exception.WrongDataException; + +import java.nio.ByteBuffer; + +public class IntDumper implements Dumper { + @Override + public byte[] dump(Integer integer) throws WrongDataException { + ByteBuffer bb = ByteBuffer.allocate(4); + bb.putInt(integer); + return bb.array(); + } + + @Override + public Integer restore(byte[] bytes) throws WrongDataException { + if (bytes.length < 4) throw new WrongDataException("Can't read int"); + ByteBuffer bb = ByteBuffer.wrap(bytes); + return bb.getInt(); + } +} diff --git a/src/test/java/com/github/nizshee/client/ClientKeeperImplTest.java b/src/test/java/com/github/nizshee/client/ClientKeeperImplTest.java new file mode 100644 index 0000000..c6f20ca --- /dev/null +++ b/src/test/java/com/github/nizshee/client/ClientKeeperImplTest.java @@ -0,0 +1,44 @@ +package com.github.nizshee.client; + + +import static org.junit.Assert.*; + +import com.github.nizshee.client.util.FilePart; +import com.github.nizshee.server.util.FileDescriptor; +import org.junit.Test; + +import java.io.File; +import java.util.Random; + +public class ClientKeeperImplTest { + + @Test + @SuppressWarnings("all") + public void putGetTest() throws Exception { + String fileName = "test_file"; + File file = new File(fileName); + ClientKeeperImpl clientKeeper = new ClientKeeperImpl(); + byte[] data1 = new byte[ClientKeeperImpl.PART_SIZE]; + byte[] data2 = new byte[ClientKeeperImpl.PART_SIZE / 2]; + long totalSize = ClientKeeperImpl.PART_SIZE + (ClientKeeperImpl.PART_SIZE / 2); + new Random().nextBytes(data1); + new Random().nextBytes(data2); + + assertFalse(file.exists()); + + clientKeeper.get(new FileDescriptor(1, fileName, totalSize)); + + assertTrue(file.exists()); + assertEquals(totalSize, file.length()); + + clientKeeper.put(new FilePart(1, 0), data1); + assertArrayEquals(data1, clientKeeper.get(new FilePart(1, 0))); + + clientKeeper.put(new FilePart(1, 1), data2); + assertArrayEquals(data2, clientKeeper.get(new FilePart(1, 1))); + + file.delete(); + } + + +} diff --git a/src/test/java/com/github/nizshee/client/procedure/GetProcedureTest.java b/src/test/java/com/github/nizshee/client/procedure/GetProcedureTest.java new file mode 100644 index 0000000..cd42822 --- /dev/null +++ b/src/test/java/com/github/nizshee/client/procedure/GetProcedureTest.java @@ -0,0 +1,22 @@ +package com.github.nizshee.client.procedure; + + +import com.github.nizshee.client.ClientKeeper; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class GetProcedureTest { + + @Test + public void executeDumpTest() throws Exception { + ClientKeeper keeper = new TestKeeper(); + GetProcedure procedure = new GetProcedure(); + + byte[] from = procedure.request2Byes(TestKeeper.FILE_PART); + byte[] to = procedure.executeDump(new byte[0], keeper, from); + byte[] bytes = procedure.bytes2Response(to); + + assertArrayEquals(TestKeeper.BYTES, bytes); + } +} \ No newline at end of file diff --git a/src/test/java/com/github/nizshee/client/procedure/StatProcedureTest.java b/src/test/java/com/github/nizshee/client/procedure/StatProcedureTest.java new file mode 100644 index 0000000..d81f363 --- /dev/null +++ b/src/test/java/com/github/nizshee/client/procedure/StatProcedureTest.java @@ -0,0 +1,24 @@ +package com.github.nizshee.client.procedure; + + +import com.github.nizshee.client.ClientKeeper; +import org.junit.Test; + +import java.util.Set; + +import static org.junit.Assert.*; + +public class StatProcedureTest { + + @Test + public void executeDumpTest() throws Exception { + ClientKeeper keeper = new TestKeeper(); + StatProcedure procedure = new StatProcedure(); + + byte[] from = procedure.request2Byes(TestKeeper.IDENTIFIER); + byte[] to = procedure.executeDump(new byte[0], keeper, from); + Set stat = procedure.bytes2Response(to); + + assertEquals(TestKeeper.STAT, stat); + } +} \ No newline at end of file diff --git a/src/test/java/com/github/nizshee/client/procedure/TestKeeper.java b/src/test/java/com/github/nizshee/client/procedure/TestKeeper.java new file mode 100644 index 0000000..032c784 --- /dev/null +++ b/src/test/java/com/github/nizshee/client/procedure/TestKeeper.java @@ -0,0 +1,29 @@ +package com.github.nizshee.client.procedure; + + +import com.github.nizshee.client.ClientKeeper; +import com.github.nizshee.client.util.FilePart; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class TestKeeper implements ClientKeeper { + + public static final int IDENTIFIER = 1942; + public static final Set STAT = new HashSet<>(Arrays.asList(8, 1, 7, 2, 6, 3, 5, 4)); + public static final FilePart FILE_PART = new FilePart(123, 654); + public static final byte[] BYTES = {0, 9, 8, 7, 6, 5, 4, 3, 2, 1}; + + @Override + public Set stat(int identifier) { + if (identifier != IDENTIFIER) throw new RuntimeException("Not equals."); + return STAT; + } + + @Override + public byte[] get(FilePart part) { + if (!part.equals(FILE_PART)) throw new RuntimeException("Not equals."); + return BYTES; + } +} diff --git a/src/test/java/com/github/nizshee/server/ServerKeeperImplTest.java b/src/test/java/com/github/nizshee/server/ServerKeeperImplTest.java new file mode 100644 index 0000000..68a6a81 --- /dev/null +++ b/src/test/java/com/github/nizshee/server/ServerKeeperImplTest.java @@ -0,0 +1,68 @@ +package com.github.nizshee.server; + + +import com.github.nizshee.server.util.ClientItem; +import com.github.nizshee.server.util.FileDescriptor; +import com.github.nizshee.server.util.UpdateItem; +import com.github.nizshee.server.util.UploadItem; +import org.junit.Test; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.*; + +public class ServerKeeperImplTest { + + @Test + public void listTest() throws Exception { + ServerKeeperImpl serverKeeper = new ServerKeeperImpl(); + + int i0 = serverKeeper.upload(new UploadItem("0", 0)); + int i1 = serverKeeper.upload(new UploadItem("1", 1)); + int i2 = serverKeeper.upload(new UploadItem("2", 2)); + int i3 = serverKeeper.upload(new UploadItem("3", 3)); + + List res = Arrays.asList(new FileDescriptor(i0, "0", 0), new FileDescriptor(i1, "1", 1), + new FileDescriptor(i2, "2", 2), new FileDescriptor(i3, "3", 3)); + + assertEquals(res, serverKeeper.list()); + } + + @Test + public void clearOldTest() throws Exception { + ServerKeeperImpl serverKeeper = new ServerKeeperImpl(); + + byte[] port1 = {1, 2, 3, 4}; + byte[] port2 = {4, 3, 2, 1}; + int i0 = serverKeeper.upload(new UploadItem("0", 0)); + + serverKeeper.update(port1, new UpdateItem((short) 1, Collections.singletonList(i0))); + + Thread.sleep(1000); + + serverKeeper.update(port2, new UpdateItem((short) 2, Collections.singletonList(i0))); + + serverKeeper.clearOld(LocalDateTime.now().plusMinutes(4)); + + assertEquals( + Arrays.asList( + new ClientItem((byte) 1, (byte) 2, (byte) 3, (byte) 4, (short) 1), + new ClientItem((byte) 4, (byte) 3, (byte) 2, (byte) 1, (short) 2) + ), serverKeeper.sources(i0) + ); + + serverKeeper.clearOld(LocalDateTime.now().plusMinutes(4).plusSeconds(59)); + + assertEquals( + Collections.singletonList(new ClientItem((byte) 4, (byte) 3, (byte) 2, (byte) 1, (short) 2)), + serverKeeper.sources(i0) + ); + + serverKeeper.clearOld(LocalDateTime.now().plusMinutes(5)); + + assertEquals(Collections.emptyList(), serverKeeper.sources(i0)); + } +} diff --git a/src/test/java/com/github/nizshee/server/procedure/ListProcedureTest.java b/src/test/java/com/github/nizshee/server/procedure/ListProcedureTest.java new file mode 100644 index 0000000..d75011b --- /dev/null +++ b/src/test/java/com/github/nizshee/server/procedure/ListProcedureTest.java @@ -0,0 +1,24 @@ +package com.github.nizshee.server.procedure; + + +import com.github.nizshee.server.util.FileDescriptor; +import com.github.nizshee.server.ServerKeeper; +import org.junit.Test; + +import static org.junit.Assert.*; +import java.util.List; + +public class ListProcedureTest { + + @Test + public void executeDumpTest() throws Exception { + ServerKeeper keeper = new TestKeeper(); + ListProcedure procedure = new ListProcedure(); + + byte[] from = procedure.request2Byes(new Object()); + byte[] to = procedure.executeDump(new byte[0], keeper, from); + List list = procedure.bytes2Response(to); + + assertEquals(TestKeeper.LIST, list); + } +} diff --git a/src/test/java/com/github/nizshee/server/procedure/SourcesProcedureTest.java b/src/test/java/com/github/nizshee/server/procedure/SourcesProcedureTest.java new file mode 100644 index 0000000..c54dd73 --- /dev/null +++ b/src/test/java/com/github/nizshee/server/procedure/SourcesProcedureTest.java @@ -0,0 +1,24 @@ +package com.github.nizshee.server.procedure; + + +import com.github.nizshee.server.util.ClientItem; +import com.github.nizshee.server.ServerKeeper; +import org.junit.Test; + +import static org.junit.Assert.*; +import java.util.List; + +public class SourcesProcedureTest { + + @Test + public void executeDumpTest() throws Exception { + ServerKeeper keeper = new TestKeeper(); + SourcesProcedure procedure = new SourcesProcedure(); + + byte[] from = procedure.request2Byes(TestKeeper.IDENTIFIER); + byte[] to = procedure.executeDump(new byte[0], keeper, from); + List list = procedure.bytes2Response(to); + + assertEquals(TestKeeper.SOURCES, list); + } +} diff --git a/src/test/java/com/github/nizshee/server/procedure/TestKeeper.java b/src/test/java/com/github/nizshee/server/procedure/TestKeeper.java new file mode 100644 index 0000000..28846a0 --- /dev/null +++ b/src/test/java/com/github/nizshee/server/procedure/TestKeeper.java @@ -0,0 +1,47 @@ +package com.github.nizshee.server.procedure; + + +import com.github.nizshee.server.util.ClientItem; +import com.github.nizshee.server.util.FileDescriptor; +import com.github.nizshee.server.ServerKeeper; +import com.github.nizshee.server.util.UpdateItem; +import com.github.nizshee.server.util.UploadItem; + +import java.util.Arrays; +import java.util.List; + +public class TestKeeper implements ServerKeeper { + + public final static List LIST = Arrays.asList(new FileDescriptor(0, "0", 0), new FileDescriptor(1, "1", 1000), + new FileDescriptor(2, "2", 2000)); + public final static int IDENTIFIER = 1033; + public final static UploadItem UPLOAD_ITEM = new UploadItem("123", 123456); + public final static List SOURCES = Arrays.asList( + new ClientItem((byte) 1, (byte) 2, (byte) 3, (byte) 4, (short) 2), + new ClientItem((byte) 4, (byte) 3, (byte) 2, (byte) 1, (short) 4), + new ClientItem((byte) 12, (byte) 21, (byte) 11, (byte) 22, (short) 6)); + public final static UpdateItem UPDATE_ITEM = new UpdateItem((short) 1423, Arrays.asList(1, 2, 3, 4, 5)); + + @Override + public List list() { + return LIST; + } + + @Override + public int upload(UploadItem item) { + if (!item.equals(UPLOAD_ITEM)) throw new RuntimeException("Not equals"); + return IDENTIFIER; + } + + @Override + public List sources(int identifier) { + if (identifier != IDENTIFIER) throw new RuntimeException("Not equals"); + return SOURCES; + } + + @Override + public boolean update(byte[] ip, UpdateItem item) { + if (!item.equals(UPDATE_ITEM)) throw new RuntimeException("Not equals"); + return true; + } +} diff --git a/src/test/java/com/github/nizshee/server/procedure/UpdateProcedureTest.java b/src/test/java/com/github/nizshee/server/procedure/UpdateProcedureTest.java new file mode 100644 index 0000000..9df6e44 --- /dev/null +++ b/src/test/java/com/github/nizshee/server/procedure/UpdateProcedureTest.java @@ -0,0 +1,23 @@ +package com.github.nizshee.server.procedure; + + +import com.github.nizshee.server.ServerKeeper; +import org.junit.Test; + +import static org.junit.Assert.*; + + +public class UpdateProcedureTest { + + @Test + public void executeDumpTest() throws Exception { + ServerKeeper keeper = new TestKeeper(); + UpdateProcedure procedure = new UpdateProcedure(); + + byte[] from = procedure.request2Byes(TestKeeper.UPDATE_ITEM); + byte[] to = procedure.executeDump(new byte[0], keeper, from); + boolean res = procedure.bytes2Response(to); + + assertTrue(res); + } +} diff --git a/src/test/java/com/github/nizshee/server/procedure/UploadProcedureTest.java b/src/test/java/com/github/nizshee/server/procedure/UploadProcedureTest.java new file mode 100644 index 0000000..de2d14c --- /dev/null +++ b/src/test/java/com/github/nizshee/server/procedure/UploadProcedureTest.java @@ -0,0 +1,23 @@ +package com.github.nizshee.server.procedure; + + +import com.github.nizshee.server.ServerKeeper; +import org.junit.Test; + +import static org.junit.Assert.*; + +public class UploadProcedureTest { + + @Test + public void executeDumpTest() throws Exception { + ServerKeeper keeper = new TestKeeper(); + UploadProcedure procedure = new UploadProcedure(); + + byte[] from = procedure.request2Byes(TestKeeper.UPLOAD_ITEM); + byte[] to = procedure.executeDump(new byte[0], keeper, from); + int identifier = procedure.bytes2Response(to); + + assertEquals(TestKeeper.IDENTIFIER, identifier); + } + +} diff --git a/src/test/java/com/github/nizshee/shared/ClientServerTest.java b/src/test/java/com/github/nizshee/shared/ClientServerTest.java new file mode 100644 index 0000000..b702568 --- /dev/null +++ b/src/test/java/com/github/nizshee/shared/ClientServerTest.java @@ -0,0 +1,88 @@ +package com.github.nizshee.shared; + + +import com.github.nizshee.shared.procedure.Registry; +import org.junit.Test; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.List; +import java.util.Optional; + +import static org.junit.Assert.*; + +public class ClientServerTest { + + private static Registry echo = (ip, bytes) -> { + byte[] local = {127, 0, 0, 1}; + assertArrayEquals(local, ip); + return Optional.of(bytes); + }; + + @Test + public void clientServerTest() throws Exception { + byte[] data = {1, 2, 3, 4, 5, 6, 7, 8}; + InetSocketAddress address = new InetSocketAddress("localhost", 8083); + + Server simpleServer = new Server(echo); + + simpleServer.start(address); + Client client = new Client(); + client.connect(address); + client.write(data); + + List result = retrieveData(simpleServer, client); + + assertArrayEquals(data, result.get(0)); + + client.disconnect(); + simpleServer.stop(); + } + + @Test + public void clientServerMultipleTest() throws Exception { + byte[] data1 = {1, 2, 3, 4, 5, 6, 7, 8}; + byte[] data2 = {4, 3, 2, 1}; + + InetSocketAddress address = new InetSocketAddress("localhost", 8084); + + Server simpleServer = new Server(echo); + + simpleServer.start(address); + Client client = new Client(); + client.connect(address); + + client.write(data1); + client.write(data2); + + List result = retrieveData(simpleServer, client); + + assertArrayEquals(data1, result.get(0)); + assertArrayEquals(data2, result.get(1)); + + client.write(data2); + client.write(data1); + + result = retrieveData(simpleServer, client); + + assertArrayEquals(data1, result.get(1)); + assertArrayEquals(data2, result.get(0)); + + client.disconnect(); + simpleServer.stop(); + } + + private static List retrieveData(Server server, Client client) + throws IOException, InterruptedException { + client.runNow(); // write data + + Thread.sleep(100); + + server.runNow(); // read data + server.runNow(); // write data + + Thread.sleep(100); + + return client.runNow(); // read data + } +} diff --git a/src/test/java/com/github/nizshee/shared/ConnectionContextTest.java b/src/test/java/com/github/nizshee/shared/ConnectionContextTest.java new file mode 100644 index 0000000..559751e --- /dev/null +++ b/src/test/java/com/github/nizshee/shared/ConnectionContextTest.java @@ -0,0 +1,115 @@ +package com.github.nizshee.shared; + +import org.junit.Test; + +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.List; + +import static org.junit.Assert.*; + + +public class ConnectionContextTest { + + @Test + @SuppressWarnings("all") + public void writeTest() throws Exception { + + ConnectionContext connectionContext = new ConnectionContext(); + + byte[] message = {1, 2, 3, 4}; + byte[] buffer = new byte[100]; + byte[] aMessage = new byte[4]; + connectionContext.write(message); + + ServerSocketChannel ssc = ServerSocketChannel.open(); + ssc.socket().bind(new InetSocketAddress("127.0.0.1", 8082)); + + Socket s = new Socket("127.0.0.1", 8082); + + SocketChannel sc = ssc.accept(); + + while (!connectionContext.writeTo(sc)); + s.getInputStream().read(buffer); + System.arraycopy(buffer, 4, aMessage, 0, 4); + assertArrayEquals(message, aMessage); + + s.close(); + sc.close(); + ssc.close(); + } + + @Test + @SuppressWarnings("all") + public void writeMultipleTest() throws Exception { + + ConnectionContext connectionContext = new ConnectionContext(); + + byte[] message = {1, 2, 3, 4}; + byte[] message1 = {4, 3, 2, 1}; + byte[] length = {0, 0, 0, 4}; + byte[] buffer = new byte[100]; + byte[] result = new byte[4]; + connectionContext.write(message); + connectionContext.write(message1); + + ServerSocketChannel ssc = ServerSocketChannel.open(); + ssc.socket().bind(new InetSocketAddress("127.0.0.1", 8081)); + + Socket s = new Socket("127.0.0.1", 8081); + + SocketChannel sc = ssc.accept(); + + while (!connectionContext.writeTo(sc)); + s.getInputStream().read(buffer); + + System.arraycopy(buffer, 0, result, 0, 4); + assertArrayEquals(length, result); + System.arraycopy(buffer, 4, result, 0, 4); + assertArrayEquals(message, result); + System.arraycopy(buffer, 8, result, 0, 4); + assertArrayEquals(length, result); + System.arraycopy(buffer, 12, result, 0, 4); + assertArrayEquals(message1, result); + + s.close(); + sc.close(); + ssc.close(); + } + + @Test + public void readTest() throws Exception { + + byte[] data = {0, 0, 0, 4, 1, 2, 3, 4}; + byte[] result = {1, 2, 3, 4}; + + ByteBuffer bb = ByteBuffer.wrap(data); + + ConnectionContext connectionContext = new ConnectionContext(); + List list = connectionContext.read(bb); + assertArrayEquals(result, list.get(0)); + } + + @Test + public void readMultipleTest() throws Exception { + + byte[] data1 = {0, 0, 0, 4, 1, 2, 3, 4, 0, 0}; + byte[] data2 = {0, 4, 4, 3, 2, 1, 0, 0, 0, 4, 3, 4, 2, 1}; + byte[] result1 = {1, 2, 3, 4}; + byte[] result2 = {4, 3, 2, 1}; + byte[] result3 = {3, 4, 2, 1}; + + ConnectionContext connectionContext = new ConnectionContext(); + + List list = connectionContext.read(ByteBuffer.wrap(data1)); + assertArrayEquals(result1, list.get(0)); + + list = connectionContext.read(ByteBuffer.wrap(data2)); + assertArrayEquals(result2, list.get(0)); + assertArrayEquals(result3, list.get(1)); + } + +}