├── front-chat ├── src │ ├── App.css │ ├── index.css │ ├── assets │ │ ├── chat.png │ │ └── react.svg │ ├── config │ │ ├── AxiosHelper.js │ │ ├── Routes.jsx │ │ └── helper.js │ ├── App.jsx │ ├── main.jsx │ ├── services │ │ └── RoomService.js │ ├── context │ │ └── ChatContext.jsx │ └── components │ │ ├── JoinCreateChat.jsx │ │ └── ChatPage.jsx ├── postcss.config.js ├── vite.config.js ├── tailwind.config.js ├── .gitignore ├── README.md ├── index.html ├── package.json ├── eslint.config.js └── public │ └── vite.svg ├── chat-app-backend ├── .gitattributes ├── src │ ├── main │ │ ├── resources │ │ │ └── application.properties │ │ └── java │ │ │ └── com │ │ │ └── substring │ │ │ └── chat │ │ │ ├── repositories │ │ │ └── RoomRepository.java │ │ │ ├── ChatAppBackendApplication.java │ │ │ ├── playload │ │ │ └── MessageRequest.java │ │ │ ├── entities │ │ │ ├── Message.java │ │ │ └── Room.java │ │ │ ├── config │ │ │ └── WebSocketConfig.java │ │ │ └── controllers │ │ │ ├── ChatController.java │ │ │ └── RoomController.java │ └── test │ │ └── java │ │ └── com │ │ └── substring │ │ └── chat │ │ └── ChatAppBackendApplicationTests.java ├── .gitignore ├── .mvn │ └── wrapper │ │ └── maven-wrapper.properties ├── pom.xml ├── mvnw.cmd └── mvnw └── .DS_Store /front-chat/src/App.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chat-app-backend/.gitattributes: -------------------------------------------------------------------------------- 1 | /mvnw text eol=lf 2 | *.cmd text eol=crlf 3 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearnCodeWithDurgesh/chat-app/HEAD/.DS_Store -------------------------------------------------------------------------------- /front-chat/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /front-chat/src/assets/chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LearnCodeWithDurgesh/chat-app/HEAD/front-chat/src/assets/chat.png -------------------------------------------------------------------------------- /front-chat/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /chat-app-backend/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=chat-app-backend 2 | 3 | spring.data.mongodb.uri=mongodb://localhost:27017/chatapp -------------------------------------------------------------------------------- /front-chat/src/config/AxiosHelper.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | export const baseURL = "http://localhost:8080"; 3 | export const httpClient = axios.create({ 4 | baseURL: baseURL, 5 | }); 6 | -------------------------------------------------------------------------------- /front-chat/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /front-chat/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | darkMode: "class", 9 | }; 10 | -------------------------------------------------------------------------------- /chat-app-backend/src/test/java/com/substring/chat/ChatAppBackendApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.substring.chat; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class ChatAppBackendApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /front-chat/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /chat-app-backend/src/main/java/com/substring/chat/repositories/RoomRepository.java: -------------------------------------------------------------------------------- 1 | package com.substring.chat.repositories; 2 | 3 | 4 | import com.substring.chat.entities.Room; 5 | import org.springframework.data.mongodb.repository.MongoRepository; 6 | 7 | public interface RoomRepository extends MongoRepository { 8 | //get room using room id 9 | Room findByRoomId(String roomId); 10 | } 11 | -------------------------------------------------------------------------------- /chat-app-backend/src/main/java/com/substring/chat/ChatAppBackendApplication.java: -------------------------------------------------------------------------------- 1 | package com.substring.chat; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class ChatAppBackendApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(ChatAppBackendApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /front-chat/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import reactLogo from "./assets/react.svg"; 3 | import viteLogo from "/vite.svg"; 4 | import "./App.css"; 5 | import toast from "react-hot-toast"; 6 | import JoinCreateChat from "./components/JoinCreateChat"; 7 | 8 | function App() { 9 | const [count, setCount] = useState(0); 10 | 11 | return ( 12 |
13 | 14 |
15 | ); 16 | } 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /front-chat/README.md: -------------------------------------------------------------------------------- 1 | # React + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | -------------------------------------------------------------------------------- /chat-app-backend/src/main/java/com/substring/chat/playload/MessageRequest.java: -------------------------------------------------------------------------------- 1 | package com.substring.chat.playload; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | import java.time.LocalDateTime; 9 | 10 | @Setter 11 | @Getter 12 | @AllArgsConstructor 13 | @NoArgsConstructor 14 | public class MessageRequest { 15 | 16 | private String content; 17 | private String sender; 18 | private String roomId; 19 | 20 | 21 | } 22 | -------------------------------------------------------------------------------- /chat-app-backend/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/**/target/ 5 | !**/src/test/**/target/ 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | -------------------------------------------------------------------------------- /front-chat/src/config/Routes.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Routes, Route } from "react-router"; 3 | import App from "../App"; 4 | import ChatPage from "../components/ChatPage"; 5 | const AppRoutes = () => { 6 | return ( 7 | 8 | } /> 9 | } /> 10 | This is about page} /> 11 | 404 Page Not Found} /> 12 | 13 | ); 14 | }; 15 | 16 | export default AppRoutes; 17 | -------------------------------------------------------------------------------- /front-chat/src/main.jsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import "./index.css"; 4 | import { BrowserRouter } from "react-router"; 5 | import AppRoutes from "./config/routes.jsx"; 6 | import { Toaster } from "react-hot-toast"; 7 | import { ChatProvider } from "./context/ChatContext.jsx"; 8 | 9 | createRoot(document.getElementById("root")).render( 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /front-chat/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Room Chat App 8 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /chat-app-backend/src/main/java/com/substring/chat/entities/Message.java: -------------------------------------------------------------------------------- 1 | package com.substring.chat.entities; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | import java.time.LocalDateTime; 9 | 10 | @AllArgsConstructor 11 | @NoArgsConstructor 12 | @Getter 13 | @Setter 14 | public class Message { 15 | 16 | private String sender; 17 | private String content; 18 | private LocalDateTime timeStamp; 19 | 20 | public Message(String sender, String content) { 21 | this.sender = sender; 22 | this.content = content; 23 | this.timeStamp = LocalDateTime.now(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /chat-app-backend/src/main/java/com/substring/chat/entities/Room.java: -------------------------------------------------------------------------------- 1 | package com.substring.chat.entities; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | import org.springframework.data.annotation.Id; 8 | import org.springframework.data.mongodb.core.mapping.Document; 9 | 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | @Document(collection = "rooms") 14 | @Getter 15 | @Setter 16 | @NoArgsConstructor 17 | @AllArgsConstructor 18 | public class Room { 19 | @Id 20 | private String id;//Mongo db : unique identifier 21 | private String roomId; 22 | private List messages = new ArrayList<>(); 23 | 24 | 25 | } 26 | -------------------------------------------------------------------------------- /front-chat/src/services/RoomService.js: -------------------------------------------------------------------------------- 1 | import { httpClient } from "../config/AxiosHelper"; 2 | 3 | export const createRoomApi = async (roomDetail) => { 4 | const respone = await httpClient.post(`/api/v1/rooms`, roomDetail, { 5 | headers: { 6 | "Content-Type": "text/plain", 7 | }, 8 | }); 9 | return respone.data; 10 | }; 11 | 12 | export const joinChatApi = async (roomId) => { 13 | const response = await httpClient.get(`/api/v1/rooms/${roomId}`); 14 | return response.data; 15 | }; 16 | 17 | export const getMessagess = async (roomId, size = 50, page = 0) => { 18 | const response = await httpClient.get( 19 | `/api/v1/rooms/${roomId}/messages?size=${size}&page=${page}` 20 | ); 21 | return response.data; 22 | }; 23 | -------------------------------------------------------------------------------- /front-chat/src/context/ChatContext.jsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useState } from "react"; 2 | 3 | const ChatContext = createContext(); 4 | 5 | export const ChatProvider = ({ children }) => { 6 | const [roomId, setRoomId] = useState(""); 7 | const [currentUser, setCurrentUser] = useState(""); 8 | const [connected, setConnected] = useState(false); 9 | 10 | return ( 11 | 21 | {children} 22 | 23 | ); 24 | }; 25 | 26 | const useChatContext = () => useContext(ChatContext); 27 | export default useChatContext; 28 | -------------------------------------------------------------------------------- /front-chat/src/config/helper.js: -------------------------------------------------------------------------------- 1 | export function timeAgo(date) { 2 | const now = new Date(); 3 | const past = new Date(date); 4 | const secondsAgo = Math.floor((now - past) / 1000); 5 | 6 | if (secondsAgo < 60) return `${secondsAgo} seconds ago`; 7 | const minutesAgo = Math.floor(secondsAgo / 60); 8 | if (minutesAgo < 60) return `${minutesAgo} minutes ago`; 9 | const hoursAgo = Math.floor(minutesAgo / 60); 10 | if (hoursAgo < 24) return `${hoursAgo} hours ago`; 11 | const daysAgo = Math.floor(hoursAgo / 24); 12 | if (daysAgo < 30) return `${daysAgo} days ago`; 13 | const monthsAgo = Math.floor(daysAgo / 30); 14 | if (monthsAgo < 12) return `${monthsAgo} months ago`; 15 | const yearsAgo = Math.floor(monthsAgo / 12); 16 | return `${yearsAgo} years ago`; 17 | } 18 | 19 | // Example usage 20 | console.log(timeAgo("2023-12-01T14:00:00Z")); // Output depends on the current time 21 | -------------------------------------------------------------------------------- /chat-app-backend/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. 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, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 20 | -------------------------------------------------------------------------------- /front-chat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "front-chat", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@stomp/stompjs": "^7.0.0", 14 | "axios": "^1.7.9", 15 | "react": "^18.3.1", 16 | "react-dom": "^18.3.1", 17 | "react-hot-toast": "^2.4.1", 18 | "react-icons": "^5.4.0", 19 | "react-router": "^7.0.2", 20 | "sockjs-client": "^1.6.1" 21 | }, 22 | "devDependencies": { 23 | "@eslint/js": "^9.15.0", 24 | "@types/react": "^18.3.12", 25 | "@types/react-dom": "^18.3.1", 26 | "@vitejs/plugin-react": "^4.3.4", 27 | "autoprefixer": "^10.4.20", 28 | "eslint": "^9.15.0", 29 | "eslint-plugin-react": "^7.37.2", 30 | "eslint-plugin-react-hooks": "^5.0.0", 31 | "eslint-plugin-react-refresh": "^0.4.14", 32 | "globals": "^15.12.0", 33 | "postcss": "^8.4.49", 34 | "tailwindcss": "^3.4.16", 35 | "vite": "^6.0.1" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /front-chat/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import react from 'eslint-plugin-react' 4 | import reactHooks from 'eslint-plugin-react-hooks' 5 | import reactRefresh from 'eslint-plugin-react-refresh' 6 | 7 | export default [ 8 | { ignores: ['dist'] }, 9 | { 10 | files: ['**/*.{js,jsx}'], 11 | languageOptions: { 12 | ecmaVersion: 2020, 13 | globals: globals.browser, 14 | parserOptions: { 15 | ecmaVersion: 'latest', 16 | ecmaFeatures: { jsx: true }, 17 | sourceType: 'module', 18 | }, 19 | }, 20 | settings: { react: { version: '18.3' } }, 21 | plugins: { 22 | react, 23 | 'react-hooks': reactHooks, 24 | 'react-refresh': reactRefresh, 25 | }, 26 | rules: { 27 | ...js.configs.recommended.rules, 28 | ...react.configs.recommended.rules, 29 | ...react.configs['jsx-runtime'].rules, 30 | ...reactHooks.configs.recommended.rules, 31 | 'react/jsx-no-target-blank': 'off', 32 | 'react-refresh/only-export-components': [ 33 | 'warn', 34 | { allowConstantExport: true }, 35 | ], 36 | }, 37 | }, 38 | ] 39 | -------------------------------------------------------------------------------- /chat-app-backend/src/main/java/com/substring/chat/config/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package com.substring.chat.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.messaging.simp.config.MessageBrokerRegistry; 5 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; 6 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry; 7 | import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; 8 | 9 | @Configuration 10 | @EnableWebSocketMessageBroker 11 | public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { 12 | 13 | 14 | @Override 15 | public void configureMessageBroker(MessageBrokerRegistry config) { 16 | 17 | config.enableSimpleBroker("/topic"); 18 | // /topic/messages 19 | 20 | config.setApplicationDestinationPrefixes("/app"); 21 | // /app/chat 22 | // server-side: @MessagingMapping("/chat) 23 | 24 | 25 | } 26 | 27 | @Override 28 | public void registerStompEndpoints(StompEndpointRegistry registry) { 29 | registry.addEndpoint("/chat")//connection establishment 30 | .setAllowedOrigins("http://localhost:5173") 31 | .withSockJS(); 32 | } 33 | // /chat endpoint par connection apka establish hoga 34 | } 35 | -------------------------------------------------------------------------------- /front-chat/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /chat-app-backend/src/main/java/com/substring/chat/controllers/ChatController.java: -------------------------------------------------------------------------------- 1 | package com.substring.chat.controllers; 2 | 3 | import com.substring.chat.entities.Message; 4 | import com.substring.chat.entities.Room; 5 | import com.substring.chat.playload.MessageRequest; 6 | import com.substring.chat.repositories.RoomRepository; 7 | import org.springframework.messaging.handler.annotation.DestinationVariable; 8 | import org.springframework.messaging.handler.annotation.MessageMapping; 9 | import org.springframework.messaging.handler.annotation.SendTo; 10 | import org.springframework.stereotype.Controller; 11 | import org.springframework.web.bind.annotation.CrossOrigin; 12 | import org.springframework.web.bind.annotation.RequestBody; 13 | 14 | import java.time.LocalDateTime; 15 | 16 | @Controller 17 | @CrossOrigin("http://localhost:5173") 18 | public class ChatController { 19 | 20 | 21 | private RoomRepository roomRepository; 22 | 23 | public ChatController(RoomRepository roomRepository) { 24 | this.roomRepository = roomRepository; 25 | } 26 | 27 | 28 | //for sending and receiving messages 29 | @MessageMapping("/sendMessage/{roomId}")// /app/sendMessage/roomId 30 | @SendTo("/topic/room/{roomId}")//subscribe 31 | public Message sendMessage( 32 | @DestinationVariable String roomId, 33 | @RequestBody MessageRequest request 34 | ) { 35 | 36 | Room room = roomRepository.findByRoomId(request.getRoomId()); 37 | Message message = new Message(); 38 | message.setContent(request.getContent()); 39 | message.setSender(request.getSender()); 40 | message.setTimeStamp(LocalDateTime.now()); 41 | if (room != null) { 42 | room.getMessages().add(message); 43 | roomRepository.save(room); 44 | } else { 45 | throw new RuntimeException("room not found !!"); 46 | } 47 | 48 | return message; 49 | 50 | 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /chat-app-backend/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.4.0 9 | 10 | 11 | com.substring.chat 12 | chat-app-backend 13 | 0.0.1-SNAPSHOT 14 | chat-app-backend 15 | Chat app backend 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 21 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-data-mongodb 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-starter-web 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-starter-websocket 44 | 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-devtools 49 | runtime 50 | true 51 | 52 | 53 | org.projectlombok 54 | lombok 55 | true 56 | 57 | 58 | org.springframework.boot 59 | spring-boot-starter-test 60 | test 61 | 62 | 63 | 64 | 65 | 66 | 67 | org.springframework.boot 68 | spring-boot-maven-plugin 69 | 70 | 71 | 72 | org.projectlombok 73 | lombok 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /chat-app-backend/src/main/java/com/substring/chat/controllers/RoomController.java: -------------------------------------------------------------------------------- 1 | package com.substring.chat.controllers; 2 | 3 | import com.substring.chat.entities.Message; 4 | import com.substring.chat.entities.Room; 5 | import com.substring.chat.repositories.RoomRepository; 6 | import lombok.Getter; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.http.ResponseEntity; 11 | import org.springframework.stereotype.Repository; 12 | import org.springframework.web.bind.annotation.*; 13 | 14 | import java.util.List; 15 | import java.util.Map; 16 | 17 | @RestController 18 | @RequestMapping("/api/v1/rooms") 19 | @CrossOrigin("http://localhost:5173") 20 | public class RoomController { 21 | 22 | private RoomRepository roomRepository; 23 | 24 | 25 | public RoomController(RoomRepository roomRepository) { 26 | this.roomRepository = roomRepository; 27 | } 28 | 29 | //create room 30 | @PostMapping 31 | public ResponseEntity createRoom(@RequestBody String roomId) { 32 | 33 | if (roomRepository.findByRoomId(roomId) != null) { 34 | //room is already there 35 | return ResponseEntity.badRequest().body("Room already exists!"); 36 | 37 | } 38 | 39 | 40 | //create new room 41 | Room room = new Room(); 42 | room.setRoomId(roomId); 43 | Room savedRoom = roomRepository.save(room); 44 | return ResponseEntity.status(HttpStatus.CREATED).body(room); 45 | 46 | 47 | } 48 | 49 | 50 | //get room: join 51 | @GetMapping("/{roomId}") 52 | public ResponseEntity joinRoom( 53 | @PathVariable String roomId 54 | ) { 55 | 56 | Room room = roomRepository.findByRoomId(roomId); 57 | if (room == null) { 58 | return ResponseEntity.badRequest() 59 | .body("Room not found!!"); 60 | } 61 | return ResponseEntity.ok(room); 62 | } 63 | 64 | 65 | //get messages of room 66 | 67 | @GetMapping("/{roomId}/messages") 68 | public ResponseEntity> getMessages( 69 | @PathVariable String roomId, 70 | @RequestParam(value = "page", defaultValue = "0", required = false) int page, 71 | @RequestParam(value = "size", defaultValue = "20", required = false) int size 72 | ) { 73 | Room room = roomRepository.findByRoomId(roomId); 74 | if (room == null) { 75 | return ResponseEntity.badRequest().build() 76 | ; 77 | } 78 | //get messages : 79 | //pagination 80 | List messages = room.getMessages(); 81 | int start = Math.max(0, messages.size() - (page + 1) * size); 82 | int end = Math.min(messages.size(), start + size); 83 | List paginatedMessages = messages.subList(start, end); 84 | return ResponseEntity.ok(paginatedMessages); 85 | 86 | } 87 | 88 | 89 | } 90 | -------------------------------------------------------------------------------- /front-chat/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /front-chat/src/components/JoinCreateChat.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import chatIcon from "../assets/chat.png"; 3 | import toast from "react-hot-toast"; 4 | import { createRoomApi, joinChatApi } from "../services/RoomService"; 5 | import useChatContext from "../context/ChatContext"; 6 | import { useNavigate } from "react-router"; 7 | const JoinCreateChat = () => { 8 | const [detail, setDetail] = useState({ 9 | roomId: "", 10 | userName: "", 11 | }); 12 | 13 | const { roomId, userName, setRoomId, setCurrentUser, setConnected } = 14 | useChatContext(); 15 | const navigate = useNavigate(); 16 | 17 | function handleFormInputChange(event) { 18 | setDetail({ 19 | ...detail, 20 | [event.target.name]: event.target.value, 21 | }); 22 | } 23 | 24 | function validateForm() { 25 | if (detail.roomId === "" || detail.userName === "") { 26 | toast.error("Invalid Input !!"); 27 | return false; 28 | } 29 | return true; 30 | } 31 | 32 | async function joinChat() { 33 | if (validateForm()) { 34 | //join chat 35 | 36 | try { 37 | const room = await joinChatApi(detail.roomId); 38 | toast.success("joined.."); 39 | setCurrentUser(detail.userName); 40 | setRoomId(room.roomId); 41 | setConnected(true); 42 | navigate("/chat"); 43 | } catch (error) { 44 | if (error.status == 400) { 45 | toast.error(error.response.data); 46 | } else { 47 | toast.error("Error in joining room"); 48 | } 49 | console.log(error); 50 | } 51 | } 52 | } 53 | 54 | async function createRoom() { 55 | if (validateForm()) { 56 | //create room 57 | console.log(detail); 58 | // call api to create room on backend 59 | try { 60 | const response = await createRoomApi(detail.roomId); 61 | console.log(response); 62 | toast.success("Room Created Successfully !!"); 63 | //join the room 64 | setCurrentUser(detail.userName); 65 | setRoomId(response.roomId); 66 | setConnected(true); 67 | 68 | navigate("/chat"); 69 | 70 | //forward to chat page... 71 | } catch (error) { 72 | console.log(error); 73 | if (error.status == 400) { 74 | toast.error("Room already exists !!"); 75 | } else { 76 | toast("Error in creating room"); 77 | } 78 | } 79 | } 80 | } 81 | 82 | return ( 83 |
84 |
85 |
86 | 87 |
88 | 89 |

