├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── clean.sh ├── make.sh ├── run_client.sh ├── run_relay.sh ├── run_server.sh ├── run_simple_gui_client.sh ├── src └── codeu │ └── chat │ ├── ClientMain.java │ ├── RelayMain.java │ ├── ServerMain.java │ ├── SimpleGuiClientMain.java │ ├── client │ ├── ClientContext.java │ ├── ClientConversation.java │ ├── ClientMessage.java │ ├── ClientUser.java │ ├── Controller.java │ ├── View.java │ ├── commandline │ │ ├── Chat.java │ │ └── ListNavigator.java │ └── simplegui │ │ ├── ChatSimpleGui.java │ │ ├── ConversationPanel.java │ │ ├── MessagePanel.java │ │ └── UserPanel.java │ ├── common │ ├── BasicController.java │ ├── BasicView.java │ ├── Conversation.java │ ├── ConversationSummary.java │ ├── LinearUuidGenerator.java │ ├── ListViewable.java │ ├── LogicalView.java │ ├── Message.java │ ├── NetworkCode.java │ ├── RandomUuidGenerator.java │ ├── RawController.java │ ├── Relay.java │ ├── Secret.java │ ├── SinglesView.java │ └── User.java │ ├── relay │ ├── Server.java │ └── ServerFrontEnd.java │ ├── server │ ├── Controller.java │ ├── Model.java │ ├── NoOpRelay.java │ ├── RemoteRelay.java │ ├── Server.java │ └── View.java │ └── util │ ├── Logger.java │ ├── Method.java │ ├── RemoteAddress.java │ ├── Serializer.java │ ├── Serializers.java │ ├── Time.java │ ├── Timeline.java │ ├── Uuid.java │ ├── connections │ ├── ClientConnectionSource.java │ ├── Connection.java │ ├── ConnectionSource.java │ └── ServerConnectionSource.java │ └── store │ ├── LinkIterable.java │ ├── LinkIterator.java │ ├── Store.java │ ├── StoreAccessor.java │ └── StoreLink.java ├── teams ├── test.sh ├── test └── codeu │ └── chat │ ├── TestRunner.java │ ├── common │ └── SecretTest.java │ ├── relay │ └── ServerTest.java │ ├── server │ ├── BasicControllerTest.java │ └── RawControllerTest.java │ └── util │ ├── TimeTest.java │ ├── UuidTest.java │ └── store │ └── StoreTest.java └── third_party ├── LICENSE ├── hamcrest-core-1.3.jar ├── hamcrest-core.jar ├── junit4-4.11.jar └── junit4.jar /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | sudo: false 4 | 5 | jdk: 6 | - openjdk8 7 | 8 | os: 9 | - linux 10 | 11 | 12 | script: 13 | - ./clean.sh 14 | - ./make.sh 15 | - ./test.sh 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | We'd love to accept your patches and contributions to this project. Just remember 4 | this project is designed as a playground for people to develop their skills. As 5 | such your proposed changes should keep in mind this project's intended purpose as 6 | platform for skill development. If you simply want to develop cool features based 7 | on this project, that's great - just fork this project and go wild. 8 | 9 | ## Contributor License Agreement 10 | 11 | Contributions to this project must be accompanied by a Contributor License 12 | Agreement. You (or your employer) retain the copyright to your contribution, 13 | this simply gives us permission to use and redistribute your contributions as 14 | part of the project. Head over to to see 15 | your current agreements on file or to sign a new one. 16 | 17 | You generally only need to submit a CLA once, so if you've already submitted one 18 | (even if it was for a different project), you probably don't need to do it 19 | again. 20 | 21 | ## Code reviews 22 | 23 | All submissions, including submissions by project members, require review. We 24 | use GitHub pull requests for this purpose. Consult [GitHub Help] for more 25 | information on using pull requests. 26 | 27 | [GitHub Help]: https://help.github.com/articles/about-pull-requests/ 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # CODEU CHAT SERVER | README 3 | 4 | 5 | ## DISCLAIMER 6 | 7 | CODEU is a program created by Google to develop the skills of future software 8 | engineers. This project is not an official Google Product. This project is a 9 | playground for those looking to develop their coding and software engineering 10 | skills. 11 | 12 | 13 | ## ENVIRONMENT 14 | 15 | All instructions here are relative to a LINUX environment. There will be some 16 | differences if you are working on a non-LINUX system. We will not support any 17 | other development environment. 18 | 19 | This project was built using JAVA 7. It is recommended that you install 20 | JAVA 7 when working with this project. 21 | 22 | 23 | ## GETTING STARTED 24 | 25 | 1. To build the project: 26 | ``` 27 | $ sh clean.sh 28 | $ sh make.sh 29 | ``` 30 | 31 | 1. To test the project: 32 | ``` 33 | $ sh test.sh 34 | ``` 35 | 36 | 1. To run the project you will need to run both the client and the server. Run 37 | the following two commands in separate shells: 38 | 39 | ``` 40 | $ sh run_server.sh 41 | $ sh run_client.sh 42 | ``` 43 | 44 | You must specify the following startup arguments for `run_server.sh: 45 | + `` and ``: a numeric id for your team, and a secret 46 | code, which are used to authenticate your server with the Relay server. 47 | You can specify any integer value for ``, and a value expressed 48 | in hexadecimal format (using numbers `0-9` and letters in the range 49 | `A-F`) for `` when you launch the server in your local setup 50 | since it will not connect to the Relay server. 51 | + ``: the TCP port that your Server will listen on for connections 52 | from the Client. You can use any value between 1024 and 65535, as long as 53 | there is no other service currently listening on that port in your 54 | system. The server will return an error: 55 | 56 | ``` 57 | java.net.BindException: Address already in use (Bind failed) 58 | ``` 59 | 60 | if the port is already in use. 61 | + ``: the path where you want the server to save data between 62 | runs. 63 | 64 | The startup arguments for `run_client.sh` are the following: 65 | + ``: the hostname or IP address of the computer on which the server 66 | is listening. If you are running server and client on the same computer, 67 | you can use `localhost` here. 68 | + ``: the port on which your server is listening. Must be the same 69 | port number you have specified when you launched `run_server.sh`. 70 | 71 | All running images write informational and exceptional events to log files. 72 | The default setting for log messages is "INFO". You may change this to get 73 | more or fewer messages, and you are encouraged to add more LOG statements 74 | to the code. The logging is implemented in `codeu.chat.util.Logger.java`, 75 | which is built on top of `java.util.logging.Logger`, which you can refer to 76 | for more information. 77 | 78 | In addition to your team's client and server, the project also includes a 79 | Relay Server and a script that runs it (`run_relay.sh`). 80 | This is not needed to get started with the project. 81 | 82 | 83 | ## Finding your way around the project 84 | 85 | All the source files (except test-related source files) are in 86 | `./src/codeu/chat`. The test source files are in `./test/codeu/chat`. If you 87 | use the supplied scripts to build the project, the `.class` files will be placed 88 | in `./bin`. There is a `./third_party` directory that holds the jar files for 89 | JUnit (a Java testing framework). Your environment may or may not already have 90 | this installed. The supplied scripts use the version in `./third_party`. 91 | 92 | Finally, there are some high-level design documents in the project Wiki. Please 93 | review them as they can help you find your way around the sources. 94 | 95 | 96 | 97 | ## Source Directories 98 | 99 | The major project components have been separated into their own packages. The 100 | main packages/directories under `src/codeu/chat` are: 101 | 102 | ### codeu.chat.client 103 | 104 | Classes for building the two clients (`codeu.chat.ClientMain` and 105 | `codeu.chat.SimpleGuiClientMain`). 106 | 107 | ### codeu.chat.server 108 | 109 | Classes for building the server (`codeu.chat.ServerMain`). 110 | 111 | ### codeu.chat.relay 112 | 113 | Classes for building the Relay Server (`codeu.chat.RelayMain`). The Relay Server 114 | is not needed to get started. 115 | 116 | ### codeu.chat.common 117 | 118 | Classes that are shared by the clients and servers. 119 | 120 | ### codeu.chat.util 121 | 122 | Some basic infrastructure classes used throughout the project. 123 | -------------------------------------------------------------------------------- /clean.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2017 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -e 18 | 19 | rm -rf ./bin/* 20 | -------------------------------------------------------------------------------- /make.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2017 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -e 18 | 19 | mkdir -p bin 20 | 21 | javac -Xlint $(find * | grep "\\.java$") -d ./bin -sourcepath ./src -cp ./third_party/junit4.jar:./bin 22 | javac -Xlint $(find * | grep "\\.java$") -d ./bin -sourcepath ./test -cp ./third_party/junit4.jar:./bin 23 | -------------------------------------------------------------------------------- /run_client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2017 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | HOST="$1" 18 | PORT="$2" 19 | 20 | if [[ "${HOST}" == "" || "${PORT}" == "" ]] ; then 21 | echo 'usage: ' 22 | exit 1 23 | fi 24 | 25 | cd './bin' 26 | java codeu.chat.ClientMain "${HOST}@${PORT}" 27 | -------------------------------------------------------------------------------- /run_relay.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2017 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | PORT="$1" 18 | TEAM_FILE="$2" 19 | 20 | if [[ "${PORT}" == "" || "${TEAM_FILE}" == "" ]] ; then 21 | echo 'usage: ' 22 | exit 1 23 | fi 24 | 25 | if [ ! -f "${TEAM_FILE}" ] ; then 26 | echo "No file at ${TEAM_FILE}" 27 | exit 1 28 | fi 29 | 30 | cd './bin' 31 | java codeu.chat.RelayMain "${PORT}" "${TEAM_FILE}" 32 | -------------------------------------------------------------------------------- /run_server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2017 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | TEAM_ID="$1" 18 | TEAM_SECRET="$2" 19 | PORT="$3" 20 | PERSISTENT_DIR="$4" 21 | RELAY_ADDRESS="$5" 22 | 23 | if [[ "${TEAM_ID}" == "" || "${TEAM_SECRET}" == "" || "${PORT}" == "" || "${PERSISTENT_DIR}" == "" ]] ; then 24 | echo 'usage: [RELAY ADDRESS]' 25 | echo '' 26 | echo 'TEAM ID : The id registered with the relay server. If you are' 27 | echo ' not connecting to a relay server, use "100".' 28 | echo 'TEAM SECRET : The secret registerd with the relay server. If you are' 29 | echo ' not connecting to a relay server, use "ABABAB".' 30 | echo 'PORT : The port that the server will listen to for incoming ' 31 | echo ' connections. This can be anything from 1024 to 65535.' 32 | echo 'PERSISTENT DIR : The directory where the server can save data that will' 33 | echo ' exists between runs.' 34 | echo 'RELAY ADDRESS : This value is optional. If you want to connect to a ' 35 | echo ' relay server, the address must be IP@PORT where IP is' 36 | echo ' the ip address of the relay server and PORT is the port' 37 | echo ' the relay server is listing on.' 38 | echo '' 39 | exit 1 40 | fi 41 | 42 | 43 | cd './bin' 44 | if [ "${RELAY_ADDRESS}" == "" ] ; then 45 | java codeu.chat.ServerMain \ 46 | "${TEAM_ID}" \ 47 | "${TEAM_SECRET}" \ 48 | "${PORT}" \ 49 | "${PERSISTENT_DIR}" 50 | else 51 | java codeu.chat.ServerMain \ 52 | "${TEAM_ID}" \ 53 | "${TEAM_SECRET}" \ 54 | "${PORT}" \ 55 | "${PERSISTENT_DIR}" \ 56 | "${RELAY_ADDRESS}" 57 | fi 58 | -------------------------------------------------------------------------------- /run_simple_gui_client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Copyright 2017 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | LOCAL_MACHINE="localhost@2007" 18 | 19 | cd './bin' 20 | 21 | java codeu.chat.SimpleGuiClientMain "${LOCAL_MACHINE}" 22 | -------------------------------------------------------------------------------- /src/codeu/chat/ClientMain.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codeu.chat; 16 | 17 | import java.io.IOException; 18 | import java.util.Scanner; 19 | 20 | import codeu.chat.client.commandline.Chat; 21 | import codeu.chat.client.Controller; 22 | import codeu.chat.client.View; 23 | import codeu.chat.util.Logger; 24 | import codeu.chat.util.RemoteAddress; 25 | import codeu.chat.util.connections.ClientConnectionSource; 26 | import codeu.chat.util.connections.ConnectionSource; 27 | 28 | final class ClientMain { 29 | 30 | private static final Logger.Log LOG = Logger.newLog(ClientMain.class); 31 | 32 | public static void main(String [] args) { 33 | 34 | try { 35 | Logger.enableFileOutput("chat_client_log.log"); 36 | } catch (IOException ex) { 37 | LOG.error(ex, "Failed to set logger to write to file"); 38 | } 39 | 40 | LOG.info("============================= START OF LOG ============================="); 41 | 42 | LOG.info("Starting chat client..."); 43 | 44 | final RemoteAddress address = RemoteAddress.parse(args[0]); 45 | 46 | final ConnectionSource source = new ClientConnectionSource(address.host, address.port); 47 | final Controller controller = new Controller(source); 48 | final View view = new View(source); 49 | 50 | LOG.info("Creating client..."); 51 | final Chat chat = new Chat(controller, view); 52 | 53 | LOG.info("Created client"); 54 | 55 | final Scanner input = new Scanner(System.in); 56 | 57 | while (chat.handleCommand(input)) { 58 | // everything is done in "run" 59 | } 60 | 61 | input.close(); 62 | 63 | LOG.info("chat client has exited."); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/codeu/chat/RelayMain.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codeu.chat; 16 | 17 | import java.io.BufferedReader; 18 | import java.io.FileReader; 19 | import java.io.IOException; 20 | 21 | import codeu.chat.common.Secret; 22 | import codeu.chat.relay.Server; 23 | import codeu.chat.relay.ServerFrontEnd; 24 | import codeu.chat.util.Logger; 25 | import codeu.chat.util.Timeline; 26 | import codeu.chat.util.Uuid; 27 | import codeu.chat.util.connections.Connection; 28 | import codeu.chat.util.connections.ConnectionSource; 29 | import codeu.chat.util.connections.ServerConnectionSource; 30 | 31 | final class RelayMain { 32 | 33 | private static final Logger.Log LOG = Logger.newLog(RelayMain.class); 34 | 35 | public static void main(String[] args) { 36 | 37 | Logger.enableConsoleOutput(); 38 | 39 | try { 40 | Logger.enableFileOutput("chat_relay_log.log"); 41 | } catch (IOException ex) { 42 | LOG.error(ex, "Failed to set logger to write to file"); 43 | } 44 | 45 | LOG.info("============================= START OF LOG ============================="); 46 | 47 | final int myPort = Integer.parseInt(args[0]); 48 | 49 | try (final ConnectionSource source = ServerConnectionSource.forPort(myPort)) { 50 | 51 | // Limit the number of messages that the server tracks to be 1024 and limit the 52 | // max number of messages that the relay will send out to be 16. 53 | final Server relay = new Server(1024, 16); 54 | 55 | LOG.info("Relay object created."); 56 | 57 | LOG.info("Starting relay..."); 58 | 59 | startRelay(relay, source, args[1]); 60 | 61 | } catch (IOException ex) { 62 | LOG.error(ex, "Failed to establish server accept port"); 63 | } 64 | } 65 | 66 | private static void startRelay(final Server relay, 67 | final ConnectionSource source, 68 | final String teamFile) { 69 | 70 | final ServerFrontEnd frontEnd = new ServerFrontEnd(relay); 71 | LOG.info("Relay front end object created."); 72 | 73 | final Timeline timeline = new Timeline(); 74 | LOG.info("Relay timeline created."); 75 | 76 | timeline.scheduleNow(new Runnable() { 77 | @Override 78 | public void run() { 79 | LOG.info("Loading team data..."); 80 | loadTeamInfo(relay, teamFile); 81 | LOG.info("Done loading team data."); 82 | 83 | // Add this again in 1 minute so that new team entries will be added to 84 | // the relay. This won't support updating entries. 85 | timeline.scheduleIn(60000, this); 86 | } 87 | }); 88 | 89 | LOG.info("Starting relay main loop..."); 90 | 91 | while (true) { 92 | try { 93 | 94 | LOG.info("Establishing connection..."); 95 | final Connection connection = source.connect(); 96 | LOG.info("Connection established."); 97 | 98 | timeline.scheduleNow(new Runnable() { 99 | @Override 100 | public void run() { 101 | try { 102 | frontEnd.handleConnection(connection); 103 | } catch (Exception ex) { 104 | LOG.error(ex, "Exception handling connection."); 105 | } 106 | } 107 | }); 108 | 109 | } catch (IOException ex) { 110 | LOG.error(ex, "Failed to establish connection."); 111 | } 112 | } 113 | } 114 | 115 | private static void loadTeamInfo(Server relay, String file) { 116 | 117 | try (final BufferedReader reader = new BufferedReader(new FileReader(file))) { 118 | 119 | String line; 120 | for (line = reader.readLine(); 121 | line != null; 122 | line = reader.readLine()) { 123 | 124 | line = line.trim(); 125 | 126 | if (line.length() == 0) { 127 | // This line is blank, skip it 128 | } else if (line.startsWith("#")) { 129 | // this is a comment, skip it 130 | } else { 131 | 132 | try { 133 | 134 | final String[] tokens = line.split(":"); 135 | 136 | // There are just so many things that could go wrong when parsing 137 | // this line that it is not worth trying to handle ahead of time. 138 | // So instead just try to parse it and catch any exception. 139 | 140 | final Uuid id = Uuid.parse(tokens[0].trim()); 141 | final byte[] secret = Secret.parse(tokens[1].trim()); 142 | 143 | relay.addTeam(id, secret); 144 | } catch (Exception ex) { 145 | LOG.error(ex, "Skipping line \"%s\". Could not parse", line); 146 | } 147 | } 148 | } 149 | } catch (IOException ex) { 150 | LOG.error(ex, "Failed to load team data"); 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/codeu/chat/ServerMain.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | 16 | package codeu.chat; 17 | 18 | import java.io.IOException; 19 | 20 | import codeu.chat.common.Relay; 21 | import codeu.chat.common.Secret; 22 | import codeu.chat.server.NoOpRelay; 23 | import codeu.chat.server.RemoteRelay; 24 | import codeu.chat.server.Server; 25 | import codeu.chat.util.Logger; 26 | import codeu.chat.util.RemoteAddress; 27 | import codeu.chat.util.Uuid; 28 | import codeu.chat.util.connections.ClientConnectionSource; 29 | import codeu.chat.util.connections.Connection; 30 | import codeu.chat.util.connections.ConnectionSource; 31 | import codeu.chat.util.connections.ServerConnectionSource; 32 | 33 | final class ServerMain { 34 | 35 | private static final Logger.Log LOG = Logger.newLog(ServerMain.class); 36 | 37 | public static void main(String[] args) { 38 | 39 | Logger.enableConsoleOutput(); 40 | 41 | try { 42 | Logger.enableFileOutput("chat_server_log.log"); 43 | } catch (IOException ex) { 44 | LOG.error(ex, "Failed to set logger to write to file"); 45 | } 46 | 47 | LOG.info("============================= START OF LOG ============================="); 48 | 49 | final int myPort = Integer.parseInt(args[2]); 50 | final byte[] secret = Secret.parse(args[1]); 51 | 52 | Uuid id = null; 53 | try { 54 | id = Uuid.parse(args[0]); 55 | } catch (IOException ex) { 56 | System.out.println("Invalid id - shutting down server"); 57 | System.exit(1); 58 | } 59 | 60 | // This is the directory where it is safe to store data accross runs 61 | // of the server. 62 | final String persistentPath = args[3]; 63 | 64 | final RemoteAddress relayAddress = args.length > 4 ? 65 | RemoteAddress.parse(args[4]) : 66 | null; 67 | 68 | try ( 69 | final ConnectionSource serverSource = ServerConnectionSource.forPort(myPort); 70 | final ConnectionSource relaySource = relayAddress == null ? null : new ClientConnectionSource(relayAddress.host, relayAddress.port) 71 | ) { 72 | 73 | LOG.info("Starting server..."); 74 | runServer(id, secret, serverSource, relaySource); 75 | 76 | } catch (IOException ex) { 77 | 78 | LOG.error(ex, "Failed to establish connections"); 79 | 80 | } 81 | } 82 | 83 | private static void runServer(Uuid id, 84 | byte[] secret, 85 | ConnectionSource serverSource, 86 | ConnectionSource relaySource) { 87 | 88 | final Relay relay = relaySource == null ? 89 | new NoOpRelay() : 90 | new RemoteRelay(relaySource); 91 | 92 | final Server server = new Server(id, secret, relay); 93 | 94 | LOG.info("Created server."); 95 | 96 | while (true) { 97 | 98 | try { 99 | 100 | LOG.info("Established connection..."); 101 | final Connection connection = serverSource.connect(); 102 | LOG.info("Connection established."); 103 | 104 | server.handleConnection(connection); 105 | 106 | } catch (IOException ex) { 107 | LOG.error(ex, "Failed to establish connection."); 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/codeu/chat/SimpleGuiClientMain.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | //Licensed under the Apache License, Version 2.0 (the "License"); 4 | //you may not use this file except in compliance with the License. 5 | //You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codeu.chat; 16 | 17 | import java.io.IOException; 18 | 19 | import codeu.chat.client.Controller; 20 | import codeu.chat.client.simplegui.ChatSimpleGui; 21 | import codeu.chat.client.View; 22 | import codeu.chat.util.Logger; 23 | import codeu.chat.util.RemoteAddress; 24 | import codeu.chat.util.connections.ClientConnectionSource; 25 | import codeu.chat.util.connections.ConnectionSource; 26 | 27 | final class SimpleGuiClientMain { 28 | 29 | private static final Logger.Log LOG = Logger.newLog(SimpleGuiClientMain.class); 30 | 31 | public static void main(String [] args) { 32 | 33 | try { 34 | Logger.enableFileOutput("chat_simple_gui_client_log.log"); 35 | } catch (IOException ex) { 36 | LOG.error(ex, "Failed to set logger to write to file"); 37 | } 38 | 39 | LOG.info("============================= START OF LOG ============================="); 40 | 41 | LOG.info("Starting chat client..."); 42 | 43 | // Start up server connection/interface. 44 | 45 | final RemoteAddress address = RemoteAddress.parse(args[0]); 46 | 47 | try ( 48 | final ConnectionSource source = new ClientConnectionSource(address.host, address.port) 49 | ) { 50 | final Controller controller = new Controller(source); 51 | final View view = new View(source); 52 | 53 | LOG.info("Creating client..."); 54 | 55 | runClient(controller, view); 56 | 57 | } catch (Exception ex) { 58 | System.out.println("ERROR: Exception setting up client. Check log for details."); 59 | LOG.error(ex, "Exception setting up client."); 60 | } 61 | } 62 | 63 | private static void runClient(Controller controller, View view) { 64 | 65 | final ChatSimpleGui chatSimpleGui = new ChatSimpleGui(controller, view); 66 | 67 | LOG.info("Created client"); 68 | 69 | chatSimpleGui.run(); 70 | 71 | LOG.info("chat client is running."); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/codeu/chat/client/ClientContext.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codeu.chat.client; 16 | 17 | import codeu.chat.client.ClientConversation; 18 | import codeu.chat.client.ClientMessage; 19 | import codeu.chat.client.ClientUser; 20 | import codeu.chat.client.Controller; 21 | import codeu.chat.client.View; 22 | 23 | public final class ClientContext { 24 | 25 | public final ClientUser user; 26 | public final ClientConversation conversation; 27 | public final ClientMessage message; 28 | 29 | public ClientContext(Controller controller, View view) { 30 | user = new ClientUser(controller, view); 31 | conversation = new ClientConversation(controller, view, user); 32 | message = new ClientMessage(controller, view, user, conversation); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/codeu/chat/client/ClientConversation.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codeu.chat.client; 16 | 17 | import java.util.Arrays; 18 | import java.util.HashMap; 19 | import java.util.Map; 20 | 21 | import codeu.chat.common.Conversation; 22 | import codeu.chat.common.ConversationSummary; 23 | import codeu.chat.util.Logger; 24 | import codeu.chat.util.Method; 25 | import codeu.chat.util.Uuid; 26 | import codeu.chat.util.store.Store; 27 | 28 | public final class ClientConversation { 29 | 30 | private final static Logger.Log LOG = Logger.newLog(ClientConversation.class); 31 | 32 | private final Controller controller; 33 | private final View view; 34 | 35 | private ConversationSummary currentSummary = null; 36 | private Conversation currentConversation = null; 37 | 38 | private final ClientUser userContext; 39 | private ClientMessage messageContext = null; 40 | 41 | // This is the set of conversations known to the server. 42 | private final Map summariesByUuid = new HashMap<>(); 43 | 44 | // This is the set of conversations known to the server, sorted by title. 45 | private Store summariesSortedByTitle = 46 | new Store<>(String.CASE_INSENSITIVE_ORDER); 47 | 48 | public ClientConversation(Controller controller, View view, ClientUser userContext) { 49 | this.controller = controller; 50 | this.view = view; 51 | this.userContext = userContext; 52 | } 53 | 54 | public void setMessageContext(ClientMessage messageContext) { 55 | this.messageContext = messageContext; 56 | } 57 | 58 | // Validate the title of the conversation 59 | static public boolean isValidTitle(String title) { 60 | boolean clean = true; 61 | if ((title.length() <= 0) || (title.length() > 64)) { 62 | clean = false; 63 | } else { 64 | 65 | // TODO: check for invalid characters 66 | 67 | } 68 | return clean; 69 | } 70 | 71 | public boolean hasCurrent() { 72 | return (currentSummary != null); 73 | } 74 | 75 | public ConversationSummary getCurrent() { 76 | return currentSummary; 77 | } 78 | 79 | public Uuid getCurrentId() { return (currentSummary != null) ? currentSummary.id : null; } 80 | 81 | public int currentMessageCount() { 82 | return messageContext.currentMessageCount(); 83 | } 84 | 85 | public void showCurrent() { 86 | printConversation(currentSummary, userContext); 87 | } 88 | 89 | public void startConversation(String title, Uuid owner) { 90 | final boolean validInputs = isValidTitle(title); 91 | 92 | final Conversation conv = (validInputs) ? controller.newConversation(title, owner) : null; 93 | 94 | if (conv == null) { 95 | System.out.format("Error: conversation not created - %s.\n", 96 | (validInputs) ? "server failure" : "bad input value"); 97 | } else { 98 | LOG.info("New conversation: Title= \"%s\" UUID= %s", conv.title, conv.id); 99 | 100 | currentSummary = conv.summary; 101 | 102 | updateAllConversations(currentSummary != null); 103 | } 104 | } 105 | 106 | public void setCurrent(ConversationSummary conv) { currentSummary = conv; } 107 | 108 | public void showAllConversations() { 109 | updateAllConversations(false); 110 | 111 | for (final ConversationSummary c : summariesByUuid.values()) { 112 | printConversation(c, userContext); 113 | } 114 | } 115 | 116 | // Get a single conversation from the server. 117 | public Conversation getConversation(Uuid conversationId) { 118 | for (final Conversation c : view.getConversations(Arrays.asList(conversationId))) { 119 | return c; 120 | } 121 | return null; 122 | } 123 | 124 | private void joinConversation(String match) { 125 | Method.notImplemented(); 126 | } 127 | 128 | private void leaveCurrentConversation() { 129 | Method.notImplemented(); 130 | } 131 | 132 | private void updateCurrentConversation() { 133 | if (currentSummary == null) { 134 | currentConversation = null; 135 | } else { 136 | currentConversation = getConversation(currentSummary.id); 137 | if (currentConversation == null) { 138 | LOG.info("GetConversation: current=%s, current.id=%s, but currentConversation == null", 139 | currentSummary, currentSummary.id); 140 | } else { 141 | LOG.info("Get Conversation: Title=\"%s\" UUID=%s first=%s last=%s\n", 142 | currentConversation.title, currentConversation.id, currentConversation.firstMessage, 143 | currentConversation.lastMessage); 144 | } 145 | } 146 | } 147 | 148 | public int conversationsCount() { 149 | return summariesByUuid.size(); 150 | } 151 | 152 | public Iterable getConversationSummaries() { 153 | return summariesSortedByTitle.all(); 154 | } 155 | 156 | // Update the list of known Conversations. 157 | // If the input currentChanged is true, then re-establish the state of 158 | // the current Conversation, including its messages. 159 | public void updateAllConversations(boolean currentChanged) { 160 | 161 | summariesByUuid.clear(); 162 | summariesSortedByTitle = new Store<>(String.CASE_INSENSITIVE_ORDER); 163 | 164 | for (final ConversationSummary cs : view.getAllConversations()) { 165 | summariesByUuid.put(cs.id, cs); 166 | summariesSortedByTitle.insert(cs.title, cs); 167 | } 168 | 169 | if (currentChanged) { 170 | updateCurrentConversation(); 171 | messageContext.resetCurrent(true); 172 | } 173 | } 174 | 175 | // Print Conversation. User context is used to map from owner UUID to name. 176 | public static void printConversation(ConversationSummary c, ClientUser userContext) { 177 | if (c == null) { 178 | System.out.println("Null conversation"); 179 | } else { 180 | final String name = (userContext == null) ? null : userContext.getName(c.owner); 181 | final String ownerName = (name == null) ? "" : String.format(" (%s)", name); 182 | System.out.format(" Title: %s\n", c.title); 183 | System.out.format(" Id: %s owner: %s%s created %s\n", c.id, c.owner, ownerName, c.creation); 184 | } 185 | } 186 | 187 | // Print Conversation outside of User context. 188 | public static void printConversation(ConversationSummary c) { 189 | printConversation(c, null); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/codeu/chat/client/ClientUser.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codeu.chat.client; 16 | 17 | import java.util.Arrays; 18 | import java.util.Collection; 19 | import java.util.HashMap; 20 | import java.util.Map; 21 | 22 | import codeu.chat.common.User; 23 | import codeu.chat.util.Logger; 24 | import codeu.chat.util.Uuid; 25 | import codeu.chat.util.store.Store; 26 | 27 | public final class ClientUser { 28 | 29 | private final static Logger.Log LOG = Logger.newLog(ClientUser.class); 30 | 31 | private static final Collection EMPTY = Arrays.asList(new Uuid[0]); 32 | private final Controller controller; 33 | private final View view; 34 | 35 | private User current = null; 36 | 37 | private final Map usersById = new HashMap<>(); 38 | 39 | // This is the set of users known to the server, sorted by name. 40 | private Store usersByName = new Store<>(String.CASE_INSENSITIVE_ORDER); 41 | 42 | public ClientUser(Controller controller, View view) { 43 | this.controller = controller; 44 | this.view = view; 45 | } 46 | 47 | // Validate the username string 48 | static public boolean isValidName(String userName) { 49 | boolean clean = true; 50 | if (userName.length() == 0) { 51 | clean = false; 52 | } else { 53 | 54 | // TODO: check for invalid characters 55 | 56 | } 57 | return clean; 58 | } 59 | 60 | public boolean hasCurrent() { 61 | return (current != null); 62 | } 63 | 64 | public User getCurrent() { 65 | return current; 66 | } 67 | 68 | public boolean signInUser(String name) { 69 | updateUsers(); 70 | 71 | final User prev = current; 72 | if (name != null) { 73 | final User newCurrent = usersByName.first(name); 74 | if (newCurrent != null) { 75 | current = newCurrent; 76 | } 77 | } 78 | return (prev != current); 79 | } 80 | 81 | public boolean signOutUser() { 82 | boolean hadCurrent = hasCurrent(); 83 | current = null; 84 | return hadCurrent; 85 | } 86 | 87 | public void showCurrent() { 88 | printUser(current); 89 | } 90 | 91 | public void addUser(String name) { 92 | final boolean validInputs = isValidName(name); 93 | 94 | final User user = (validInputs) ? controller.newUser(name) : null; 95 | 96 | if (user == null) { 97 | System.out.format("Error: user not created - %s.\n", 98 | (validInputs) ? "server failure" : "bad input value"); 99 | } else { 100 | LOG.info("New user complete, Name= \"%s\" UUID=%s", user.name, user.id); 101 | updateUsers(); 102 | } 103 | } 104 | 105 | public void showAllUsers() { 106 | updateUsers(); 107 | for (final User u : usersByName.all()) { 108 | printUser(u); 109 | } 110 | } 111 | 112 | public User lookup(Uuid id) { 113 | return (usersById.containsKey(id)) ? usersById.get(id) : null; 114 | } 115 | 116 | public String getName(Uuid id) { 117 | final User user = lookup(id); 118 | if (user == null) { 119 | LOG.warning("userContext.lookup() failed on ID: %s", id); 120 | return null; 121 | } else { 122 | return user.name; 123 | } 124 | } 125 | 126 | public Iterable getUsers() { 127 | return usersByName.all(); 128 | } 129 | 130 | public void updateUsers() { 131 | usersById.clear(); 132 | usersByName = new Store<>(String.CASE_INSENSITIVE_ORDER); 133 | 134 | for (final User user : view.getUsersExcluding(EMPTY)) { 135 | usersById.put(user.id, user); 136 | usersByName.insert(user.name, user); 137 | } 138 | } 139 | 140 | public static String getUserInfoString(User user) { 141 | return (user == null) ? "Null user" : 142 | String.format(" User: %s\n Id: %s\n created: %s\n", user.name, user.id, user.creation); 143 | } 144 | 145 | public String showUserInfo(String uname) { 146 | return getUserInfoString(usersByName.first(uname)); 147 | } 148 | 149 | // Move to User's toString() 150 | public static void printUser(User user) { 151 | System.out.println(getUserInfoString(user)); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/codeu/chat/client/Controller.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codeu.chat.client; 16 | 17 | import java.io.PrintWriter; 18 | import java.io.StringWriter; 19 | import java.lang.Thread; 20 | 21 | import codeu.chat.common.BasicController; 22 | import codeu.chat.common.Conversation; 23 | import codeu.chat.common.Message; 24 | import codeu.chat.common.NetworkCode; 25 | import codeu.chat.common.User; 26 | import codeu.chat.util.Logger; 27 | import codeu.chat.util.Serializers; 28 | import codeu.chat.util.Uuid; 29 | import codeu.chat.util.connections.Connection; 30 | import codeu.chat.util.connections.ConnectionSource; 31 | 32 | public class Controller implements BasicController { 33 | 34 | private final static Logger.Log LOG = Logger.newLog(Controller.class); 35 | 36 | private final ConnectionSource source; 37 | 38 | public Controller(ConnectionSource source) { 39 | this.source = source; 40 | } 41 | 42 | @Override 43 | public Message newMessage(Uuid author, Uuid conversation, String body) { 44 | 45 | Message response = null; 46 | 47 | try (final Connection connection = source.connect()) { 48 | 49 | Serializers.INTEGER.write(connection.out(), NetworkCode.NEW_MESSAGE_REQUEST); 50 | Uuid.SERIALIZER.write(connection.out(), author); 51 | Uuid.SERIALIZER.write(connection.out(), conversation); 52 | Serializers.STRING.write(connection.out(), body); 53 | 54 | if (Serializers.INTEGER.read(connection.in()) == NetworkCode.NEW_MESSAGE_RESPONSE) { 55 | response = Serializers.nullable(Message.SERIALIZER).read(connection.in()); 56 | } else { 57 | LOG.error("Response from server failed."); 58 | } 59 | } catch (Exception ex) { 60 | System.out.println("ERROR: Exception during call on server. Check log for details."); 61 | LOG.error(ex, "Exception during call on server."); 62 | } 63 | 64 | return response; 65 | } 66 | 67 | @Override 68 | public User newUser(String name) { 69 | 70 | User response = null; 71 | 72 | try (final Connection connection = source.connect()) { 73 | 74 | Serializers.INTEGER.write(connection.out(), NetworkCode.NEW_USER_REQUEST); 75 | Serializers.STRING.write(connection.out(), name); 76 | LOG.info("newUser: Request completed."); 77 | 78 | if (Serializers.INTEGER.read(connection.in()) == NetworkCode.NEW_USER_RESPONSE) { 79 | response = Serializers.nullable(User.SERIALIZER).read(connection.in()); 80 | LOG.info("newUser: Response completed."); 81 | } else { 82 | LOG.error("Response from server failed."); 83 | } 84 | } catch (Exception ex) { 85 | System.out.println("ERROR: Exception during call on server. Check log for details."); 86 | LOG.error(ex, "Exception during call on server."); 87 | } 88 | 89 | return response; 90 | } 91 | 92 | @Override 93 | public Conversation newConversation(String title, Uuid owner) { 94 | 95 | Conversation response = null; 96 | 97 | try (final Connection connection = source.connect()) { 98 | 99 | Serializers.INTEGER.write(connection.out(), NetworkCode.NEW_CONVERSATION_REQUEST); 100 | Serializers.STRING.write(connection.out(), title); 101 | Uuid.SERIALIZER.write(connection.out(), owner); 102 | 103 | if (Serializers.INTEGER.read(connection.in()) == NetworkCode.NEW_CONVERSATION_RESPONSE) { 104 | response = Serializers.nullable(Conversation.SERIALIZER).read(connection.in()); 105 | } else { 106 | LOG.error("Response from server failed."); 107 | } 108 | } catch (Exception ex) { 109 | System.out.println("ERROR: Exception during call on server. Check log for details."); 110 | LOG.error(ex, "Exception during call on server."); 111 | } 112 | 113 | return response; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/codeu/chat/client/commandline/ListNavigator.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codeu.chat.client.commandline; 16 | 17 | import java.lang.Math; 18 | import java.util.ArrayList; 19 | import java.util.Collection; 20 | import java.util.List; 21 | import java.util.Scanner; 22 | 23 | import codeu.chat.common.ListViewable; 24 | 25 | // Page up and down through a list of objects and allow user to select one. 26 | // T must implement ListViewable. This gives access to listView(), which produces 27 | // the human-readable string that identifies the object in the list. 28 | public final class ListNavigator { 29 | 30 | private final List selection = new ArrayList(); 31 | private final Scanner lineScanner; 32 | private int top; 33 | private int bottom; 34 | private int pageSize; 35 | private boolean hasInt; 36 | private int intValue; 37 | private String choice; 38 | 39 | // Creates a ListNavigator of the desired ListViewable subclass 40 | public ListNavigator(Iterable objectList, Scanner scanner, int pageSize) { 41 | this.pageSize = pageSize; 42 | this.lineScanner = scanner; 43 | for (final T e : objectList) { 44 | this.selection.add(e); 45 | } 46 | moveUp(); // set top and bottom for first page. 47 | } 48 | 49 | // Run the chooser. Returns true when a selection has been made, or false on an error 50 | // or if the user cancels the operation. 51 | public boolean chooseFromList() { 52 | if (selection.size() == 0) { 53 | System.out.println("ERROR: selection is empty - cannot select"); 54 | return false; 55 | } else { 56 | while (true) { 57 | displayChoices(); 58 | issuePrompt(); 59 | getChoice(); 60 | if (indexSelected()) { 61 | return true; 62 | } else if (moveDownSelected()) { 63 | moveDown(); 64 | } else if (moveUpSelected()) { 65 | moveUp(); 66 | } else if (cancelSelected()) { 67 | return false; 68 | } else { 69 | System.out.println("Poor choice - try again."); 70 | } 71 | } 72 | } 73 | } 74 | 75 | // Get the selectes object from the chooser. 76 | // Should be called after chooseFromList returns true (otherwise returns null). 77 | public T getSelectedChoice() { 78 | return (hasInt) ? selection.get(intValue - 1) : null; 79 | } 80 | 81 | // Print a prompt that tells the user their options: 82 | // 1) the index of a list item that is currently on view. Return the object. 83 | // 2) '+' page down 84 | // 3) '-' page back up 85 | // 4) '*' cancel (return null) 86 | private void issuePrompt() { 87 | System.out.format("Enter index (%s)%s%s or '*' to cancel: ", 88 | (top != bottom) ? String.format("%d-%d", top, bottom) : String.format("%d", top), 89 | (canMoveUp()) ? " or '-' to back up" : "", 90 | (canMoveDown()) ? " or '+' to advance" : ""); 91 | } 92 | 93 | // Display a set of entries with index numbers. 94 | // Number of entries displayed is determined by pageSize. 95 | private void displayChoices() { 96 | for (int i = top; i <= bottom; i++) { 97 | System.out.format(" [%d]: %s\n", i, selection.get(i - 1).listView()); 98 | } 99 | } 100 | 101 | // Process user's response to issuePrompt() prompt. 102 | private void getChoice() { 103 | final Scanner tokenScanner = new Scanner(lineScanner.nextLine()); 104 | choice = tokenScanner.nextLine().trim(); 105 | try { 106 | intValue = Integer.parseInt(choice); 107 | hasInt = true; 108 | } catch (NumberFormatException ex) { 109 | hasInt = false; 110 | } 111 | tokenScanner.close(); 112 | } 113 | 114 | // Unseen entries precede the current view. 115 | private boolean canMoveUp() { 116 | return top > 1; 117 | } 118 | 119 | // Unseen entries follow the current view. 120 | private boolean canMoveDown() { 121 | return bottom < selection.size(); 122 | } 123 | 124 | private boolean indexSelected() { 125 | return hasInt && (intValue >= top) && (intValue <= bottom); 126 | } 127 | 128 | private final boolean moveDownSelected() { 129 | return choice.equals("+") && canMoveDown(); 130 | } 131 | 132 | private final boolean moveUpSelected() { 133 | return choice.equals("-") && canMoveUp(); 134 | } 135 | 136 | private final boolean cancelSelected() { 137 | return choice.equals("*"); 138 | } 139 | 140 | // Move the view up one page (or to top of page). 141 | private void moveUp() { 142 | top = Math.max(top - pageSize, 1); 143 | bottom = Math.min(top + pageSize - 1, selection.size()); 144 | } 145 | 146 | // Move the view down one page (or to bottom of page). 147 | private void moveDown() { 148 | if (selection.size() == 0) return; 149 | bottom = Math.min(bottom + pageSize, selection.size()); 150 | top = bottom - pageSize + 1; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/codeu/chat/client/simplegui/ChatSimpleGui.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codeu.chat.client.simplegui; 16 | 17 | import java.awt.*; 18 | import javax.swing.*; 19 | import javax.swing.border.Border; 20 | 21 | import codeu.chat.client.ClientContext; 22 | import codeu.chat.client.Controller; 23 | import codeu.chat.client.View; 24 | import codeu.chat.util.Logger; 25 | 26 | // Chat - top-level client application - Java Simple GUI (using Java Swing) 27 | public final class ChatSimpleGui { 28 | 29 | private final static Logger.Log LOG = Logger.newLog(ChatSimpleGui.class); 30 | 31 | private JFrame mainFrame; 32 | 33 | private final ClientContext clientContext; 34 | 35 | // Constructor - sets up the Chat Application 36 | public ChatSimpleGui(Controller controller, View view) { 37 | clientContext = new ClientContext(controller, view); 38 | } 39 | 40 | // Run the GUI client 41 | public void run() { 42 | 43 | try { 44 | 45 | initialize(); 46 | mainFrame.setVisible(true); 47 | 48 | } catch (Exception ex) { 49 | System.out.println("ERROR: Exception in ChatSimpleGui.run. Check log for details."); 50 | LOG.error(ex, "Exception in ChatSimpleGui.run"); 51 | System.exit(1); 52 | } 53 | } 54 | 55 | private Border paneBorder() { 56 | Border outside = BorderFactory.createLineBorder(Color.LIGHT_GRAY); 57 | Border inside = BorderFactory.createEmptyBorder(8, 8, 8, 8); 58 | return BorderFactory.createCompoundBorder(outside, inside); 59 | } 60 | 61 | // Initialize the GUI 62 | private void initialize() { 63 | 64 | // Outermost frame. 65 | // NOTE: may have tweak size, or place in scrollable panel. 66 | mainFrame = new JFrame("Chat"); 67 | mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 68 | mainFrame.setSize(790, 450); 69 | 70 | // Main View - outermost graphics panel. 71 | final JPanel mainViewPanel = new JPanel(new GridBagLayout()); 72 | mainViewPanel.setBorder(paneBorder()); 73 | 74 | // Build main panels - Users, Conversations, Messages. 75 | final JPanel usersViewPanel = new UserPanel(clientContext); 76 | usersViewPanel.setBorder(paneBorder()); 77 | final GridBagConstraints usersViewC = new GridBagConstraints(); 78 | 79 | final MessagePanel messagesViewPanel = new MessagePanel(clientContext); 80 | messagesViewPanel.setBorder(paneBorder()); 81 | final GridBagConstraints messagesViewC = new GridBagConstraints(); 82 | 83 | // ConversationsPanel gets access to MessagesPanel 84 | final JPanel conversationsViewPanel = new ConversationPanel(clientContext, messagesViewPanel); 85 | conversationsViewPanel.setBorder(paneBorder()); 86 | final GridBagConstraints conversationViewC = new GridBagConstraints(); 87 | 88 | // Placement of main panels. 89 | usersViewC.gridx = 0; 90 | usersViewC.gridy = 0; 91 | usersViewC.gridwidth = 1; 92 | usersViewC.gridheight = 1; 93 | usersViewC.fill = GridBagConstraints.BOTH; 94 | usersViewC.weightx = 0.3; 95 | usersViewC.weighty = 0.3; 96 | 97 | conversationViewC.gridx = 1; 98 | conversationViewC.gridy = 0; 99 | conversationViewC.gridwidth = 1; 100 | conversationViewC.gridheight = 1; 101 | conversationViewC.fill = GridBagConstraints.BOTH; 102 | conversationViewC.weightx = 0.7; 103 | conversationViewC.weighty = 0.3; 104 | 105 | messagesViewC.gridx = 0; 106 | messagesViewC.gridy = 1; 107 | messagesViewC.gridwidth = 2; 108 | messagesViewC.gridheight = 1; 109 | messagesViewC.fill = GridBagConstraints.BOTH; 110 | messagesViewC.weighty = 0.7; 111 | 112 | mainViewPanel.add(usersViewPanel, usersViewC); 113 | mainViewPanel.add(conversationsViewPanel, conversationViewC); 114 | mainViewPanel.add(messagesViewPanel, messagesViewC); 115 | 116 | mainFrame.add(mainViewPanel); 117 | mainFrame.pack(); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/codeu/chat/client/simplegui/ConversationPanel.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codeu.chat.client.simplegui; 16 | 17 | import java.awt.*; 18 | import java.awt.event.ActionEvent; 19 | import java.awt.event.ActionListener; 20 | import javax.swing.*; 21 | import javax.swing.event.ListSelectionEvent; 22 | import javax.swing.event.ListSelectionListener; 23 | 24 | import codeu.chat.client.ClientContext; 25 | import codeu.chat.common.ConversationSummary; 26 | 27 | // NOTE: JPanel is serializable, but there is no need to serialize ConversationPanel 28 | // without the @SuppressWarnings, the compiler will complain of no override for serialVersionUID 29 | @SuppressWarnings("serial") 30 | public final class ConversationPanel extends JPanel { 31 | 32 | private final ClientContext clientContext; 33 | private final MessagePanel messagePanel; 34 | 35 | public ConversationPanel(ClientContext clientContext, MessagePanel messagePanel) { 36 | super(new GridBagLayout()); 37 | this.clientContext = clientContext; 38 | this.messagePanel = messagePanel; 39 | initialize(); 40 | } 41 | 42 | private void initialize() { 43 | 44 | // This panel contains from top to bottom: a title bar, 45 | // a list of conversations, and a button bar. 46 | 47 | // Title 48 | final JPanel titlePanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); 49 | final GridBagConstraints titlePanelC = new GridBagConstraints(); 50 | titlePanelC.gridx = 0; 51 | titlePanelC.gridy = 0; 52 | titlePanelC.anchor = GridBagConstraints.PAGE_START; 53 | 54 | final JLabel titleLabel = new JLabel("Conversations", JLabel.LEFT); 55 | titleLabel.setAlignmentX(Component.LEFT_ALIGNMENT); 56 | titlePanel.add(titleLabel); 57 | 58 | // Conversation list 59 | final JPanel listShowPanel = new JPanel(); 60 | final GridBagConstraints listPanelC = new GridBagConstraints(); 61 | 62 | final DefaultListModel listModel = new DefaultListModel<>(); 63 | final JList objectList = new JList<>(listModel); 64 | objectList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 65 | objectList.setVisibleRowCount(15); 66 | objectList.setSelectedIndex(-1); 67 | 68 | final JScrollPane listScrollPane = new JScrollPane(objectList); 69 | listShowPanel.add(listScrollPane); 70 | listScrollPane.setMinimumSize(new Dimension(250, 200)); 71 | 72 | // Button bar 73 | final JPanel buttonPanel = new JPanel(); 74 | final GridBagConstraints buttonPanelC = new GridBagConstraints(); 75 | 76 | final JButton updateButton = new JButton("Update"); 77 | final JButton addButton = new JButton("Add"); 78 | 79 | updateButton.setAlignmentX(Component.LEFT_ALIGNMENT); 80 | buttonPanel.add(updateButton); 81 | buttonPanel.add(addButton); 82 | 83 | // Put panels together 84 | titlePanelC.gridx = 0; 85 | titlePanelC.gridy = 0; 86 | titlePanelC.gridwidth = 10; 87 | titlePanelC.gridheight = 4; 88 | titlePanelC.fill = GridBagConstraints.HORIZONTAL; 89 | titlePanelC.anchor = GridBagConstraints.FIRST_LINE_START; 90 | 91 | listPanelC.gridx = 0; 92 | listPanelC.gridy = 4; 93 | listPanelC.gridwidth = 10; 94 | listPanelC.gridheight = 4; 95 | listPanelC.fill = GridBagConstraints.BOTH; 96 | listPanelC.anchor = GridBagConstraints.FIRST_LINE_START; 97 | listPanelC.weightx = 0.8; 98 | listPanelC.weighty = 0.5; 99 | 100 | buttonPanelC.gridx = 0; 101 | buttonPanelC.gridy = 8; 102 | buttonPanelC.gridwidth = 10; 103 | buttonPanelC.gridheight = 4; 104 | buttonPanelC.fill = GridBagConstraints.HORIZONTAL; 105 | buttonPanelC.anchor = GridBagConstraints.FIRST_LINE_START; 106 | 107 | this.add(titlePanel, titlePanelC); 108 | this.add(listShowPanel, listPanelC); 109 | this.add(buttonPanel, buttonPanelC); 110 | 111 | // User clicks Conversations Update button. 112 | updateButton.addActionListener(new ActionListener() { 113 | @Override 114 | public void actionPerformed(ActionEvent e) { 115 | ConversationPanel.this.getAllConversations(listModel); 116 | } 117 | }); 118 | 119 | // User clicks Conversations Add button. 120 | addButton.addActionListener(new ActionListener() { 121 | @Override 122 | public void actionPerformed(ActionEvent e) { 123 | if (clientContext.user.hasCurrent()) { 124 | final String s = (String) JOptionPane.showInputDialog( 125 | ConversationPanel.this, "Enter title:", "Add Conversation", JOptionPane.PLAIN_MESSAGE, 126 | null, null, ""); 127 | if (s != null && s.length() > 0) { 128 | clientContext.conversation.startConversation(s, clientContext.user.getCurrent().id); 129 | ConversationPanel.this.getAllConversations(listModel); 130 | } 131 | } else { 132 | JOptionPane.showMessageDialog(ConversationPanel.this, "You are not signed in."); 133 | } 134 | } 135 | }); 136 | 137 | // User clicks on Conversation - Set Conversation to current and fill in Messages panel. 138 | objectList.addListSelectionListener(new ListSelectionListener() { 139 | @Override 140 | public void valueChanged(ListSelectionEvent e) { 141 | if (objectList.getSelectedIndex() != -1) { 142 | final int index = objectList.getSelectedIndex(); 143 | final String data = objectList.getSelectedValue(); 144 | final ConversationSummary cs = ConversationPanel.this.lookupByTitle(data, index); 145 | 146 | clientContext.conversation.setCurrent(cs); 147 | 148 | messagePanel.update(cs); 149 | } 150 | } 151 | }); 152 | 153 | getAllConversations(listModel); 154 | } 155 | 156 | // Populate ListModel - updates display objects. 157 | private void getAllConversations(DefaultListModel convDisplayList) { 158 | 159 | clientContext.conversation.updateAllConversations(false); 160 | convDisplayList.clear(); 161 | 162 | for (final ConversationSummary conv : clientContext.conversation.getConversationSummaries()) { 163 | convDisplayList.addElement(conv.title); 164 | } 165 | } 166 | 167 | // Locate the Conversation object for a selected title string. 168 | // index handles possible duplicate titles. 169 | private ConversationSummary lookupByTitle(String title, int index) { 170 | 171 | int localIndex = 0; 172 | for (final ConversationSummary cs : clientContext.conversation.getConversationSummaries()) { 173 | if ((localIndex >= index) && cs.title.equals(title)) { 174 | return cs; 175 | } 176 | localIndex++; 177 | } 178 | return null; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/codeu/chat/client/simplegui/MessagePanel.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codeu.chat.client.simplegui; 16 | 17 | import java.awt.*; 18 | import java.awt.event.ActionEvent; 19 | import java.awt.event.ActionListener; 20 | import javax.swing.*; 21 | 22 | import codeu.chat.client.ClientContext; 23 | import codeu.chat.common.ConversationSummary; 24 | import codeu.chat.common.Message; 25 | import codeu.chat.common.User; 26 | 27 | // NOTE: JPanel is serializable, but there is no need to serialize MessagePanel 28 | // without the @SuppressWarnings, the compiler will complain of no override for serialVersionUID 29 | @SuppressWarnings("serial") 30 | public final class MessagePanel extends JPanel { 31 | 32 | // These objects are modified by the Conversation Panel. 33 | private final JLabel messageOwnerLabel = new JLabel("Owner:", JLabel.RIGHT); 34 | private final JLabel messageConversationLabel = new JLabel("Conversation:", JLabel.LEFT); 35 | private final DefaultListModel messageListModel = new DefaultListModel<>(); 36 | 37 | private final ClientContext clientContext; 38 | 39 | public MessagePanel(ClientContext clientContext) { 40 | super(new GridBagLayout()); 41 | this.clientContext = clientContext; 42 | initialize(); 43 | } 44 | 45 | // External agent calls this to trigger an update of this panel's contents. 46 | public void update(ConversationSummary owningConversation) { 47 | 48 | final User u = (owningConversation == null) ? 49 | null : 50 | clientContext.user.lookup(owningConversation.owner); 51 | 52 | messageOwnerLabel.setText("Owner: " + 53 | ((u==null) ? 54 | ((owningConversation==null) ? "" : owningConversation.owner) : 55 | u.name)); 56 | 57 | messageConversationLabel.setText("Conversation: " + owningConversation.title); 58 | 59 | getAllMessages(owningConversation); 60 | } 61 | 62 | private void initialize() { 63 | 64 | // This panel contains the messages in the current conversation. 65 | // It has a title bar with the current conversation and owner, 66 | // then a list panel with the messages, then a button bar. 67 | 68 | // Title bar - current conversation and owner 69 | final JPanel titlePanel = new JPanel(new GridBagLayout()); 70 | final GridBagConstraints titlePanelC = new GridBagConstraints(); 71 | 72 | final JPanel titleConvPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); 73 | final GridBagConstraints titleConvPanelC = new GridBagConstraints(); 74 | titleConvPanelC.gridx = 0; 75 | titleConvPanelC.gridy = 0; 76 | titleConvPanelC.anchor = GridBagConstraints.PAGE_START; 77 | 78 | final JPanel titleOwnerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); 79 | final GridBagConstraints titleOwnerPanelC = new GridBagConstraints(); 80 | titleOwnerPanelC.gridx = 0; 81 | titleOwnerPanelC.gridy = 1; 82 | titleOwnerPanelC.anchor = GridBagConstraints.PAGE_START; 83 | 84 | // messageConversationLabel is an instance variable of Conversation panel 85 | // can update it. 86 | messageConversationLabel.setAlignmentX(Component.LEFT_ALIGNMENT); 87 | titleConvPanel.add(messageConversationLabel); 88 | 89 | // messageOwnerLabel is an instance variable of Conversation panel 90 | // can update it. 91 | messageOwnerLabel.setAlignmentX(Component.LEFT_ALIGNMENT); 92 | titleOwnerPanel.add(messageOwnerLabel); 93 | 94 | titlePanel.add(titleConvPanel, titleConvPanelC); 95 | titlePanel.add(titleOwnerPanel, titleOwnerPanelC); 96 | titlePanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); 97 | 98 | // User List panel. 99 | final JPanel listShowPanel = new JPanel(); 100 | final GridBagConstraints listPanelC = new GridBagConstraints(); 101 | 102 | // messageListModel is an instance variable so Conversation panel 103 | // can update it. 104 | final JList userList = new JList<>(messageListModel); 105 | userList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 106 | userList.setVisibleRowCount(15); 107 | userList.setSelectedIndex(-1); 108 | 109 | final JScrollPane userListScrollPane = new JScrollPane(userList); 110 | listShowPanel.add(userListScrollPane); 111 | userListScrollPane.setMinimumSize(new Dimension(500, 200)); 112 | userListScrollPane.setPreferredSize(new Dimension(500, 200)); 113 | 114 | // Button panel 115 | final JPanel buttonPanel = new JPanel(); 116 | final GridBagConstraints buttonPanelC = new GridBagConstraints(); 117 | 118 | final JButton addButton = new JButton("Add"); 119 | buttonPanel.add(addButton); 120 | 121 | // Placement of title, list panel, buttons, and current user panel. 122 | titlePanelC.gridx = 0; 123 | titlePanelC.gridy = 0; 124 | titlePanelC.gridwidth = 10; 125 | titlePanelC.gridheight = 1; 126 | titlePanelC.fill = GridBagConstraints.HORIZONTAL; 127 | titlePanelC.anchor = GridBagConstraints.FIRST_LINE_START; 128 | 129 | listPanelC.gridx = 0; 130 | listPanelC.gridy = 1; 131 | listPanelC.gridwidth = 10; 132 | listPanelC.gridheight = 8; 133 | listPanelC.fill = GridBagConstraints.BOTH; 134 | listPanelC.anchor = GridBagConstraints.FIRST_LINE_START; 135 | listPanelC.weighty = 0.8; 136 | 137 | buttonPanelC.gridx = 0; 138 | buttonPanelC.gridy = 11; 139 | buttonPanelC.gridwidth = 10; 140 | buttonPanelC.gridheight = 1; 141 | buttonPanelC.fill = GridBagConstraints.HORIZONTAL; 142 | buttonPanelC.anchor = GridBagConstraints.FIRST_LINE_START; 143 | 144 | this.add(titlePanel, titlePanelC); 145 | this.add(listShowPanel, listPanelC); 146 | this.add(buttonPanel, buttonPanelC); 147 | 148 | // User click Messages Add button - prompt for message body and add new Message to Conversation 149 | addButton.addActionListener(new ActionListener() { 150 | @Override 151 | public void actionPerformed(ActionEvent e) { 152 | if (!clientContext.user.hasCurrent()) { 153 | JOptionPane.showMessageDialog(MessagePanel.this, "You are not signed in."); 154 | } else if (!clientContext.conversation.hasCurrent()) { 155 | JOptionPane.showMessageDialog(MessagePanel.this, "You must select a conversation."); 156 | } else { 157 | final String messageText = (String) JOptionPane.showInputDialog( 158 | MessagePanel.this, "Enter message:", "Add Message", JOptionPane.PLAIN_MESSAGE, 159 | null, null, ""); 160 | if (messageText != null && messageText.length() > 0) { 161 | clientContext.message.addMessage( 162 | clientContext.user.getCurrent().id, 163 | clientContext.conversation.getCurrentId(), 164 | messageText); 165 | MessagePanel.this.getAllMessages(clientContext.conversation.getCurrent()); 166 | } 167 | } 168 | } 169 | }); 170 | 171 | // Panel is set up. If there is a current conversation, Populate the conversation list. 172 | getAllMessages(clientContext.conversation.getCurrent()); 173 | } 174 | 175 | // Populate ListModel 176 | // TODO: don't refetch messages if current conversation not changed 177 | private void getAllMessages(ConversationSummary conversation) { 178 | messageListModel.clear(); 179 | 180 | for (final Message m : clientContext.message.getConversationContents(conversation)) { 181 | // Display author name if available. Otherwise display the author UUID. 182 | final String authorName = clientContext.user.getName(m.author); 183 | 184 | final String displayString = String.format("%s: [%s]: %s", 185 | ((authorName == null) ? m.author : authorName), m.creation, m.content); 186 | 187 | messageListModel.addElement(displayString); 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/codeu/chat/client/simplegui/UserPanel.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codeu.chat.client.simplegui; 16 | 17 | import java.awt.*; 18 | import java.awt.event.ActionEvent; 19 | import java.awt.event.ActionListener; 20 | import javax.swing.*; 21 | import javax.swing.event.ListSelectionEvent; 22 | import javax.swing.event.ListSelectionListener; 23 | 24 | import codeu.chat.client.ClientContext; 25 | import codeu.chat.common.User; 26 | 27 | // NOTE: JPanel is serializable, but there is no need to serialize UserPanel 28 | // without the @SuppressWarnings, the compiler will complain of no override for serialVersionUID 29 | @SuppressWarnings("serial") 30 | public final class UserPanel extends JPanel { 31 | 32 | private final ClientContext clientContext; 33 | 34 | public UserPanel(ClientContext clientContext) { 35 | super(new GridBagLayout()); 36 | this.clientContext = clientContext; 37 | initialize(); 38 | } 39 | 40 | private void initialize() { 41 | 42 | // This panel contains from top to bottom; a title bar, a list of users, 43 | // information about the current (selected) user, and a button bar. 44 | 45 | // Title bar - also includes name of currently signed-in user. 46 | final JPanel titlePanel = new JPanel(new GridBagLayout()); 47 | final GridBagConstraints titlePanelC = new GridBagConstraints(); 48 | 49 | final JLabel titleLabel = new JLabel("Users", JLabel.LEFT); 50 | final GridBagConstraints titleLabelC = new GridBagConstraints(); 51 | titleLabelC.gridx = 0; 52 | titleLabelC.gridy = 0; 53 | titleLabelC.anchor = GridBagConstraints.PAGE_START; 54 | 55 | final GridBagConstraints titleGapC = new GridBagConstraints(); 56 | titleGapC.gridx = 1; 57 | titleGapC.gridy = 0; 58 | titleGapC.fill = GridBagConstraints.HORIZONTAL; 59 | titleGapC.weightx = 0.9; 60 | 61 | final JLabel userSignedInLabel = new JLabel("not signed in", JLabel.RIGHT); 62 | final GridBagConstraints titleUserC = new GridBagConstraints(); 63 | titleUserC.gridx = 2; 64 | titleUserC.gridy = 0; 65 | titleUserC.anchor = GridBagConstraints.LINE_END; 66 | 67 | titlePanel.add(titleLabel, titleLabelC); 68 | titlePanel.add(Box.createHorizontalGlue(), titleGapC); 69 | titlePanel.add(userSignedInLabel, titleUserC); 70 | titlePanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); 71 | 72 | // User List panel. 73 | final JPanel listShowPanel = new JPanel(); 74 | final GridBagConstraints listPanelC = new GridBagConstraints(); 75 | 76 | final DefaultListModel listModel = new DefaultListModel<>(); 77 | final JList userList = new JList<>(listModel); 78 | userList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 79 | userList.setVisibleRowCount(10); 80 | userList.setSelectedIndex(-1); 81 | 82 | final JScrollPane userListScrollPane = new JScrollPane(userList); 83 | listShowPanel.add(userListScrollPane); 84 | userListScrollPane.setPreferredSize(new Dimension(150, 150)); 85 | 86 | // Current User panel 87 | final JPanel currentPanel = new JPanel(); 88 | final GridBagConstraints currentPanelC = new GridBagConstraints(); 89 | 90 | final JTextArea userInfoPanel = new JTextArea(); 91 | final JScrollPane userInfoScrollPane = new JScrollPane(userInfoPanel); 92 | currentPanel.add(userInfoScrollPane); 93 | userInfoScrollPane.setPreferredSize(new Dimension(245, 85)); 94 | 95 | // Button bar 96 | final JPanel buttonPanel = new JPanel(); 97 | final GridBagConstraints buttonPanelC = new GridBagConstraints(); 98 | 99 | final JButton userUpdateButton = new JButton("Update"); 100 | final JButton userSignInButton = new JButton("Sign In"); 101 | final JButton userAddButton = new JButton("Add"); 102 | 103 | buttonPanel.add(userUpdateButton); 104 | buttonPanel.add(userSignInButton); 105 | buttonPanel.add(userAddButton); 106 | 107 | // Placement of title, list panel, buttons, and current user panel. 108 | titlePanelC.gridx = 0; 109 | titlePanelC.gridy = 0; 110 | titlePanelC.gridwidth = 10; 111 | titlePanelC.gridheight = 1; 112 | titlePanelC.fill = GridBagConstraints.HORIZONTAL; 113 | titlePanelC.anchor = GridBagConstraints.FIRST_LINE_START; 114 | 115 | listPanelC.gridx = 0; 116 | listPanelC.gridy = 1; 117 | listPanelC.gridwidth = 10; 118 | listPanelC.gridheight = 8; 119 | listPanelC.fill = GridBagConstraints.BOTH; 120 | listPanelC.anchor = GridBagConstraints.FIRST_LINE_START; 121 | listPanelC.weighty = 0.8; 122 | 123 | currentPanelC.gridx = 0; 124 | currentPanelC.gridy = 9; 125 | currentPanelC.gridwidth = 10; 126 | currentPanelC.gridheight = 3; 127 | currentPanelC.fill = GridBagConstraints.HORIZONTAL; 128 | currentPanelC.anchor = GridBagConstraints.FIRST_LINE_START; 129 | 130 | buttonPanelC.gridx = 0; 131 | buttonPanelC.gridy = 12; 132 | buttonPanelC.gridwidth = 10; 133 | buttonPanelC.gridheight = 1; 134 | buttonPanelC.fill = GridBagConstraints.HORIZONTAL; 135 | buttonPanelC.anchor = GridBagConstraints.FIRST_LINE_START; 136 | 137 | this.add(titlePanel, titlePanelC); 138 | this.add(listShowPanel, listPanelC); 139 | this.add(buttonPanel, buttonPanelC); 140 | this.add(currentPanel, currentPanelC); 141 | 142 | userUpdateButton.addActionListener(new ActionListener() { 143 | @Override 144 | public void actionPerformed(ActionEvent e) { 145 | UserPanel.this.getAllUsers(listModel); 146 | } 147 | }); 148 | 149 | userSignInButton.addActionListener(new ActionListener() { 150 | @Override 151 | public void actionPerformed(ActionEvent e) { 152 | if (userList.getSelectedIndex() != -1) { 153 | final String data = userList.getSelectedValue(); 154 | clientContext.user.signInUser(data); 155 | userSignedInLabel.setText("Hello " + data); 156 | } 157 | } 158 | }); 159 | 160 | userAddButton.addActionListener(new ActionListener() { 161 | @Override 162 | public void actionPerformed(ActionEvent e) { 163 | final String s = (String) JOptionPane.showInputDialog( 164 | UserPanel.this, "Enter user name:", "Add User", JOptionPane.PLAIN_MESSAGE, 165 | null, null, ""); 166 | if (s != null && s.length() > 0) { 167 | clientContext.user.addUser(s); 168 | UserPanel.this.getAllUsers(listModel); 169 | } 170 | } 171 | }); 172 | 173 | userList.addListSelectionListener(new ListSelectionListener() { 174 | @Override 175 | public void valueChanged(ListSelectionEvent e) { 176 | if (userList.getSelectedIndex() != -1) { 177 | final String data = userList.getSelectedValue(); 178 | userInfoPanel.setText(clientContext.user.showUserInfo(data)); 179 | } 180 | } 181 | }); 182 | 183 | getAllUsers(listModel); 184 | } 185 | 186 | // Swing UI: populate ListModel object - updates display objects. 187 | private void getAllUsers(DefaultListModel usersList) { 188 | clientContext.user.updateUsers(); 189 | usersList.clear(); 190 | 191 | for (final User u : clientContext.user.getUsers()) { 192 | usersList.addElement(u.name); 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/codeu/chat/common/BasicController.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codeu.chat.common; 16 | 17 | import codeu.chat.util.Uuid; 18 | 19 | // BASIC CONTROLLER 20 | // 21 | // The controller component in the Model-View-Controller pattern. This 22 | // component is used to write information to the model where the model 23 | // is the current state of the server. Data returned from the controller 24 | // should be treated as read only data as manipulating any data returned 25 | // from the controller may have no effect on the server's state. 26 | public interface BasicController { 27 | 28 | // NEW MESSAGE 29 | // 30 | // Create a new message on the server. All parameters must be provided 31 | // or else the server won't apply the change. If the operation is 32 | // successful, a Message object will be returned representing the full 33 | // state of the message on the server. 34 | Message newMessage(Uuid author, Uuid conversation, String body); 35 | 36 | // NEW USER 37 | // 38 | // Create a new user on the server. All parameters must be provided 39 | // or else the server won't apply the change. If the operation is 40 | // successful, a User object will be returned representing the full 41 | // state of the user on the server. Whether user names can be shared 42 | // is undefined. 43 | User newUser(String name); 44 | 45 | // NEW CONVERSATION 46 | // 47 | // Create a new conversation on the server. All parameters must be 48 | // provided or else the server won't apply the change. If the 49 | // operation is successful, a Conversation object will be returned 50 | // representing the full state of the conversation on the server. 51 | // Whether conversations can have the same title is undefined. 52 | Conversation newConversation(String title, Uuid owner); 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/codeu/chat/common/BasicView.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codeu.chat.common; 16 | 17 | import java.util.Collection; 18 | 19 | import codeu.chat.common.Conversation; 20 | import codeu.chat.common.ConversationSummary; 21 | import codeu.chat.common.Message; 22 | import codeu.chat.common.User; 23 | import codeu.chat.util.Uuid; 24 | 25 | // BASIC VIEW 26 | // 27 | // The view component in the Model-View-Controller pattern. This component 28 | // is used to read information from the model where the model is the current 29 | // state of the server. Data returned from the view should be treated as 30 | // read only data as manipulating any data returned from the view may 31 | // have no effect on the server's state. 32 | 33 | public interface BasicView { 34 | 35 | // GET USERS 36 | // 37 | // Return all users whose id is found in the given collection. 38 | Collection getUsers(Collection ids); 39 | 40 | // GET ALL CONVERSATIONS 41 | // 42 | // Return a summary of each converation. 43 | Collection getAllConversations(); 44 | 45 | // GET CONVERSATIONS 46 | // 47 | // Return all conversations whose id is found in the given collection. 48 | Collection getConversations(Collection ids); 49 | 50 | // GET MESSAGES 51 | // 52 | // Return all messages whose id is found in the given collection. 53 | Collection getMessages(Collection ids); 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/codeu/chat/common/Conversation.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codeu.chat.common; 16 | 17 | import java.io.IOException; 18 | import java.io.InputStream; 19 | import java.io.OutputStream; 20 | import java.util.Collection; 21 | import java.util.HashSet; 22 | 23 | import codeu.chat.util.Serializer; 24 | import codeu.chat.util.Serializers; 25 | import codeu.chat.util.Time; 26 | import codeu.chat.util.Uuid; 27 | 28 | public final class Conversation { 29 | 30 | public static final Serializer SERIALIZER = new Serializer() { 31 | 32 | @Override 33 | public void write(OutputStream out, Conversation value) throws IOException { 34 | 35 | Uuid.SERIALIZER.write(out, value.id); 36 | Uuid.SERIALIZER.write(out, value.owner); 37 | Time.SERIALIZER.write(out, value.creation); 38 | Serializers.STRING.write(out, value.title); 39 | Serializers.collection(Uuid.SERIALIZER).write(out, value.users); 40 | Uuid.SERIALIZER.write(out, value.firstMessage); 41 | Uuid.SERIALIZER.write(out, value.lastMessage); 42 | 43 | } 44 | 45 | @Override 46 | public Conversation read(InputStream in) throws IOException { 47 | 48 | final Conversation value = new Conversation( 49 | Uuid.SERIALIZER.read(in), 50 | Uuid.SERIALIZER.read(in), 51 | Time.SERIALIZER.read(in), 52 | Serializers.STRING.read(in) 53 | ); 54 | 55 | value.users.addAll(Serializers.collection(Uuid.SERIALIZER).read(in)); 56 | 57 | value.firstMessage = Uuid.SERIALIZER.read(in); 58 | value.lastMessage = Uuid.SERIALIZER.read(in); 59 | 60 | return value; 61 | 62 | } 63 | }; 64 | 65 | public final ConversationSummary summary; 66 | 67 | public final Uuid id; 68 | public final Uuid owner; 69 | public final Time creation; 70 | public final String title; 71 | public final Collection users = new HashSet<>(); 72 | public Uuid firstMessage = Uuid.NULL; 73 | public Uuid lastMessage = Uuid.NULL; 74 | 75 | public Conversation(Uuid id, Uuid owner, Time creation, String title) { 76 | 77 | this.id = id; 78 | this.owner = owner; 79 | this.creation = creation; 80 | this.title = title; 81 | 82 | this.summary = new ConversationSummary(id, owner, creation, title); 83 | 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/codeu/chat/common/ConversationSummary.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codeu.chat.common; 16 | 17 | import java.io.IOException; 18 | import java.io.InputStream; 19 | import java.io.OutputStream; 20 | 21 | import codeu.chat.util.Serializer; 22 | import codeu.chat.util.Serializers; 23 | import codeu.chat.util.Time; 24 | import codeu.chat.util.Uuid; 25 | 26 | public final class ConversationSummary implements ListViewable { 27 | 28 | public static final Serializer SERIALIZER = new Serializer() { 29 | 30 | @Override 31 | public void write(OutputStream out, ConversationSummary value) throws IOException { 32 | 33 | Uuid.SERIALIZER.write(out, value.id); 34 | Uuid.SERIALIZER.write(out, value.owner); 35 | Time.SERIALIZER.write(out, value.creation); 36 | Serializers.STRING.write(out, value.title); 37 | 38 | } 39 | 40 | @Override 41 | public ConversationSummary read(InputStream in) throws IOException { 42 | 43 | return new ConversationSummary( 44 | Uuid.SERIALIZER.read(in), 45 | Uuid.SERIALIZER.read(in), 46 | Time.SERIALIZER.read(in), 47 | Serializers.STRING.read(in) 48 | ); 49 | 50 | } 51 | }; 52 | 53 | public final Uuid id; 54 | public final Uuid owner; 55 | public final Time creation; 56 | public final String title; 57 | 58 | public ConversationSummary(Uuid id, Uuid owner, Time creation, String title) { 59 | 60 | this.id = id; 61 | this.owner = owner; 62 | this.creation = creation; 63 | this.title = title; 64 | 65 | } 66 | 67 | // How this object should appear in a user-viewable list 68 | @Override 69 | public String listView() { 70 | return title; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/codeu/chat/common/LinearUuidGenerator.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codeu.chat.common; 16 | 17 | import codeu.chat.util.Uuid; 18 | 19 | public final class LinearUuidGenerator implements Uuid.Generator { 20 | 21 | private final Uuid commonRoot; 22 | private final int start; 23 | private final int end; 24 | 25 | private int current; 26 | 27 | public LinearUuidGenerator(Uuid root, int start, int end) { 28 | this.commonRoot = root; 29 | this.start = start; 30 | this.end = end; 31 | this.current = start; 32 | } 33 | 34 | @Override 35 | public Uuid make() { 36 | return new Uuid(commonRoot, next()); 37 | } 38 | 39 | private int next() { 40 | if (current == end) { 41 | throw new IllegalStateException("Uuid overflow"); 42 | } else { 43 | current++; 44 | return current; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/codeu/chat/common/ListViewable.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codeu.chat.common; 16 | 17 | public interface ListViewable { 18 | 19 | // Produce a string for presenting this object within a list. 20 | String listView(); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/codeu/chat/common/LogicalView.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codeu.chat.common; 16 | 17 | import java.util.Collection; 18 | 19 | import codeu.chat.util.Time; 20 | import codeu.chat.util.Uuid; 21 | 22 | // LOGICAL VIEW 23 | // 24 | // The logical view is another view for the Model-View-Control pattern. This view 25 | // focuses on providing more logical methods of accessing data. Each function is 26 | // based on a query rather than fetching specific objects. 27 | public interface LogicalView { 28 | 29 | // GET USER GENERATION 30 | // 31 | // Get an identifier that specifies the generation of all users. Storing and 32 | // tracking this number will allow checking if it is worth fetching all users. 33 | Uuid getUserGeneration(); 34 | 35 | // GET USERS EXCLUDING 36 | // 37 | // Get all users whose ID are not found in the given set of ids. 38 | Collection getUsersExcluding(Collection ids); 39 | 40 | // GET CONVERSATIONS 41 | // 42 | // Get a collection of conversations given the start and end of a time series. 43 | // all conversations that are found to have been created between the start 44 | // and end time will be returned. 45 | Collection getConversations(Time start, Time end); 46 | 47 | // GET CONVERSATIONS 48 | // 49 | // Get a collection of conversations given a regex expression that will be 50 | // used against every conversation's title. All conversations whose title 51 | // matches the given regex expression will be returned. 52 | Collection getConversations(String filter); 53 | 54 | // GET MESSAGES 55 | // 56 | // Get all messages from a single conversation whose time value falls 57 | // between the start and end times. If the conversation is not found, 58 | // or the start time is invalid, or the end time is invalid, no 59 | // messages will be returned. 60 | Collection getMessages(Uuid conversation, Time start, Time end); 61 | 62 | // GET MESSAGES 63 | // 64 | // Get all messages within the specified range starting from the given 65 | // message id. If the range is greater than zero, all messages after the 66 | // given messages up to and including |range| will be returned. If the 67 | // range is negative, all messages before the given message up to and 68 | // including |range| will be returned. If the root message is not found 69 | // no messages will be returned. 70 | Collection getMessages(Uuid rootMessage, int range); 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/codeu/chat/common/Message.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codeu.chat.common; 16 | 17 | import java.io.IOException; 18 | import java.io.InputStream; 19 | import java.io.OutputStream; 20 | 21 | import codeu.chat.util.Serializer; 22 | import codeu.chat.util.Serializers; 23 | import codeu.chat.util.Time; 24 | import codeu.chat.util.Uuid; 25 | 26 | public final class Message { 27 | 28 | public static final Serializer SERIALIZER = new Serializer() { 29 | 30 | @Override 31 | public void write(OutputStream out, Message value) throws IOException { 32 | 33 | Uuid.SERIALIZER.write(out, value.id); 34 | Uuid.SERIALIZER.write(out, value.next); 35 | Uuid.SERIALIZER.write(out, value.previous); 36 | Time.SERIALIZER.write(out, value.creation); 37 | Uuid.SERIALIZER.write(out, value.author); 38 | Serializers.STRING.write(out, value.content); 39 | 40 | } 41 | 42 | @Override 43 | public Message read(InputStream in) throws IOException { 44 | 45 | return new Message( 46 | Uuid.SERIALIZER.read(in), 47 | Uuid.SERIALIZER.read(in), 48 | Uuid.SERIALIZER.read(in), 49 | Time.SERIALIZER.read(in), 50 | Uuid.SERIALIZER.read(in), 51 | Serializers.STRING.read(in) 52 | ); 53 | 54 | } 55 | }; 56 | 57 | public final Uuid id; 58 | public final Uuid previous; 59 | public final Time creation; 60 | public final Uuid author; 61 | public final String content; 62 | public Uuid next; 63 | 64 | public Message(Uuid id, Uuid next, Uuid previous, Time creation, Uuid author, String content) { 65 | 66 | this.id = id; 67 | this.next = next; 68 | this.previous = previous; 69 | this.creation = creation; 70 | this.author = author; 71 | this.content = content; 72 | 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/codeu/chat/common/NetworkCode.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codeu.chat.common; 16 | 17 | public final class NetworkCode { 18 | 19 | public static final int 20 | NO_MESSAGE = 0, 21 | GET_USERS_BY_ID_REQUEST = 1, 22 | GET_USERS_BY_ID_RESPONSE = 2, 23 | GET_ALL_CONVERSATIONS_REQUEST = 3, 24 | GET_ALL_CONVERSATIONS_RESPONSE = 4, 25 | GET_CONVERSATIONS_BY_ID_RESPONSE = 5, 26 | GET_CONVERSATIONS_BY_ID_REQUEST = 6, 27 | GET_MESSAGES_BY_ID_REQUEST = 7, 28 | GET_MESSAGES_BY_ID_RESPONSE = 8, 29 | NEW_MESSAGE_REQUEST = 9, 30 | NEW_MESSAGE_RESPONSE = 10, 31 | NEW_USER_REQUEST = 11, 32 | NEW_USER_RESPONSE = 12, 33 | NEW_CONVERSATION_REQUEST = 13, 34 | NEW_CONVERSATION_RESPONSE = 14, 35 | GET_CONVERSATIONS_BY_TIME_REQUEST = 15, 36 | GET_CONVERSATIONS_BY_TIME_RESPONSE = 16, 37 | GET_CONVERSATIONS_BY_TITLE_REQUEST = 17, 38 | GET_CONVERSATIONS_BY_TITLE_RESPONSE = 18, 39 | GET_MESSAGES_BY_TIME_REQUEST = 19, 40 | GET_MESSAGES_BY_TIME_RESPONSE = 20, 41 | GET_MESSAGES_BY_RANGE_REQUEST = 21, 42 | GET_MESSAGES_BY_RANGE_RESPONSE = 22, 43 | GET_USER_GENERATION_REQUEST = 23, 44 | GET_USER_GENERATION_RESPONSE = 24, 45 | GET_USERS_EXCLUDING_REQUEST = 25, 46 | GET_USERS_EXCLUDING_RESPONSE = 26, 47 | RELAY_READ_REQUEST = 27, 48 | RELAY_READ_RESPONSE = 28, 49 | RELAY_WRITE_REQUEST = 29, 50 | RELAY_WRITE_RESPONSE = 30; 51 | } 52 | -------------------------------------------------------------------------------- /src/codeu/chat/common/RandomUuidGenerator.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codeu.chat.common; 16 | 17 | import java.util.Random; 18 | 19 | import codeu.chat.util.Uuid; 20 | 21 | // Create a new random uuid. Uuids from this generator are random 22 | // but are not guaranteed to be unique. Checking uniqueness is left 23 | // to the caller. 24 | public final class RandomUuidGenerator implements Uuid.Generator { 25 | 26 | private final Uuid commonRoot; 27 | private final Random random; 28 | 29 | public RandomUuidGenerator(Uuid root, long seed) { 30 | this.commonRoot = root; 31 | this.random = new Random(seed); 32 | } 33 | 34 | @Override 35 | public Uuid make() { 36 | return new Uuid(commonRoot, random.nextInt()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/codeu/chat/common/RawController.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codeu.chat.common; 16 | 17 | import codeu.chat.util.Time; 18 | import codeu.chat.util.Uuid; 19 | 20 | // RAW CONTROLLER 21 | // 22 | // A controller that grants a large amount of control over how data is inserted 23 | // into the model. If there is a conflict in data, the call will be rejected and 24 | // a null value returned. 25 | public interface RawController { 26 | 27 | // NEW MESSAGE 28 | // 29 | // Add a new message to the model with a specific id. If the id is already 30 | // in use, the call will fail and null will be returned. 31 | Message newMessage(Uuid id, Uuid author, Uuid conversation, String body, Time creationTime); 32 | 33 | // NEW USER 34 | // 35 | // Add a new user to the model with a specific id. If the id is already in 36 | // use, the call will fail and null will be returned. 37 | User newUser(Uuid id, String name, Time creationTime); 38 | 39 | // NEW CONVERSATION 40 | // 41 | // Add a new conversation to the model with a specific if. If the id is 42 | // already in use, the call will fail and null will be returned. 43 | Conversation newConversation(Uuid id, String title, Uuid owner, Time creationTime); 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/codeu/chat/common/Relay.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codeu.chat.common; 16 | 17 | import java.util.Collection; 18 | 19 | import codeu.chat.util.Time; 20 | import codeu.chat.util.Uuid; 21 | 22 | // RELAY 23 | // 24 | // This is the interface for communicating with the relay. Communication with the relay 25 | // is limited to two simple interfaces (writes and reads). As the relay is not expected 26 | // to track conversations or users, each bundle sent to the relay and received from the 27 | // the relay must be atomic and contain all information about the user, conversation, and 28 | // message. 29 | public interface Relay { 30 | 31 | // BUNDLE 32 | // 33 | // The relay's representation of what a message looks like. As the relay is not 34 | // going to track users and conversations so all information about a message must 35 | // be in the bundle. This means that each bundle will have a copy of the author's 36 | // id and name, a copy of the conversation's id and title. All information about 37 | // a message must be present for a server to recreate the series of events with 38 | // as little reasoning as possible. 39 | interface Bundle { 40 | 41 | // COMPONENT 42 | // 43 | // As there is a lot of similar information in a bundle. Component groups together 44 | // common fields to make the bundle interface easier to read. As a bundle is made-up 45 | // of three parts (user, conversation, and message) and each parts have a uuid, 46 | // string, and time field it cluttered the interface. A commonent is just a wrapper 47 | // to make the Bundle interface easier to read. 48 | interface Component { 49 | 50 | // ID 51 | // 52 | // The id for the component. As just about every piece of data has an ID, this 53 | // id is the id for the piece of data that the component is pointing to. 54 | Uuid id(); 55 | 56 | // TEXT 57 | // 58 | // The text for the component. As just about every piece of data has a string value, 59 | // this string is used to represent that string value. For a user it will be their 60 | // name, for a conversation it is the title, and for a message it is the content. 61 | String text(); 62 | 63 | // TIME 64 | // 65 | // The time for the component. As just about every piece of data has a time value, 66 | // this time is used to represent that time value. For messages, users, and conversations 67 | // this is the creation time. 68 | Time time(); 69 | 70 | } 71 | 72 | // ID 73 | // 74 | // The id for the bundle. This is the id of the bundle which can be used with read as the root. 75 | Uuid id(); 76 | 77 | // ID 78 | // 79 | // The time for when the bundle was created on the relay. This is the time that the bundle was 80 | // created on the relay and not the time the message was created on the server. 81 | Time time(); 82 | 83 | // TEAM 84 | // 85 | // This id of the team that owns this message. This is the id of the team that sent the bundle 86 | // to the relay. 87 | Uuid team(); 88 | 89 | // USER 90 | // 91 | // All the information about the user who authored the message. 92 | Component user(); 93 | 94 | // CONVERSATION 95 | // 96 | // All the infromation about the conversation that the message is part of. 97 | Component conversation(); 98 | 99 | // MESSAGE 100 | // 101 | // All the information about the message that was sent from the server to 102 | // the relay. 103 | Component message(); 104 | 105 | } 106 | 107 | // PACK 108 | // 109 | // Pack together a uuid, string, and time into a component. This is to make 110 | // the signature for "write" to be shorter and easier to read. 111 | Bundle.Component pack(Uuid id, String text, Time time); 112 | 113 | // WRITE 114 | // 115 | // Write a single message and all its extra data to the relay server. A message 116 | // must have information about the user and conversation that the message is 117 | // part of as the relay does not track users or conversations. In order to write 118 | // a message to the relay, a team must write their team id and team secret or 119 | // else the relay will reject the message. 120 | boolean write(Uuid teamId, 121 | byte[] teamSecret, 122 | Bundle.Component user, 123 | Bundle.Component conversation, 124 | Bundle.Component message); 125 | 126 | // READ 127 | // 128 | // Read a series of bundles from the relay. Given a Uuid as the starting point 129 | // the relay will return up to but may return less than the range. The range must 130 | // be positive. Negative ranges are not allowed and will return an empty 131 | // collection. If the root is Uuids.NULL then the relay will start sending from 132 | // its earliest point. If the root is not found the relay will treat it as if it 133 | // was given Uuids.NULL. 134 | Collection read(Uuid teamId, byte[] teamSecret, Uuid root, int range); 135 | 136 | } 137 | -------------------------------------------------------------------------------- /src/codeu/chat/common/Secret.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codeu.chat.common; 16 | 17 | public final class Secret { 18 | 19 | // PARSE 20 | // 21 | // Take in the string representation of the secret and convert it into a byte 22 | // array. The string form of a secret should be a hex string that will be 23 | // converted into a byte array. 24 | // 25 | // For example: "ABABAB" becomes { 0xAB, 0xAB, 0xAB } 26 | public static byte[] parse(String stringSecret) { 27 | 28 | final byte[] expanded = new byte[stringSecret.length() + stringSecret.length() % 2]; 29 | 30 | final int offset = stringSecret.length() % 2; 31 | 32 | for (int i = 0; i < stringSecret.length(); i++) { 33 | expanded[offset + i] = (byte) toHex(stringSecret.charAt(i)); 34 | } 35 | 36 | final byte[] compressed = new byte[expanded.length / 2]; 37 | 38 | for (int i = 0; i < compressed.length; i++) { 39 | compressed[i] = (byte) ((expanded[2 * i] << 4) | expanded[2 * i + 1]); 40 | } 41 | 42 | return compressed; 43 | } 44 | 45 | private static final int toHex(char c) { 46 | 47 | // If an invalid value was given, it will be treated as 0. 48 | 49 | switch (c) { 50 | case '0': 51 | case '1': 52 | case '2': 53 | case '3': 54 | case '4': 55 | case '5': 56 | case '6': 57 | case '7': 58 | case '8': 59 | case '9': 60 | return c - '0'; 61 | 62 | case 'a': 63 | case 'b': 64 | case 'c': 65 | case 'd': 66 | case 'e': 67 | case 'f': 68 | return c - 'a' + 10; 69 | 70 | case 'A': 71 | case 'B': 72 | case 'C': 73 | case 'D': 74 | case 'E': 75 | case 'F': 76 | return c - 'A' + 10; 77 | 78 | default: 79 | return 0; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/codeu/chat/common/SinglesView.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codeu.chat.common; 16 | 17 | import codeu.chat.common.Conversation; 18 | import codeu.chat.common.Message; 19 | import codeu.chat.common.User; 20 | import codeu.chat.util.Uuid; 21 | 22 | // SINGLES VIEW 23 | // 24 | // A view as part of the Model-View-Controller pattern. This view is 25 | // responsible for allowing single value reading from the model. 26 | public interface SinglesView { 27 | 28 | // FIND USER 29 | // 30 | // Find the user whose id matches the given id. If no user's id matches 31 | // the given id, null with be returned. 32 | User findUser(Uuid id); 33 | 34 | // FIND CONVERSATION 35 | // 36 | // Find the conversation whose id matches the given id. If no conversation's 37 | // matches the given id, null will be returned. 38 | Conversation findConversation(Uuid id); 39 | 40 | // FIND MESSAGE 41 | // 42 | // Find the message whose id matches the given id. if no message's id 43 | // matches the given id, null will be returned. 44 | Message findMessage(Uuid id); 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/codeu/chat/common/User.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codeu.chat.common; 16 | 17 | import java.io.IOException; 18 | import java.io.InputStream; 19 | import java.io.OutputStream; 20 | 21 | import codeu.chat.util.Serializer; 22 | import codeu.chat.util.Serializers; 23 | import codeu.chat.util.Time; 24 | import codeu.chat.util.Uuid; 25 | 26 | public final class User { 27 | 28 | public static final Serializer SERIALIZER = new Serializer() { 29 | 30 | @Override 31 | public void write(OutputStream out, User value) throws IOException { 32 | 33 | Uuid.SERIALIZER.write(out, value.id); 34 | Serializers.STRING.write(out, value.name); 35 | Time.SERIALIZER.write(out, value.creation); 36 | 37 | } 38 | 39 | @Override 40 | public User read(InputStream in) throws IOException { 41 | 42 | return new User( 43 | Uuid.SERIALIZER.read(in), 44 | Serializers.STRING.read(in), 45 | Time.SERIALIZER.read(in) 46 | ); 47 | 48 | } 49 | }; 50 | 51 | public final Uuid id; 52 | public final String name; 53 | public final Time creation; 54 | 55 | public User(Uuid id, String name, Time creation) { 56 | 57 | this.id = id; 58 | this.name = name; 59 | this.creation = creation; 60 | 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/codeu/chat/relay/Server.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codeu.chat.relay; 16 | 17 | import java.util.ArrayList; 18 | import java.util.Arrays; 19 | import java.util.Collection; 20 | import java.util.HashMap; 21 | import java.util.LinkedList; 22 | import java.util.Map; 23 | import java.util.Queue; 24 | 25 | import codeu.chat.common.LinearUuidGenerator; 26 | import codeu.chat.common.Relay; 27 | import codeu.chat.util.Logger; 28 | import codeu.chat.util.Time; 29 | import codeu.chat.util.Logger; 30 | import codeu.chat.util.Uuid; 31 | 32 | public final class Server implements Relay { 33 | 34 | private final static Logger.Log LOG = Logger.newLog(Server.class); 35 | 36 | private static final class Component implements Relay.Bundle.Component { 37 | 38 | private final Uuid id; 39 | private final String text; 40 | private final Time time; 41 | 42 | public Component(Uuid id, String text, Time time) { 43 | this.id = id; 44 | this.text = text; 45 | this.time = time; 46 | } 47 | 48 | @Override 49 | public Uuid id() { return id; } 50 | 51 | @Override 52 | public String text() { return text; } 53 | 54 | @Override 55 | public Time time() { return time; } 56 | 57 | } 58 | 59 | private static final class Bundle implements Relay.Bundle { 60 | 61 | private final Uuid id; 62 | private final Time time; 63 | private final Uuid team; 64 | private final Component user; 65 | private final Component conversation; 66 | private final Component message; 67 | 68 | public Bundle(Uuid id, 69 | Time time, 70 | Uuid team, 71 | Component user, 72 | Component conversation, 73 | Component message) { 74 | 75 | this.id = id; 76 | this.time = time; 77 | this.team = team; 78 | this.user = user; 79 | this.conversation = conversation; 80 | this.message = message; 81 | 82 | } 83 | 84 | @Override 85 | public Uuid id() { return id; } 86 | 87 | @Override 88 | public Time time() { return time; } 89 | 90 | @Override 91 | public Uuid team() { return team; } 92 | 93 | @Override 94 | public Component user() { return user; } 95 | 96 | @Override 97 | public Component conversation() { return conversation; } 98 | 99 | @Override 100 | public Component message() { return message; } 101 | 102 | } 103 | 104 | private final Queue history = new LinkedList<>(); 105 | private final Map teamSecrets = new HashMap<>(); 106 | 107 | private final int maxHistory; 108 | private final int maxRead; 109 | 110 | // Okay, some reasoning behind why I'm using a statically initialized linear 111 | // generator for the ids for the relay server. 112 | // 113 | // Point A : The ids only need to be uniqiue for a single run time of the 114 | // relay. Ids from the relay are only used as a position into its 115 | // history. If it repeats an id its not a problem. 116 | // 117 | // Point B : The chance that the history would be so long that an id could 118 | // be reused and appear along side's twin is way too small. The 119 | // range for the ids is 1 to MAX INT (32 bit signed). This means 120 | // that there would need to be 2147483646 messages in memory. If 121 | // each message was 160 bytes long the relay server would need 122 | // over 319 GB of ram. 123 | // 124 | // As a side note, the ids start at 1 and not 0 to avoid the first id from 125 | // matching the NULL id which is defined as (null, 0); 126 | 127 | private final Uuid.Generator idGenerator = new LinearUuidGenerator(null, 1, Integer.MAX_VALUE); 128 | 129 | // SERVER 130 | // 131 | // When initializing the server keep the following in mind. 132 | // - Keep "maxHistory" small enough to avoid using too much memory. 133 | // - Keep "maxRead" small enough to avoid any one client from connecting to 134 | // the server for too long. 135 | public Server(int maxHistory, int maxRead) { 136 | this.maxHistory = Math.max(0, maxHistory); 137 | this.maxRead = Math.max(0, maxRead); 138 | } 139 | 140 | // ADD TEAM 141 | // 142 | // Let the relay know of a team's secret so that it will accept messages from that 143 | // team. If there is already a team entry, the secret will NOT be updated and the 144 | // call will return false. 145 | public boolean addTeam(Uuid id, byte[] secret) { 146 | 147 | LOG.info("Adding team to relay %s", id); 148 | 149 | final boolean open = teamSecrets.get(id) == null; 150 | 151 | if (open) { 152 | teamSecrets.put(id, secret); 153 | } 154 | 155 | LOG.info(open ? 156 | "Adding team was successful" : 157 | "Adding team failed - team id already exists"); 158 | 159 | return open; 160 | } 161 | 162 | @Override 163 | public Relay.Bundle.Component pack(Uuid id, String text, Time time) { 164 | return new Component(id, text, time); 165 | } 166 | 167 | @Override 168 | public boolean write(Uuid teamId, 169 | byte[] teamSecret, 170 | Relay.Bundle.Component user, 171 | Relay.Bundle.Component conversation, 172 | Relay.Bundle.Component message) { 173 | 174 | if (authenticate(teamId, teamSecret)) { 175 | 176 | LOG.info( 177 | "Writing to server team=%s user=%s conversation=%s message=%s", 178 | teamId, 179 | user.id(), 180 | conversation.id(), 181 | message.id()); 182 | 183 | if (history.size() >= maxHistory) { 184 | history.remove(); 185 | } 186 | 187 | return history.offer(new Bundle( 188 | idGenerator.make(), 189 | Time.now(), 190 | teamId, 191 | user, 192 | conversation, 193 | message)); 194 | } else { 195 | 196 | LOG.warning( 197 | "Unauthorized write attempt to server team=%s user=%s conversation=%s message=%s", 198 | teamId, 199 | user.id(), 200 | conversation.id(), 201 | message.id()); 202 | 203 | return false; 204 | } 205 | } 206 | 207 | @Override 208 | public Collection read(Uuid teamId, byte[] teamSecret, Uuid root, int range) { 209 | 210 | final Collection found = new ArrayList<>(); 211 | 212 | if (authenticate(teamId, teamSecret)) { 213 | 214 | LOG.info( 215 | "Request to read from server requested=%d allowed=%d", 216 | range, 217 | maxRead); 218 | 219 | for (final Relay.Bundle message : history) { 220 | 221 | // Only add a message if there is room. We cannot stop 222 | // searching in case we see the root later on. 223 | if (found.size() < Math.min(range, maxRead)) { 224 | found.add(message); 225 | } 226 | 227 | // If the start is found, drop all previous messages. 228 | if (message.id().equals(root)) { 229 | found.clear(); 230 | } 231 | } 232 | 233 | LOG.info( 234 | "Read request complete requested=%d fullfilled=%d", 235 | range, 236 | found.size()); 237 | 238 | } else { 239 | 240 | LOG.info( 241 | "Unauthroized attempt to read from server team=%s", 242 | teamId); 243 | } 244 | 245 | return found; 246 | } 247 | 248 | private boolean authenticate(Uuid id, byte[] secret) { 249 | return id != null && Arrays.equals(secret, teamSecrets.get(id)); 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/codeu/chat/relay/ServerFrontEnd.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codeu.chat.relay; 16 | 17 | import java.io.IOException; 18 | import java.io.InputStream; 19 | import java.io.OutputStream; 20 | import java.util.Collection; 21 | 22 | import codeu.chat.common.NetworkCode; 23 | import codeu.chat.common.Relay; 24 | import codeu.chat.util.Logger; 25 | import codeu.chat.util.Serializer; 26 | import codeu.chat.util.Serializers; 27 | import codeu.chat.util.Time; 28 | import codeu.chat.util.Uuid; 29 | import codeu.chat.util.connections.Connection; 30 | 31 | public final class ServerFrontEnd { 32 | 33 | private final static Logger.Log LOG = Logger.newLog(ServerFrontEnd.class); 34 | 35 | private static final Serializer COMPONENT_SERIALIZER = 36 | new Serializer() { 37 | 38 | @Override 39 | public Relay.Bundle.Component read(InputStream in) throws IOException { 40 | 41 | final Uuid id = Uuid.SERIALIZER.read(in); 42 | final String text = Serializers.STRING.read(in); 43 | final Time time = Time.SERIALIZER.read(in); 44 | 45 | // I could have passed the relay and use its "pack" method but that would 46 | // have been more work than just building an object here. 47 | return new Relay.Bundle.Component() { 48 | @Override 49 | public Uuid id() { return id; } 50 | @Override 51 | public String text() { return text; } 52 | @Override 53 | public Time time() { return time; } 54 | }; 55 | } 56 | 57 | @Override 58 | public void write(OutputStream out, Relay.Bundle.Component value) throws IOException { 59 | Uuid.SERIALIZER.write(out, value.id()); 60 | Serializers.STRING.write(out, value.text()); 61 | Time.SERIALIZER.write(out, value.time()); 62 | } 63 | }; 64 | 65 | private static final Serializer BUNDLE_SERIALIZER = 66 | new Serializer() { 67 | 68 | @Override 69 | public Relay.Bundle read(InputStream in) throws IOException { 70 | 71 | final Uuid id = Uuid.SERIALIZER.read(in); 72 | final Time time = Time.SERIALIZER.read(in); 73 | final Uuid team = Uuid.SERIALIZER.read(in); 74 | final Relay.Bundle.Component user = COMPONENT_SERIALIZER.read(in); 75 | final Relay.Bundle.Component conversation = COMPONENT_SERIALIZER.read(in); 76 | final Relay.Bundle.Component message = COMPONENT_SERIALIZER.read(in); 77 | 78 | return new Relay.Bundle() { 79 | @Override 80 | public Uuid id() { return id; } 81 | @Override 82 | public Time time() { return time; } 83 | @Override 84 | public Uuid team() { return team; } 85 | @Override 86 | public Relay.Bundle.Component user() { return user; } 87 | @Override 88 | public Relay.Bundle.Component conversation() { return conversation; } 89 | @Override 90 | public Relay.Bundle.Component message() { return message; } 91 | }; 92 | } 93 | 94 | @Override 95 | public void write(OutputStream out, Relay.Bundle value) throws IOException { 96 | Uuid.SERIALIZER.write(out, value.id()); 97 | Time.SERIALIZER.write(out, value.time()); 98 | Uuid.SERIALIZER.write(out, value.team()); 99 | COMPONENT_SERIALIZER.write(out, value.user()); 100 | COMPONENT_SERIALIZER.write(out, value.conversation()); 101 | COMPONENT_SERIALIZER.write(out, value.message()); 102 | } 103 | }; 104 | 105 | private final Relay backEnd; 106 | 107 | public ServerFrontEnd(Relay backEnd) { 108 | this.backEnd = backEnd; 109 | } 110 | 111 | public void handleConnection(Connection connection) throws IOException { 112 | 113 | LOG.info("Handling Connection - start"); 114 | 115 | switch (Serializers.INTEGER.read(connection.in())) { 116 | case NetworkCode.RELAY_READ_REQUEST: handleReadMessage(connection); break; 117 | case NetworkCode.RELAY_WRITE_REQUEST: handleWriteMessage(connection); break; 118 | } 119 | 120 | LOG.info("Handling Connection - end"); 121 | } 122 | 123 | private void handleReadMessage(Connection connection) throws IOException { 124 | 125 | LOG.info("Handling Read Message - start"); 126 | 127 | final Uuid teamId = Uuid.SERIALIZER.read(connection.in()); 128 | final byte[] teamSecret = Serializers.BYTES.read(connection.in()); 129 | final Uuid root = Uuid.SERIALIZER.read(connection.in()); 130 | final int range = Serializers.INTEGER.read(connection.in()); 131 | 132 | LOG.info( 133 | "Reading team=%s root=%s range=%d", 134 | teamId, 135 | root, 136 | range); 137 | 138 | final Collection result = backEnd.read(teamId, teamSecret, root, range); 139 | 140 | LOG.info("Reading result.size=%d", result.size()); 141 | 142 | Serializers.INTEGER.write(connection.out(), NetworkCode.RELAY_READ_RESPONSE); 143 | Serializers.collection(BUNDLE_SERIALIZER).write(connection.out(), result); 144 | 145 | LOG.info("Handling Read Message - end"); 146 | } 147 | 148 | private void handleWriteMessage(Connection connection) throws IOException { 149 | 150 | LOG.info("Handling Write Message - start"); 151 | 152 | final Uuid teamId = Uuid.SERIALIZER.read(connection.in()); 153 | final byte[] teamSecret = Serializers.BYTES.read(connection.in()); 154 | final Relay.Bundle.Component user = COMPONENT_SERIALIZER.read(connection.in()); 155 | final Relay.Bundle.Component conversation = COMPONENT_SERIALIZER.read(connection.in()); 156 | final Relay.Bundle.Component message = COMPONENT_SERIALIZER.read(connection.in()); 157 | 158 | LOG.info( 159 | "Writing team=%s user=%s conversation=%s message=%s", 160 | teamId, 161 | user.id(), 162 | conversation.id(), 163 | message.id()); 164 | 165 | final boolean result = backEnd.write(teamId, 166 | teamSecret, 167 | user, 168 | conversation, 169 | message); 170 | 171 | LOG.info("Writing result=%s", result ? "success" : "fail"); 172 | 173 | Serializers.INTEGER.write(connection.out(), NetworkCode.RELAY_WRITE_RESPONSE); 174 | Serializers.BOOLEAN.write(connection.out(), result); 175 | 176 | LOG.info("Handling Write Message - end"); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/codeu/chat/server/Controller.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codeu.chat.server; 16 | 17 | import java.util.Collection; 18 | 19 | import codeu.chat.common.BasicController; 20 | import codeu.chat.common.Conversation; 21 | import codeu.chat.common.Message; 22 | import codeu.chat.common.RandomUuidGenerator; 23 | import codeu.chat.common.RawController; 24 | import codeu.chat.common.User; 25 | import codeu.chat.util.Logger; 26 | import codeu.chat.util.Time; 27 | import codeu.chat.util.Uuid; 28 | 29 | public final class Controller implements RawController, BasicController { 30 | 31 | private final static Logger.Log LOG = Logger.newLog(Controller.class); 32 | 33 | private final Model model; 34 | private final Uuid.Generator uuidGenerator; 35 | 36 | public Controller(Uuid serverId, Model model) { 37 | this.model = model; 38 | this.uuidGenerator = new RandomUuidGenerator(serverId, System.currentTimeMillis()); 39 | } 40 | 41 | @Override 42 | public Message newMessage(Uuid author, Uuid conversation, String body) { 43 | return newMessage(createId(), author, conversation, body, Time.now()); 44 | } 45 | 46 | @Override 47 | public User newUser(String name) { 48 | return newUser(createId(), name, Time.now()); 49 | } 50 | 51 | @Override 52 | public Conversation newConversation(String title, Uuid owner) { 53 | return newConversation(createId(), title, owner, Time.now()); 54 | } 55 | 56 | @Override 57 | public Message newMessage(Uuid id, Uuid author, Uuid conversation, String body, Time creationTime) { 58 | 59 | final User foundUser = model.userById().first(author); 60 | final Conversation foundConversation = model.conversationById().first(conversation); 61 | 62 | Message message = null; 63 | 64 | if (foundUser != null && foundConversation != null && isIdFree(id)) { 65 | 66 | message = new Message(id, Uuid.NULL, Uuid.NULL, creationTime, author, body); 67 | model.add(message); 68 | LOG.info("Message added: %s", message.id); 69 | 70 | // Find and update the previous "last" message so that it's "next" value 71 | // will point to the new message. 72 | 73 | if (Uuid.equals(foundConversation.lastMessage, Uuid.NULL)) { 74 | 75 | // The conversation has no messages in it, that's why the last message is NULL (the first 76 | // message should be NULL too. Since there is no last message, then it is not possible 77 | // to update the last message's "next" value. 78 | 79 | } else { 80 | final Message lastMessage = model.messageById().first(foundConversation.lastMessage); 81 | lastMessage.next = message.id; 82 | } 83 | 84 | // If the first message points to NULL it means that the conversation was empty and that 85 | // the first message should be set to the new message. Otherwise the message should 86 | // not change. 87 | 88 | foundConversation.firstMessage = 89 | Uuid.equals(foundConversation.firstMessage, Uuid.NULL) ? 90 | message.id : 91 | foundConversation.firstMessage; 92 | 93 | // Update the conversation to point to the new last message as it has changed. 94 | 95 | foundConversation.lastMessage = message.id; 96 | 97 | if (!foundConversation.users.contains(foundUser)) { 98 | foundConversation.users.add(foundUser.id); 99 | } 100 | } 101 | 102 | return message; 103 | } 104 | 105 | @Override 106 | public User newUser(Uuid id, String name, Time creationTime) { 107 | 108 | User user = null; 109 | 110 | if (isIdFree(id)) { 111 | 112 | user = new User(id, name, creationTime); 113 | model.add(user); 114 | 115 | LOG.info( 116 | "newUser success (user.id=%s user.name=%s user.time=%s)", 117 | id, 118 | name, 119 | creationTime); 120 | 121 | } else { 122 | 123 | LOG.info( 124 | "newUser fail - id in use (user.id=%s user.name=%s user.time=%s)", 125 | id, 126 | name, 127 | creationTime); 128 | } 129 | 130 | return user; 131 | } 132 | 133 | @Override 134 | public Conversation newConversation(Uuid id, String title, Uuid owner, Time creationTime) { 135 | 136 | final User foundOwner = model.userById().first(owner); 137 | 138 | Conversation conversation = null; 139 | 140 | if (foundOwner != null && isIdFree(id)) { 141 | conversation = new Conversation(id, owner, creationTime, title); 142 | model.add(conversation); 143 | 144 | LOG.info("Conversation added: " + conversation.id); 145 | } 146 | 147 | return conversation; 148 | } 149 | 150 | private Uuid createId() { 151 | 152 | Uuid candidate; 153 | 154 | for (candidate = uuidGenerator.make(); 155 | isIdInUse(candidate); 156 | candidate = uuidGenerator.make()) { 157 | 158 | // Assuming that "randomUuid" is actually well implemented, this 159 | // loop should never be needed, but just incase make sure that the 160 | // Uuid is not actually in use before returning it. 161 | 162 | } 163 | 164 | return candidate; 165 | } 166 | 167 | private boolean isIdInUse(Uuid id) { 168 | return model.messageById().first(id) != null || 169 | model.conversationById().first(id) != null || 170 | model.userById().first(id) != null; 171 | } 172 | 173 | private boolean isIdFree(Uuid id) { return !isIdInUse(id); } 174 | 175 | } 176 | -------------------------------------------------------------------------------- /src/codeu/chat/server/Model.java: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Google Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package codeu.chat.server; 16 | 17 | import java.util.Comparator; 18 | 19 | import codeu.chat.common.Conversation; 20 | import codeu.chat.common.ConversationSummary; 21 | import codeu.chat.common.LinearUuidGenerator; 22 | import codeu.chat.common.Message; 23 | import codeu.chat.common.User; 24 | import codeu.chat.util.Time; 25 | import codeu.chat.util.Uuid; 26 | import codeu.chat.util.store.Store; 27 | import codeu.chat.util.store.StoreAccessor; 28 | 29 | public final class Model { 30 | 31 | private static final Comparator UUID_COMPARE = new Comparator() { 32 | 33 | @Override 34 | public int compare(Uuid a, Uuid b) { 35 | 36 | if (a == b) { return 0; } 37 | 38 | if (a == null && b != null) { return -1; } 39 | 40 | if (a != null && b == null) { return 1; } 41 | 42 | final int order = Integer.compare(a.id(), b.id()); 43 | return order == 0 ? compare(a.root(), b.root()) : order; 44 | } 45 | }; 46 | 47 | private static final Comparator