90 | Join Room / Create Room .. 91 |

92 | {/* name div */} 93 |
94 | 97 | 106 |
107 | 108 | {/* room id div */} 109 |
110 | 113 | 122 |
123 | 124 | {/* button */} 125 |
126 | 132 | 138 |
139 |
140 |
141 | ); 142 | }; 143 | 144 | export default JoinCreateChat; 145 | -------------------------------------------------------------------------------- /front-chat/src/components/ChatPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | import { MdAttachFile, MdSend } from "react-icons/md"; 3 | import useChatContext from "../context/ChatContext"; 4 | import { useNavigate } from "react-router"; 5 | import SockJS from "sockjs-client"; 6 | import { Stomp } from "@stomp/stompjs"; 7 | import toast from "react-hot-toast"; 8 | import { baseURL } from "../config/AxiosHelper"; 9 | import { getMessagess } from "../services/RoomService"; 10 | import { timeAgo } from "../config/helper"; 11 | const ChatPage = () => { 12 | const { 13 | roomId, 14 | currentUser, 15 | connected, 16 | setConnected, 17 | setRoomId, 18 | setCurrentUser, 19 | } = useChatContext(); 20 | // console.log(roomId); 21 | // console.log(currentUser); 22 | // console.log(connected); 23 | 24 | const navigate = useNavigate(); 25 | useEffect(() => { 26 | if (!connected) { 27 | navigate("/"); 28 | } 29 | }, [connected, roomId, currentUser]); 30 | 31 | const [messages, setMessages] = useState([]); 32 | const [input, setInput] = useState(""); 33 | const inputRef = useRef(null); 34 | const chatBoxRef = useRef(null); 35 | const [stompClient, setStompClient] = useState(null); 36 | 37 | //page init: 38 | //messages ko load karne honge 39 | 40 | useEffect(() => { 41 | async function loadMessages() { 42 | try { 43 | const messages = await getMessagess(roomId); 44 | // console.log(messages); 45 | setMessages(messages); 46 | } catch (error) {} 47 | } 48 | if (connected) { 49 | loadMessages(); 50 | } 51 | }, []); 52 | 53 | //scroll down 54 | 55 | useEffect(() => { 56 | if (chatBoxRef.current) { 57 | chatBoxRef.current.scroll({ 58 | top: chatBoxRef.current.scrollHeight, 59 | behavior: "smooth", 60 | }); 61 | } 62 | }, [messages]); 63 | 64 | //stompClient ko init karne honge 65 | //subscribe 66 | 67 | useEffect(() => { 68 | const connectWebSocket = () => { 69 | ///SockJS 70 | const sock = new SockJS(`${baseURL}/chat`); 71 | const client = Stomp.over(sock); 72 | 73 | client.connect({}, () => { 74 | setStompClient(client); 75 | 76 | toast.success("connected"); 77 | 78 | client.subscribe(`/topic/room/${roomId}`, (message) => { 79 | console.log(message); 80 | 81 | const newMessage = JSON.parse(message.body); 82 | 83 | setMessages((prev) => [...prev, newMessage]); 84 | 85 | //rest of the work after success receiving the message 86 | }); 87 | }); 88 | }; 89 | 90 | if (connected) { 91 | connectWebSocket(); 92 | } 93 | 94 | //stomp client 95 | }, [roomId]); 96 | 97 | //send message handle 98 | 99 | const sendMessage = async () => { 100 | if (stompClient && connected && input.trim()) { 101 | console.log(input); 102 | 103 | const message = { 104 | sender: currentUser, 105 | content: input, 106 | roomId: roomId, 107 | }; 108 | 109 | stompClient.send( 110 | `/app/sendMessage/${roomId}`, 111 | {}, 112 | JSON.stringify(message) 113 | ); 114 | setInput(""); 115 | } 116 | 117 | // 118 | }; 119 | 120 | function handleLogout() { 121 | stompClient.disconnect(); 122 | setConnected(false); 123 | setRoomId(""); 124 | setCurrentUser(""); 125 | navigate("/"); 126 | } 127 | 128 | return ( 129 |
130 | {/* this is a header */} 131 |
132 | {/* room name container */} 133 |
134 |

135 | Room : {roomId} 136 |

137 |
138 | {/* username container */} 139 | 140 |
141 |

142 | User : {currentUser} 143 |

144 |
145 | {/* button: leave room */} 146 |
147 | 153 |
154 |
155 | 156 |
160 | {messages.map((message, index) => ( 161 |
167 |
172 |
173 | 178 |
179 |

{message.sender}

180 |

{message.content}

181 |

182 | {timeAgo(message.timeStamp)} 183 |

184 |
185 |
186 |
187 |
188 | ))} 189 |
190 | {/* input message container */} 191 |
192 |
193 | { 196 | setInput(e.target.value); 197 | }} 198 | onKeyDown={(e) => { 199 | if (e.key === "Enter") { 200 | sendMessage(); 201 | } 202 | }} 203 | type="text" 204 | placeholder="Type your message here..." 205 | className=" w-full dark:border-gray-600 b dark:bg-gray-800 px-5 py-2 rounded-full h-full focus:outline-none " 206 | /> 207 | 208 |
209 | 212 | 218 |
219 |
220 |
221 |
222 | ); 223 | }; 224 | 225 | export default ChatPage; 226 | -------------------------------------------------------------------------------- /chat-app-backend/mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM http://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /chat-app-backend/mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.3.2 23 | # 24 | # Optional ENV vars 25 | # ----------------- 26 | # JAVA_HOME - location of a JDK home dir, required when download maven via java source 27 | # MVNW_REPOURL - repo url base for downloading maven distribution 28 | # MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 29 | # MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output 30 | # ---------------------------------------------------------------------------- 31 | 32 | set -euf 33 | [ "${MVNW_VERBOSE-}" != debug ] || set -x 34 | 35 | # OS specific support. 36 | native_path() { printf %s\\n "$1"; } 37 | case "$(uname)" in 38 | CYGWIN* | MINGW*) 39 | [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" 40 | native_path() { cygpath --path --windows "$1"; } 41 | ;; 42 | esac 43 | 44 | # set JAVACMD and JAVACCMD 45 | set_java_home() { 46 | # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched 47 | if [ -n "${JAVA_HOME-}" ]; then 48 | if [ -x "$JAVA_HOME/jre/sh/java" ]; then 49 | # IBM's JDK on AIX uses strange locations for the executables 50 | JAVACMD="$JAVA_HOME/jre/sh/java" 51 | JAVACCMD="$JAVA_HOME/jre/sh/javac" 52 | else 53 | JAVACMD="$JAVA_HOME/bin/java" 54 | JAVACCMD="$JAVA_HOME/bin/javac" 55 | 56 | if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then 57 | echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 58 | echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 59 | return 1 60 | fi 61 | fi 62 | else 63 | JAVACMD="$( 64 | 'set' +e 65 | 'unset' -f command 2>/dev/null 66 | 'command' -v java 67 | )" || : 68 | JAVACCMD="$( 69 | 'set' +e 70 | 'unset' -f command 2>/dev/null 71 | 'command' -v javac 72 | )" || : 73 | 74 | if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then 75 | echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 76 | return 1 77 | fi 78 | fi 79 | } 80 | 81 | # hash string like Java String::hashCode 82 | hash_string() { 83 | str="${1:-}" h=0 84 | while [ -n "$str" ]; do 85 | char="${str%"${str#?}"}" 86 | h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) 87 | str="${str#?}" 88 | done 89 | printf %x\\n $h 90 | } 91 | 92 | verbose() { :; } 93 | [ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } 94 | 95 | die() { 96 | printf %s\\n "$1" >&2 97 | exit 1 98 | } 99 | 100 | trim() { 101 | # MWRAPPER-139: 102 | # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. 103 | # Needed for removing poorly interpreted newline sequences when running in more 104 | # exotic environments such as mingw bash on Windows. 105 | printf "%s" "${1}" | tr -d '[:space:]' 106 | } 107 | 108 | # parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties 109 | while IFS="=" read -r key value; do 110 | case "${key-}" in 111 | distributionUrl) distributionUrl=$(trim "${value-}") ;; 112 | distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; 113 | esac 114 | done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" 115 | [ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" 116 | 117 | case "${distributionUrl##*/}" in 118 | maven-mvnd-*bin.*) 119 | MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ 120 | case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in 121 | *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; 122 | :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; 123 | :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; 124 | :Linux*x86_64*) distributionPlatform=linux-amd64 ;; 125 | *) 126 | echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 127 | distributionPlatform=linux-amd64 128 | ;; 129 | esac 130 | distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" 131 | ;; 132 | maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; 133 | *) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; 134 | esac 135 | 136 | # apply MVNW_REPOURL and calculate MAVEN_HOME 137 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 138 | [ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" 139 | distributionUrlName="${distributionUrl##*/}" 140 | distributionUrlNameMain="${distributionUrlName%.*}" 141 | distributionUrlNameMain="${distributionUrlNameMain%-bin}" 142 | MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" 143 | MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" 144 | 145 | exec_maven() { 146 | unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : 147 | exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" 148 | } 149 | 150 | if [ -d "$MAVEN_HOME" ]; then 151 | verbose "found existing MAVEN_HOME at $MAVEN_HOME" 152 | exec_maven "$@" 153 | fi 154 | 155 | case "${distributionUrl-}" in 156 | *?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; 157 | *) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; 158 | esac 159 | 160 | # prepare tmp dir 161 | if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then 162 | clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } 163 | trap clean HUP INT TERM EXIT 164 | else 165 | die "cannot create temp dir" 166 | fi 167 | 168 | mkdir -p -- "${MAVEN_HOME%/*}" 169 | 170 | # Download and Install Apache Maven 171 | verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 172 | verbose "Downloading from: $distributionUrl" 173 | verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 174 | 175 | # select .zip or .tar.gz 176 | if ! command -v unzip >/dev/null; then 177 | distributionUrl="${distributionUrl%.zip}.tar.gz" 178 | distributionUrlName="${distributionUrl##*/}" 179 | fi 180 | 181 | # verbose opt 182 | __MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' 183 | [ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v 184 | 185 | # normalize http auth 186 | case "${MVNW_PASSWORD:+has-password}" in 187 | '') MVNW_USERNAME='' MVNW_PASSWORD='' ;; 188 | has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; 189 | esac 190 | 191 | if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then 192 | verbose "Found wget ... using wget" 193 | wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" 194 | elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then 195 | verbose "Found curl ... using curl" 196 | curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" 197 | elif set_java_home; then 198 | verbose "Falling back to use Java to download" 199 | javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" 200 | targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" 201 | cat >"$javaSource" <<-END 202 | public class Downloader extends java.net.Authenticator 203 | { 204 | protected java.net.PasswordAuthentication getPasswordAuthentication() 205 | { 206 | return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); 207 | } 208 | public static void main( String[] args ) throws Exception 209 | { 210 | setDefault( new Downloader() ); 211 | java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); 212 | } 213 | } 214 | END 215 | # For Cygwin/MinGW, switch paths to Windows format before running javac and java 216 | verbose " - Compiling Downloader.java ..." 217 | "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" 218 | verbose " - Running Downloader.java ..." 219 | "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" 220 | fi 221 | 222 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 223 | if [ -n "${distributionSha256Sum-}" ]; then 224 | distributionSha256Result=false 225 | if [ "$MVN_CMD" = mvnd.sh ]; then 226 | echo "Checksum validation is not supported for maven-mvnd." >&2 227 | echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 228 | exit 1 229 | elif command -v sha256sum >/dev/null; then 230 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then 231 | distributionSha256Result=true 232 | fi 233 | elif command -v shasum >/dev/null; then 234 | if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then 235 | distributionSha256Result=true 236 | fi 237 | else 238 | echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 239 | echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 240 | exit 1 241 | fi 242 | if [ $distributionSha256Result = false ]; then 243 | echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 244 | echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 245 | exit 1 246 | fi 247 | fi 248 | 249 | # unzip and move 250 | if command -v unzip >/dev/null; then 251 | unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" 252 | else 253 | tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" 254 | fi 255 | printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" 256 | mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" 257 | 258 | clean || : 259 | exec_maven "$@" 260 | --------------------------------------------------------------------------------