├── twitter-emulation-spring-boot-angular-web ├── src │ ├── assets │ │ └── .gitkeep │ ├── app │ │ ├── app.component.css │ │ ├── app.component.html │ │ ├── shared │ │ │ ├── models │ │ │ │ ├── user.model.ts │ │ │ │ ├── account.model.ts │ │ │ │ ├── tweet.model.ts │ │ │ │ ├── account-statistics.model.ts │ │ │ │ └── user-state.model.ts │ │ │ ├── guards │ │ │ │ └── authentication.guard.ts │ │ │ └── services │ │ │ │ ├── tweet.service.ts │ │ │ │ ├── validation.service.ts │ │ │ │ └── follower.service.ts │ │ ├── modules │ │ │ ├── message │ │ │ │ ├── non-validation-message.component.html │ │ │ │ ├── message.model.ts │ │ │ │ ├── message.component.html │ │ │ │ ├── validation-messages.component.html │ │ │ │ ├── non-validation-message.component.ts │ │ │ │ ├── validation-messages.component.ts │ │ │ │ ├── message.module.ts │ │ │ │ ├── message.service.ts │ │ │ │ └── message.component.ts │ │ │ ├── unknown │ │ │ │ ├── not-found.component.html │ │ │ │ ├── not-found.component.ts │ │ │ │ └── unknown.module.ts │ │ │ ├── general │ │ │ │ ├── autofocus.directive.ts │ │ │ │ ├── general.module.ts │ │ │ │ └── equal-validator.directive.ts │ │ │ ├── home │ │ │ │ ├── home-account.component.html │ │ │ │ ├── status-info.component.html │ │ │ │ ├── status-info.component.ts │ │ │ │ ├── home.resolver.ts │ │ │ │ ├── home-tweets.component.html │ │ │ │ ├── home.component.html │ │ │ │ ├── home.component.ts │ │ │ │ ├── home-tweets.component.ts │ │ │ │ ├── home-account.component.ts │ │ │ │ ├── top-bar.component.ts │ │ │ │ ├── account.component.ts │ │ │ │ ├── tweets.component.ts │ │ │ │ ├── followers.component.ts │ │ │ │ ├── following.component.ts │ │ │ │ ├── top-bar.component.html │ │ │ │ ├── search.component.ts │ │ │ │ ├── home.module.ts │ │ │ │ └── home-child.ts │ │ │ ├── profile │ │ │ │ ├── delete-account.component.html │ │ │ │ ├── profile.module.ts │ │ │ │ ├── delete-account.component.ts │ │ │ │ └── profile.component.ts │ │ │ ├── login │ │ │ │ ├── login.module.ts │ │ │ │ ├── login.component.ts │ │ │ │ └── login.component.html │ │ │ ├── tweet │ │ │ │ ├── tweet.module.ts │ │ │ │ ├── new-tweet.component.ts │ │ │ │ └── new-tweet.component.html │ │ │ └── registration │ │ │ │ ├── registration.module.ts │ │ │ │ └── registration.component.ts │ │ └── app.component.ts │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ ├── index.html │ ├── main.ts │ ├── test.ts │ ├── karma.conf.js │ └── styles.scss ├── proxy.conf.js ├── e2e │ ├── tsconfig.e2e.json │ ├── src │ │ └── app.po.ts │ └── protractor.conf.js ├── .editorconfig ├── tsconfig.json ├── .gitignore ├── README.md ├── eslint.config.js ├── pom.xml └── package.json ├── twitter-emulation-spring-boot-react-web ├── src │ ├── assets │ │ └── .gitkeep │ ├── vite-env.d.ts │ ├── common │ │ ├── models │ │ │ ├── User.ts │ │ │ ├── Message.ts │ │ │ ├── Account.ts │ │ │ ├── Tweet.ts │ │ │ ├── AccountStatistics.ts │ │ │ └── UserState.ts │ │ ├── ReactUtils.tsx │ │ ├── authentication │ │ │ ├── AuthContextType.ts │ │ │ ├── RequireAuth.tsx │ │ │ └── AuthProvider.tsx │ │ ├── validation │ │ │ ├── ValidationContext.ts │ │ │ ├── ValidationMessage.tsx │ │ │ ├── ValidationMessages.tsx │ │ │ ├── ValidatableTextarea.tsx │ │ │ └── ValidatableInput.tsx │ │ ├── ReactRouterAdapter.ts │ │ ├── AxiosDateReviver.ts │ │ └── services │ │ │ ├── TweetService.ts │ │ │ └── FollowerService.ts │ ├── setupTests.ts │ ├── home │ │ ├── Loading.tsx │ │ ├── AnonymousMain.tsx │ │ ├── AccountList.tsx │ │ ├── TweetList.tsx │ │ ├── Search.tsx │ │ ├── StatusInfo.tsx │ │ ├── Tweets.tsx │ │ ├── Followers.tsx │ │ ├── Following.tsx │ │ ├── Main.tsx │ │ ├── Home.tsx │ │ └── StatusInfo.test.tsx │ ├── index.css │ ├── unknown │ │ └── NotFoundComponent.tsx │ ├── message │ │ ├── NonValidationMessageText.tsx │ │ └── MessageService.ts │ ├── App.scss │ └── main.tsx ├── public │ └── favicon.ico ├── e2e │ ├── jest-puppeteer.config.ts │ ├── jest.config.ts │ └── tests │ │ ├── app.po.ts │ │ └── App.test.ts ├── tsconfig.json ├── .gitignore ├── index.html ├── tsconfig.node.json ├── tsconfig.app.json ├── vite.config.ts ├── eslint.config.js ├── pom.xml └── README.md ├── .gitignore ├── images └── screenshot.png ├── documents ├── twitter-emulation.tdl └── twitter-emulation.epgz ├── twitter-emulation-common └── src │ ├── test │ ├── resources │ │ ├── clean-h2.sql │ │ ├── clean-oracledb.sql │ │ └── logback-test.xml │ └── java │ │ └── acme │ │ └── twitter │ │ └── dao │ │ ├── utils │ │ ├── TestSupport.java │ │ ├── H2TestSupport.java │ │ └── TestUtils.java │ │ ├── H2TweetDaoTest.java │ │ ├── H2AccountDaoTest.java │ │ ├── H2FollowerDaoTest.java │ │ ├── OracleDatabaseTweetDaoTest.java │ │ ├── OracleDatabaseAccountDaoTest.java │ │ └── OracleDatabaseFollowerDaoTest.java │ └── main │ ├── java │ └── acme │ │ └── twitter │ │ ├── dao │ │ ├── exception │ │ │ ├── AccountException.java │ │ │ ├── AccountExistsException.java │ │ │ ├── AccountNotExistsException.java │ │ │ └── AccountNotAllowedException.java │ │ ├── TweetDao.java │ │ ├── AccountRowMapper.java │ │ ├── AccountDao.java │ │ ├── FollowerDao.java │ │ └── TweetRowMapper.java │ │ ├── web │ │ ├── SearchForm.java │ │ ├── TweetForm.java │ │ ├── validation │ │ │ ├── PasswordsMatchValidator.java │ │ │ └── PasswordsMatch.java │ │ └── AccountForm.java │ │ ├── service │ │ ├── TweetService.java │ │ ├── FollowerService.java │ │ ├── AccountService.java │ │ ├── TweetServiceImpl.java │ │ └── FollowerServiceImpl.java │ │ ├── domain │ │ ├── Account.java │ │ ├── AccountStatistics.java │ │ ├── Tweet.java │ │ └── AbstractAccount.java │ │ └── config │ │ └── DevelopmentDataConfig.java │ └── resources │ ├── ValidationMessages.properties │ ├── schema-h2.sql │ ├── schema-oracledb.sql │ └── logback.xml ├── twitter-emulation-spring-boot-common-server └── src │ ├── test │ ├── resources │ │ └── clean-h2.sql │ └── java │ │ └── acme │ │ └── twitter │ │ └── util │ │ ├── JsonUtil.java │ │ └── TestUtils.java │ └── main │ ├── java │ └── acme │ │ └── twitter │ │ ├── dto │ │ ├── AccountStatisticsDto.java │ │ ├── TweetDto.java │ │ └── AccountDto.java │ │ ├── config │ │ ├── SwaggerConfig.java │ │ ├── CookieCsrfFilter.java │ │ ├── WebConfig.java │ │ └── ProductionDataConfig.java │ │ ├── App.java │ │ └── controller │ │ ├── AuthenticationController.java │ │ ├── RestErrorAttributes.java │ │ └── RestResponseEntityExceptionHandler.java │ └── resources │ └── application.yml ├── twitter-emulation-spring-boot-admin └── src │ └── main │ ├── resources │ └── application.yml │ └── java │ └── acme │ └── twitter │ └── admin │ ├── App.java │ └── ServletInitializer.java ├── twitter-emulation-spring-boot-jsp └── src │ └── main │ ├── resources │ ├── application.yml │ └── static │ │ └── css │ │ └── style.css │ ├── java │ └── acme │ │ └── twitter │ │ ├── App.java │ │ └── config │ │ └── ProductionDataConfig.java │ └── webapp │ └── WEB-INF │ └── views │ ├── deleteAccountForm.jsp │ ├── newTweetForm.jsp │ ├── accountInfo.jsp │ ├── searchForm.jsp │ ├── follow.jsp │ └── registrationForm.jsp ├── .github ├── mergify.yml ├── dependabot.yml └── workflows │ ├── deploy.yml │ ├── sonar.yml │ └── build.yml ├── twitter-emulation-distrib ├── runme.sh ├── whatsnew.txt ├── runme.bat ├── readme.txt ├── assembly-react.xml └── assembly-angular.xml ├── twitter-emulation-spring-mvc-jsp ├── src │ └── main │ │ ├── java │ │ └── acme │ │ │ └── twitter │ │ │ └── config │ │ │ ├── SecurityWebInitializer.java │ │ │ ├── WebInitializer.java │ │ │ ├── ProductionDataConfig.java │ │ │ └── RootConfig.java │ │ └── webapp │ │ ├── WEB-INF │ │ └── views │ │ │ ├── deleteAccountForm.jsp │ │ │ ├── newTweetForm.jsp │ │ │ ├── accountInfo.jsp │ │ │ ├── searchForm.jsp │ │ │ ├── follow.jsp │ │ │ └── registrationForm.jsp │ │ └── css │ │ └── style.css └── etc │ └── jetty.xml ├── twitter-emulation-spring-boot-react-server └── src │ └── main │ └── resources │ └── banner.txt ├── twitter-emulation-spring-boot-angular-server └── src │ └── main │ └── resources │ └── banner.txt └── .gitattributes /twitter-emulation-spring-boot-angular-web/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/app.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.ipr 3 | *.iws 4 | target/ 5 | .idea/ 6 | logs/ 7 | -------------------------------------------------------------------------------- /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbelob/twitter-emulation/HEAD/images/screenshot.png -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /documents/twitter-emulation.tdl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbelob/twitter-emulation/HEAD/documents/twitter-emulation.tdl -------------------------------------------------------------------------------- /documents/twitter-emulation.epgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbelob/twitter-emulation/HEAD/documents/twitter-emulation.epgz -------------------------------------------------------------------------------- /twitter-emulation-common/src/test/resources/clean-h2.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM follower; 2 | DELETE FROM tweet; 3 | DELETE FROM account; 4 | -------------------------------------------------------------------------------- /twitter-emulation-common/src/test/resources/clean-oracledb.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM follower; 2 | DELETE FROM tweet; 3 | DELETE FROM account; 4 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-common-server/src/test/resources/clean-h2.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM follower; 2 | DELETE FROM tweet; 3 | DELETE FROM account; 4 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/shared/models/user.model.ts: -------------------------------------------------------------------------------- 1 | export class User { 2 | constructor( 3 | public name?: string 4 | ) { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/src/common/models/User.ts: -------------------------------------------------------------------------------- 1 | export class User { 2 | constructor( 3 | public name?: string 4 | ) { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbelob/twitter-emulation/HEAD/twitter-emulation-spring-boot-angular-web/src/favicon.ico -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbelob/twitter-emulation/HEAD/twitter-emulation-spring-boot-react-web/public/favicon.ico -------------------------------------------------------------------------------- /twitter-emulation-common/src/main/java/acme/twitter/dao/exception/AccountException.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.dao.exception; 2 | 3 | public abstract class AccountException extends Exception { 4 | } 5 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-admin/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 9000 3 | 4 | spring: 5 | security: 6 | user: 7 | name: "admin" 8 | password: "password" 9 | -------------------------------------------------------------------------------- /twitter-emulation-common/src/main/java/acme/twitter/dao/exception/AccountExistsException.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.dao.exception; 2 | 3 | public class AccountExistsException extends AccountException { 4 | } 5 | -------------------------------------------------------------------------------- /twitter-emulation-common/src/main/java/acme/twitter/dao/exception/AccountNotExistsException.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.dao.exception; 2 | 3 | public class AccountNotExistsException extends AccountException { 4 | } 5 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/message/non-validation-message.component.html: -------------------------------------------------------------------------------- 1 | @if (!(isFormSubmitted && isFormValid)) { 2 |
3 | 4 |
5 | } 6 | -------------------------------------------------------------------------------- /twitter-emulation-common/src/main/java/acme/twitter/dao/exception/AccountNotAllowedException.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.dao.exception; 2 | 3 | public class AccountNotAllowedException extends AccountException { 4 | } 5 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/message/message.model.ts: -------------------------------------------------------------------------------- 1 | export class Message { 2 | constructor(public text: string, 3 | public date: Date, 4 | public error = false) { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-jsp/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | servlet: 3 | register-default-servlet: true 4 | 5 | oracle: 6 | url: "jdbc:oracle:thin:@acme.com:1521:sid" 7 | username: "te" 8 | password: "password" 9 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/src/common/models/Message.ts: -------------------------------------------------------------------------------- 1 | export class Message { 2 | constructor(public text: string, 3 | public date: Date, 4 | public error: boolean = false) { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.github/mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: Automatic merge for Dependabot pull requests 3 | conditions: 4 | - author=dependabot[bot] 5 | - check-success=build 6 | actions: 7 | merge: 8 | method: merge 9 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/message/message.component.html: -------------------------------------------------------------------------------- 1 | @if (isVisible()) { 2 |
3 | {{lastMessage.text}} 4 |
5 | } 6 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/proxy.conf.js: -------------------------------------------------------------------------------- 1 | const PROXY_CONFIG = [ 2 | { 3 | context: [ 4 | "/api" 5 | ], 6 | target: "http://localhost:8080", 7 | secure: false 8 | } 9 | ]; 10 | 11 | module.exports = PROXY_CONFIG; 12 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/shared/models/account.model.ts: -------------------------------------------------------------------------------- 1 | export class Account { 2 | constructor( 3 | public id?: number, 4 | public username?: string, 5 | public password?: string, 6 | public description?: string 7 | ) { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/e2e/jest-puppeteer.config.ts: -------------------------------------------------------------------------------- 1 | import { type JestPuppeteerConfig } from 'jest-environment-puppeteer'; 2 | 3 | const config: JestPuppeteerConfig = { 4 | launch: { 5 | headless: true 6 | } 7 | }; 8 | 9 | export default config; 10 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/src/common/ReactUtils.tsx: -------------------------------------------------------------------------------- 1 | import { useParams } from 'react-router'; 2 | 3 | export default class ReactUtils { 4 | static withParams(Component: any) { 5 | return (props: any) => ; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/src/common/models/Account.ts: -------------------------------------------------------------------------------- 1 | export class Account { 2 | constructor( 3 | public id?: number, 4 | public username?: string, 5 | public password?: string, 6 | public description?: string 7 | ) { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/shared/models/tweet.model.ts: -------------------------------------------------------------------------------- 1 | export class Tweet { 2 | constructor( 3 | public id?: number, 4 | public username?: string, 5 | public description?: string, 6 | public text?: string, 7 | public date?: Date 8 | ) { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /twitter-emulation-distrib/runme.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -n "$JAVA_HOME" -a -x "$JAVA_HOME/bin/java" ]; then 4 | JRE="$JAVA_HOME" 5 | echo "Application JRE: $JRE" 6 | eval "$JRE/bin/java" -jar ${app.finalName}.war 7 | else 8 | echo "No JRE found. Please validate JAVA_HOME environment variable." 9 | fi 10 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/src/common/models/Tweet.ts: -------------------------------------------------------------------------------- 1 | export class Tweet { 2 | constructor( 3 | public id?: number, 4 | public username?: string, 5 | public description?: string, 6 | public text?: string, 7 | public date?: Date 8 | ) { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ], 7 | "compilerOptions": { 8 | "experimentalDecorators": true, 9 | "types": ["vitest/globals"] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/unknown/not-found.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Page not found

3 | 4 |
5 | Go to Main page 6 |
7 |
8 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/unknown/not-found.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-not-found', 5 | templateUrl: './not-found.component.html', 6 | standalone: false 7 | }) 8 | export class NotFoundComponent { 9 | } 10 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.css'], 7 | standalone: false 8 | }) 9 | export class AppComponent { 10 | } 11 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "main.ts", 9 | "polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /twitter-emulation-distrib/whatsnew.txt: -------------------------------------------------------------------------------- 1 | Twitter Emulation 2 | 3 | Version 2.1.0 - September 1, 2024 4 | * Migration from Create React App (CRA) to Vite. 5 | * Reducing the build size. 6 | 7 | Version 2.0.0 - January 8, 2023 8 | * Implementation in Spring Boot and React. 9 | 10 | Version 1.0.0 - July 9, 2020 11 | * First release. 12 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/src/common/authentication/AuthContextType.ts: -------------------------------------------------------------------------------- 1 | export interface AuthContextType { 2 | username: any; 3 | loading: boolean; 4 | login: (username: string, password: string, successCallback: VoidFunction, errorCallback: VoidFunction) => void; 5 | logout: (callback: VoidFunction) => void; 6 | } 7 | -------------------------------------------------------------------------------- /twitter-emulation-distrib/runme.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | if "%JAVA_HOME%" == "" goto jremanual 4 | set JRE=%JAVA_HOME% 5 | goto start 6 | 7 | :jremanual 8 | set /p JRE=Java folder can't be found. Please, enter your JRE folder: 9 | 10 | :start 11 | echo Application JRE: %JRE% 12 | 13 | "%JRE%\bin\java" -jar ${app.finalName}.war 14 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/src/home/Loading.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | 3 | class Loading extends Component { 4 | render() { 5 | return ( 6 |
7 | Loading... 8 |
9 | ); 10 | } 11 | } 12 | 13 | export default Loading; 14 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-common-server/src/main/java/acme/twitter/dto/AccountStatisticsDto.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.dto; 2 | 3 | /** 4 | * Account statistics DTO. 5 | */ 6 | public record AccountStatisticsDto(String username, String description, int tweetsCount, 7 | int followingCount, int followersCount, boolean follow) { 8 | } 9 | -------------------------------------------------------------------------------- /twitter-emulation-spring-mvc-jsp/src/main/java/acme/twitter/config/SecurityWebInitializer.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.config; 2 | 3 | import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer; 4 | 5 | /** 6 | * Spring Security web initializer. 7 | */ 8 | public class SecurityWebInitializer extends AbstractSecurityWebApplicationInitializer { 9 | } -------------------------------------------------------------------------------- /twitter-emulation-common/src/test/java/acme/twitter/dao/utils/TestSupport.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.dao.utils; 2 | 3 | import javax.sql.DataSource; 4 | import java.sql.SQLException; 5 | 6 | public interface TestSupport { 7 | DataSource getDataSource(); 8 | 9 | void setUp() throws SQLException; 10 | 11 | void tearDown() throws SQLException; 12 | 13 | void stop(); 14 | } -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/shared/models/account-statistics.model.ts: -------------------------------------------------------------------------------- 1 | export class AccountStatistics { 2 | constructor( 3 | public username?: string, 4 | public description?: string, 5 | public tweetsCount?: number, 6 | public followingCount?: number, 7 | public followersCount?: number, 8 | public follow?: boolean 9 | ) { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/src/common/validation/ValidationContext.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const ValidationContext = React.createContext({ 4 | getMessagesForField: (_field: string) => [], 5 | getMessagesForFields: () => [], 6 | getFieldClasses: (_field: string) => '', 7 | isFormSubmitted: () => false, 8 | isFormValid: () => false 9 | }); 10 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/src/common/models/AccountStatistics.ts: -------------------------------------------------------------------------------- 1 | export class AccountStatistics { 2 | constructor( 3 | public username?: string, 4 | public description?: string, 5 | public tweetsCount?: number, 6 | public followingCount?: number, 7 | public followersCount?: number, 8 | public follow?: boolean 9 | ) { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /twitter-emulation-distrib/readme.txt: -------------------------------------------------------------------------------- 1 | Twitter Emulation 2 | 3 | The application emulates the main features of Twitter. 4 | 5 | 1. Run in console 6 | runme.bat (Windows) 7 | runme.sh (macOS or Linux) 8 | 9 | 2. Open browser with URL 10 | http://localhost:8080 11 | 12 | 3. Use application in browser 13 | 14 | 4. Ctrl-C in console to cancel 15 | 16 | WWW: https://github.com/dbelob/twitter-emulation 17 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/message/validation-messages.component.html: -------------------------------------------------------------------------------- 1 | @if (isFormSubmitted && form.invalid) { 2 |
3 | There are problems with the form 4 |
    5 | @for (error of getFormValidationMessages(form); track error) { 6 |
  • 7 | {{error}} 8 |
  • 9 | } 10 |
11 |
12 | } 13 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Twitter (Angular) 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/.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 | node 12 | dist 13 | dist-ssr 14 | *.local 15 | 16 | # Editor directories and files 17 | .vscode/* 18 | !.vscode/extensions.json 19 | .idea 20 | .DS_Store 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | -------------------------------------------------------------------------------- /twitter-emulation-common/src/test/java/acme/twitter/dao/H2TweetDaoTest.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.dao; 2 | 3 | import acme.twitter.dao.utils.H2TestSupport; 4 | import org.junit.jupiter.api.BeforeAll; 5 | 6 | /** 7 | * Tweet DAO test for H2. 8 | */ 9 | public class H2TweetDaoTest extends TweetDaoTest { 10 | @BeforeAll 11 | static void start() { 12 | TweetDaoTest.start(new H2TestSupport()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /twitter-emulation-common/src/test/java/acme/twitter/dao/H2AccountDaoTest.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.dao; 2 | 3 | import acme.twitter.dao.utils.H2TestSupport; 4 | import org.junit.jupiter.api.BeforeAll; 5 | 6 | /** 7 | * Account DAO test for H2. 8 | */ 9 | public class H2AccountDaoTest extends AccountDaoTest { 10 | @BeforeAll 11 | static void start() { 12 | AccountDaoTest.start(new H2TestSupport()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /twitter-emulation-common/src/test/java/acme/twitter/dao/H2FollowerDaoTest.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.dao; 2 | 3 | import acme.twitter.dao.utils.H2TestSupport; 4 | import org.junit.jupiter.api.BeforeAll; 5 | 6 | /** 7 | * Follower DAO test for H2. 8 | */ 9 | public class H2FollowerDaoTest extends FollowerDaoTest { 10 | @BeforeAll 11 | static void start() { 12 | FollowerDaoTest.start(new H2TestSupport()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/message/non-validation-message.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-non-validation-message', 5 | templateUrl: './non-validation-message.component.html', 6 | standalone: false 7 | }) 8 | export class NonValidationMessageComponent { 9 | @Input() public isFormSubmitted: boolean; 10 | @Input() public isFormValid: boolean; 11 | } 12 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Twitter (React) 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/general/autofocus.directive.ts: -------------------------------------------------------------------------------- 1 | import { AfterViewInit, Directive, ElementRef } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: '[appAutofocus]', 5 | standalone: false 6 | }) 7 | export class AutofocusDirective implements AfterViewInit { 8 | constructor(private elementRef: ElementRef) { 9 | } 10 | 11 | ngAfterViewInit(): void { 12 | this.elementRef.nativeElement.focus(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/unknown/unknown.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { RouterModule } from '@angular/router'; 4 | import { NotFoundComponent } from './not-found.component'; 5 | 6 | @NgModule({ 7 | declarations: [ 8 | NotFoundComponent 9 | ], 10 | imports: [ 11 | CommonModule, 12 | RouterModule 13 | ] 14 | }) 15 | export class UnknownModule { 16 | } 17 | -------------------------------------------------------------------------------- /twitter-emulation-common/src/main/java/acme/twitter/web/SearchForm.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.web; 2 | 3 | import jakarta.validation.constraints.NotNull; 4 | 5 | /** 6 | * Search form. 7 | */ 8 | public class SearchForm { 9 | @NotNull 10 | private String usernamePart; 11 | 12 | public String getUsernamePart() { 13 | return usernamePart; 14 | } 15 | 16 | public void setUsernamePart(String username) { 17 | this.usernamePart = username; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-admin/src/main/java/acme/twitter/admin/App.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.admin; 2 | 3 | import de.codecentric.boot.admin.server.config.EnableAdminServer; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | 7 | @SpringBootApplication 8 | @EnableAdminServer 9 | public class App { 10 | public static void main(String[] args) { 11 | SpringApplication.run(App.class, args); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/home/home-account.component.html: -------------------------------------------------------------------------------- 1 | 2 |
{{title}}
3 | 4 | @for (account of accounts; track account) { 5 | 6 | 7 | 10 | 11 | 12 | } 13 |
8 | {{account.description}} @{{account.username}} 9 |
14 |
15 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/home/status-info.component.html: -------------------------------------------------------------------------------- 1 |
2 | @if (isStateVisible()) { 3 |
4 |
5 | Logged in as {{userState.authenticatedUserName}} 6 |
7 |
8 | } 9 |
10 |
11 | © Acme, {{copyrightDate | date:"yyyy"}} 12 |
13 |
14 |
15 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/src/common/models/UserState.ts: -------------------------------------------------------------------------------- 1 | export class UserState { 2 | constructor( 3 | public authenticatedUserName?: string, 4 | public selectedUserName?: string 5 | ) { 6 | } 7 | 8 | getDataUserName(): string | undefined { 9 | return (this.selectedUserName) ? this.selectedUserName : this.authenticatedUserName; 10 | } 11 | 12 | isAuthenticated(): boolean { 13 | return (this.authenticatedUserName !== undefined); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /twitter-emulation-common/src/main/java/acme/twitter/service/TweetService.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.service; 2 | 3 | import acme.twitter.domain.Account; 4 | import acme.twitter.domain.Tweet; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * Tweet service. 10 | */ 11 | public interface TweetService { 12 | List findByAccount(Account account); 13 | 14 | List findTimelineByAccount(Account account); 15 | 16 | void add(String username, String text); 17 | 18 | int countByUsername(String username); 19 | } 20 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode, provideZoneChangeDetection } from '@angular/core'; 2 | import { platformBrowser } from '@angular/platform-browser'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowser().bootstrapModule(AppModule, { applicationProviders: [provideZoneChangeDetection()], }) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-admin/src/main/java/acme/twitter/admin/ServletInitializer.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.admin; 2 | 3 | import org.springframework.boot.builder.SpringApplicationBuilder; 4 | import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; 5 | 6 | public class ServletInitializer extends SpringBootServletInitializer { 7 | @Override 8 | protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { 9 | return application.sources(App.class); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /twitter-emulation-common/src/test/java/acme/twitter/dao/OracleDatabaseTweetDaoTest.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.dao; 2 | 3 | import acme.twitter.dao.utils.OracleDatabaseTestSupport; 4 | import org.junit.jupiter.api.BeforeAll; 5 | 6 | import java.sql.SQLException; 7 | 8 | /** 9 | * Tweet DAO test for Oracle Database. 10 | */ 11 | public class OracleDatabaseTweetDaoTest extends TweetDaoTest { 12 | @BeforeAll 13 | static void start() throws SQLException { 14 | TweetDaoTest.start(new OracleDatabaseTestSupport()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /twitter-emulation-common/src/main/java/acme/twitter/web/TweetForm.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.web; 2 | 3 | import jakarta.validation.constraints.NotNull; 4 | import jakarta.validation.constraints.Size; 5 | 6 | /** 7 | * Tweet form. 8 | */ 9 | public class TweetForm { 10 | @NotNull 11 | @Size(min = 1, max = 140, message = "{tweet.size}") 12 | private String text; 13 | 14 | public String getText() { 15 | return text; 16 | } 17 | 18 | public void setText(String text) { 19 | this.text = text; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /twitter-emulation-common/src/main/java/acme/twitter/dao/TweetDao.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.dao; 2 | 3 | import acme.twitter.domain.Account; 4 | import acme.twitter.domain.Tweet; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * Tweet DAO. 10 | */ 11 | public interface TweetDao { 12 | List findByAccount(Account account); 13 | 14 | List findTimelineByAccount(Account account); 15 | 16 | void add(String username, String text); 17 | 18 | void deleteAll(String username); 19 | 20 | int countByUsername(String username); 21 | } -------------------------------------------------------------------------------- /twitter-emulation-common/src/test/java/acme/twitter/dao/OracleDatabaseAccountDaoTest.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.dao; 2 | 3 | import acme.twitter.dao.utils.OracleDatabaseTestSupport; 4 | import org.junit.jupiter.api.BeforeAll; 5 | 6 | import java.sql.SQLException; 7 | 8 | /** 9 | * Account DAO test for Oracle Database. 10 | */ 11 | public class OracleDatabaseAccountDaoTest extends AccountDaoTest { 12 | @BeforeAll 13 | static void start() throws SQLException { 14 | AccountDaoTest.start(new OracleDatabaseTestSupport()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "esModuleInterop": true, 7 | "sourceMap": true, 8 | "declaration": false, 9 | "module": "ES2022", 10 | "moduleResolution": "bundler", 11 | "experimentalDecorators": true, 12 | "importHelpers": true, 13 | "target": "ES2022", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "useDefineForClassFields": false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/src/home/AnonymousMain.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate } from 'react-router'; 2 | import { useAuth } from '../common/authentication/AuthProvider'; 3 | import Loading from './Loading'; 4 | 5 | export default function AnonymousMain() { 6 | const auth = useAuth(); 7 | 8 | return ( 9 | <> 10 | { 11 | auth.loading ? 12 | : 13 | 14 | } 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /twitter-emulation-common/src/test/java/acme/twitter/dao/OracleDatabaseFollowerDaoTest.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.dao; 2 | 3 | import acme.twitter.dao.utils.OracleDatabaseTestSupport; 4 | import org.junit.jupiter.api.BeforeAll; 5 | 6 | import java.sql.SQLException; 7 | 8 | /** 9 | * Follower DAO test for Oracle Database. 10 | */ 11 | public class OracleDatabaseFollowerDaoTest extends FollowerDaoTest { 12 | @BeforeAll 13 | static void start() throws SQLException { 14 | FollowerDaoTest.start(new OracleDatabaseTestSupport()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /twitter-emulation-common/src/main/java/acme/twitter/domain/Account.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.domain; 2 | 3 | /** 4 | * Account. 5 | */ 6 | public class Account extends AbstractAccount { 7 | public Account(long id, String username, String password, String description) { 8 | super(id, username, password, description); 9 | } 10 | 11 | @Override 12 | public boolean equals(Object o) { 13 | return super.equals(o); 14 | } 15 | 16 | @Override 17 | public int hashCode() { 18 | return super.hashCode(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-common-server/src/test/java/acme/twitter/util/JsonUtil.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.util; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | 6 | import java.io.IOException; 7 | 8 | public class JsonUtil { 9 | public static byte[] toJson(Object object) throws IOException { 10 | ObjectMapper mapper = new ObjectMapper(); 11 | mapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL); 12 | return mapper.writeValueAsBytes(object); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/e2e/jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'jest'; 2 | 3 | const config: Config = { 4 | // roots: ["/src"], 5 | transform: { 6 | "^.+\\.tsx?$": "ts-jest", 7 | "^.+\\.js$": "babel-jest" 8 | }, 9 | testRegex: "(/e2e/.*\\.(test|spec))\\.tsx?$", 10 | moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], 11 | preset: "jest-puppeteer", 12 | transformIgnorePatterns: [ 13 | "node_modules/(?!(@bundled-es-modules|msw)/)" 14 | ] 15 | } 16 | 17 | export default config; 18 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/shared/guards/authentication.guard.ts: -------------------------------------------------------------------------------- 1 | import { inject } from '@angular/core'; 2 | import { CanActivateFn, Router } from '@angular/router'; 3 | import { AuthenticationService } from '../services/authentication.service'; 4 | 5 | export const authenticationCanActivate: CanActivateFn = () => { 6 | const authenticationService = inject(AuthenticationService); 7 | const router = inject(Router); 8 | 9 | return authenticationService.authenticate(undefined, undefined, () => { 10 | router.navigateByUrl('/login'); 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /twitter-emulation-common/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/home/status-info.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { UserState } from '../../shared/models/user-state.model'; 3 | 4 | @Component({ 5 | selector: 'app-status-info', 6 | templateUrl: './status-info.component.html', 7 | standalone: false 8 | }) 9 | export class StatusInfoComponent { 10 | @Input() 11 | userState: UserState; 12 | 13 | public copyrightDate = new Date(); 14 | 15 | isStateVisible(): boolean { 16 | return this.userState.isAuthenticated(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/profile/delete-account.component.html: -------------------------------------------------------------------------------- 1 |
2 |

Delete account

3 | 4 | 5 | 6 |
7 | Are you sure to delete account '{{user.name}}'? 8 |
9 |
10 | 11 | 12 |
13 |
14 | -------------------------------------------------------------------------------- /twitter-emulation-common/src/main/resources/ValidationMessages.properties: -------------------------------------------------------------------------------- 1 | username.size=Username must be between {min} and {max} characters long. 2 | password.size=Password must be between {min} and {max} characters long. 3 | passwordConfirmation.size=Confirmation must be between {min} and {max} characters long. 4 | description.size=Description must be between {min} and {max} characters long. 5 | password.difference=Password and confirmation must be the same. 6 | account.exists=Account with the same name already exists. 7 | tweet.size=Tweet text size must be between {min} and {max} characters long. 8 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/general/general.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { EqualValidatorDirective } from './equal-validator.directive'; 4 | import { AutofocusDirective } from './autofocus.directive'; 5 | 6 | @NgModule({ 7 | declarations: [ 8 | EqualValidatorDirective, 9 | AutofocusDirective 10 | ], 11 | imports: [ 12 | CommonModule 13 | ], 14 | exports: [ 15 | EqualValidatorDirective, 16 | AutofocusDirective] 17 | }) 18 | export class GeneralModule { 19 | } 20 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/shared/models/user-state.model.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | 3 | export class UserState { 4 | constructor( 5 | public authenticatedUserName?: string, 6 | public selectedUserName?: string 7 | ) { 8 | } 9 | 10 | getDataUserName(): string { 11 | return (this.selectedUserName) ? this.selectedUserName : this.authenticatedUserName; 12 | } 13 | 14 | isAuthenticated(): boolean { 15 | return (this.authenticatedUserName !== undefined); 16 | } 17 | } 18 | 19 | export const USER_STATE = new InjectionToken("user_state"); 20 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/src/unknown/NotFoundComponent.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | export default class NotFoundComponent extends Component { 5 | render() { 6 | return ( 7 |
8 |

Page not found

9 | 10 |
11 | Go to Main page 12 |
13 |
14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["ES2023"], 5 | "module": "ESNext", 6 | "skipLibCheck": true, 7 | 8 | /* Bundler mode */ 9 | "moduleResolution": "bundler", 10 | "allowImportingTsExtensions": true, 11 | "isolatedModules": true, 12 | "moduleDetection": "force", 13 | "noEmit": true, 14 | 15 | /* Linting */ 16 | "strict": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noFallthroughCasesInSwitch": true 20 | }, 21 | "include": ["vite.config.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/login/login.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { RouterModule } from '@angular/router'; 5 | import { LoginComponent } from './login.component'; 6 | import { GeneralModule } from '../general/general.module'; 7 | 8 | @NgModule({ 9 | declarations: [ 10 | LoginComponent 11 | ], 12 | imports: [ 13 | CommonModule, 14 | FormsModule, 15 | RouterModule, 16 | GeneralModule 17 | ] 18 | }) 19 | export class LoginModule { 20 | } 21 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/home/home.resolver.ts: -------------------------------------------------------------------------------- 1 | import { inject } from '@angular/core'; 2 | import { ActivatedRouteSnapshot, ResolveFn } from '@angular/router'; 3 | import { AccountStatistics } from '../../shared/models/account-statistics.model'; 4 | import { AccountService } from '../../shared/services/account.service'; 5 | 6 | export const homeResolve: ResolveFn = (route: ActivatedRouteSnapshot) => { 7 | const accountService = inject(AccountService); 8 | const dataUserName = route.params.user; 9 | 10 | return (dataUserName) ? accountService.getAccountStatistics(dataUserName) : undefined; 11 | }; 12 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/home/home-tweets.component.html: -------------------------------------------------------------------------------- 1 | 2 | @if (title) { 3 |
{{title}}
4 | } 5 | @for (tweet of tweets; track tweet) { 6 |
7 |
8 | {{tweet.description}} @{{tweet.username}} 9 | 10 | {{tweet.date | date:"dd.MM.yyyy HH:mm:ss"}} 11 | 12 |
13 |
{{tweet.text}}
14 |
15 | } 16 |
17 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-common-server/src/main/java/acme/twitter/config/SwaggerConfig.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.config; 2 | 3 | import io.swagger.v3.oas.models.OpenAPI; 4 | import io.swagger.v3.oas.models.info.Info; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | @Configuration 9 | public class SwaggerConfig { 10 | @Bean 11 | public OpenAPI springOpenAPI() { 12 | return new OpenAPI() 13 | .info(new Info().title("TwitterEmulation API") 14 | .description("Twitter emulation application") 15 | .version("v1.0.0")); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /twitter-emulation-common/src/main/java/acme/twitter/dao/AccountRowMapper.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.dao; 2 | 3 | import acme.twitter.domain.Account; 4 | import org.springframework.jdbc.core.RowMapper; 5 | 6 | import java.sql.ResultSet; 7 | import java.sql.SQLException; 8 | 9 | public class AccountRowMapper implements RowMapper { 10 | @Override 11 | public Account mapRow(ResultSet resultSet, int i) throws SQLException { 12 | return new Account( 13 | resultSet.getLong("account_id"), 14 | resultSet.getString("username"), 15 | resultSet.getString("password"), 16 | resultSet.getString("description")); 17 | } 18 | } -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
7 | 8 |
9 |
10 | 11 |
12 |
13 | 14 |
15 |
16 | 17 |
18 |
19 |
20 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/src/message/NonValidationMessageText.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ContextType } from 'react'; 2 | import { ValidationContext } from '../common/validation/ValidationContext'; 3 | import MessageText from './MessageText'; 4 | 5 | export default class NonValidationMessageText extends Component { 6 | static contextType = ValidationContext; 7 | declare context: ContextType; 8 | 9 | render() { 10 | return ( 11 | <> 12 | {!(this.context.isFormSubmitted() && !this.context.isFormValid()) && 13 | 14 | } 15 | 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "maven" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | ignore: 8 | - dependency-name: "org.eclipse.jetty:jetty-maven-plugin" 9 | - package-ecosystem: "npm" 10 | directory: "/twitter-emulation-spring-boot-angular-web" 11 | schedule: 12 | interval: "daily" 13 | groups: 14 | angular: 15 | patterns: 16 | - "@angular*" 17 | - package-ecosystem: "npm" 18 | directory: "/twitter-emulation-spring-boot-react-web" 19 | schedule: 20 | interval: "daily" 21 | - package-ecosystem: "github-actions" 22 | directory: "/" 23 | schedule: 24 | interval: "daily" 25 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element, ElementFinder } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getPageTitleText() { 9 | return browser.getTitle() as Promise; 10 | } 11 | 12 | getPageHeaderText() { 13 | return element(by.css('app-root h3')).getText() as Promise; 14 | } 15 | 16 | getButtonById(id: string) { 17 | return element(by.css('button[id="' + id + '"]')) as ElementFinder; 18 | } 19 | 20 | getDivByIdText(id: string) { 21 | return element(by.css('div[id="' + id + '"]')).getText() as Promise; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/tweet/tweet.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { RouterModule } from '@angular/router'; 5 | import { NewTweetComponent } from './new-tweet.component'; 6 | import { MessageModule } from '../message/message.module'; 7 | import { GeneralModule } from '../general/general.module'; 8 | 9 | @NgModule({ 10 | declarations: [ 11 | NewTweetComponent 12 | ], 13 | imports: [ 14 | CommonModule, 15 | FormsModule, 16 | RouterModule, 17 | GeneralModule, 18 | MessageModule 19 | ] 20 | }) 21 | export class TweetModule { 22 | } 23 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/e2e/tests/app.po.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer'; 2 | 3 | const rootSelector = '#root'; 4 | const titleSelector = 'h3'; 5 | 6 | export const navigateTo = async (page: Page, baseUrl: string) => { 7 | await page.goto(baseUrl); 8 | }; 9 | 10 | export const root = async (page: Page) => await page.$(rootSelector); 11 | 12 | export const getPageTitleText = async (page: Page) => await page.title(); 13 | 14 | export const getPageHeaderText = async (page: Page) => { 15 | const app = await root(page); 16 | 17 | if (!app) { 18 | throw new Error('Can\'t root node'); 19 | } 20 | 21 | return await app.$eval(titleSelector, (el: any) => el.innerText); 22 | }; 23 | -------------------------------------------------------------------------------- /twitter-emulation-common/src/main/java/acme/twitter/web/validation/PasswordsMatchValidator.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.web.validation; 2 | 3 | import acme.twitter.web.AccountForm; 4 | import jakarta.validation.ConstraintValidator; 5 | import jakarta.validation.ConstraintValidatorContext; 6 | 7 | public class PasswordsMatchValidator implements ConstraintValidator { 8 | @Override 9 | public boolean isValid(AccountForm value, ConstraintValidatorContext context) { 10 | if (value == null) { 11 | return true; 12 | } else { 13 | return (value.getPassword() == null) || value.getPassword().equals(value.getPasswordConfirmation()); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /twitter-emulation-common/src/main/java/acme/twitter/service/FollowerService.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.service; 2 | 3 | import acme.twitter.domain.Account; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * Follower service. 9 | */ 10 | public interface FollowerService { 11 | int countFollowingByUsername(String username); 12 | 13 | int countFollowersByUsername(String username); 14 | 15 | boolean isExist(String whoUsername, String whomUsername); 16 | 17 | void add(String whoUsername, String whomUsername); 18 | 19 | void delete(String whoUsername, String whomUsername); 20 | 21 | List findFollowingByUsername(String username); 22 | 23 | List findFollowersByUsername(String username); 24 | } 25 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/src/common/ReactRouterAdapter.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useLocation, useNavigate } from 'react-router'; 3 | import { PartialLocation, QueryParamAdapter } from 'use-query-params/src/types.ts'; 4 | 5 | export default function ReactRouterAdapter({children}: { children: (adapter: QueryParamAdapter) => React.ReactElement | null }) { 6 | const navigate = useNavigate(); 7 | 8 | return children({ 9 | location: useLocation(), 10 | replace: (location: PartialLocation) => navigate({search: location.search}, {replace: true, state: location.state}), 11 | push: (location: PartialLocation) => navigate({search: location.search}, {state: location.state}) 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": false, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | "experimentalDecorators": true, 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true 23 | }, 24 | "include": ["src"] 25 | } 26 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react({ 7 | babel: { 8 | parserOpts: { 9 | plugins: ['decorators-legacy'], 10 | }, 11 | }, 12 | })], 13 | build: { 14 | outDir: 'target/classes/static' 15 | }, 16 | server: { 17 | proxy: { 18 | '/api': { 19 | target: 'http://localhost:8080', 20 | changeOrigin: true, 21 | secure: false 22 | } 23 | } 24 | }, 25 | test: { 26 | globals: true, 27 | environment: 'jsdom', 28 | setupFiles: ['src/setupTests.ts'] 29 | } 30 | }) 31 | -------------------------------------------------------------------------------- /twitter-emulation-spring-mvc-jsp/etc/jetty.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | jdbc/TeDataSource 7 | 8 | 9 | thin 10 | jdbc:oracle:thin:@acme.com:1521:sid 11 | te 12 | password 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /twitter-emulation-common/src/main/java/acme/twitter/dao/AccountDao.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.dao; 2 | 3 | import acme.twitter.dao.exception.AccountExistsException; 4 | import acme.twitter.dao.exception.AccountNotExistsException; 5 | import acme.twitter.domain.Account; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * Account DAO. 11 | */ 12 | public interface AccountDao { 13 | void add(String username, String password, String description) throws AccountExistsException; 14 | 15 | void update(String username, String password, String description); 16 | 17 | void delete(String username); 18 | 19 | Account findByUsername(String username) throws AccountNotExistsException; 20 | 21 | List findByUsernamePart(String usernamePart); 22 | } -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject, OnInit } from '@angular/core'; 2 | import { Observable } from 'rxjs'; 3 | import { USER_STATE, UserState } from '../../shared/models/user-state.model'; 4 | 5 | @Component({ 6 | selector: 'app-home', 7 | templateUrl: './home.component.html', 8 | standalone: false 9 | }) 10 | export class HomeComponent implements OnInit { 11 | public userState: UserState = new UserState(); 12 | 13 | constructor(@Inject(USER_STATE) private userStateObservable: Observable) { 14 | } 15 | 16 | ngOnInit(): void { 17 | this.userStateObservable.subscribe((userState) => { 18 | this.userState = userState; 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/registration/registration.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { RouterModule } from '@angular/router'; 5 | import { RegistrationComponent } from './registration.component'; 6 | import { MessageModule } from '../message/message.module'; 7 | import { GeneralModule } from '../general/general.module'; 8 | 9 | @NgModule({ 10 | declarations: [ 11 | RegistrationComponent 12 | ], 13 | imports: [ 14 | CommonModule, 15 | FormsModule, 16 | RouterModule, 17 | GeneralModule, 18 | MessageModule 19 | ] 20 | }) 21 | export class RegistrationModule { 22 | } 23 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-jsp/src/main/java/acme/twitter/App.java: -------------------------------------------------------------------------------- 1 | package acme.twitter; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.builder.SpringApplicationBuilder; 6 | import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; 7 | 8 | @SpringBootApplication 9 | public class App extends SpringBootServletInitializer { 10 | @Override 11 | protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { 12 | return application.sources(App.class); 13 | } 14 | 15 | public static void main(String[] args) { 16 | SpringApplication.run(App.class, args); 17 | } 18 | } -------------------------------------------------------------------------------- /twitter-emulation-common/src/main/java/acme/twitter/dao/FollowerDao.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.dao; 2 | 3 | import acme.twitter.domain.Account; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * Follower DAO. 9 | */ 10 | public interface FollowerDao { 11 | int countFollowingByUsername(String username); 12 | 13 | int countFollowersByUsername(String username); 14 | 15 | boolean isExist(String whoUsername, String whomUsername); 16 | 17 | void add(String whoUsername, String whomUsername); 18 | 19 | void delete(String whoUsername, String whomUsername); 20 | 21 | void deleteAll(String username); 22 | 23 | List findFollowingByUsername(String username); 24 | 25 | List findFollowersByUsername(String username); 26 | } 27 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-common-server/src/main/java/acme/twitter/App.java: -------------------------------------------------------------------------------- 1 | package acme.twitter; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.builder.SpringApplicationBuilder; 6 | import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; 7 | 8 | @SpringBootApplication 9 | public class App extends SpringBootServletInitializer { 10 | @Override 11 | protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { 12 | return application.sources(App.class); 13 | } 14 | 15 | public static void main(String[] args) { 16 | SpringApplication.run(App.class, args); 17 | } 18 | } -------------------------------------------------------------------------------- /twitter-emulation-common/src/main/java/acme/twitter/web/validation/PasswordsMatch.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.web.validation; 2 | 3 | import jakarta.validation.Constraint; 4 | import jakarta.validation.Payload; 5 | 6 | import java.lang.annotation.ElementType; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | import java.lang.annotation.Target; 10 | 11 | @Target({ElementType.TYPE}) 12 | @Retention(RetentionPolicy.RUNTIME) 13 | @Constraint(validatedBy = PasswordsMatchValidator.class) 14 | public @interface PasswordsMatch { 15 | String message() default "{jakarta.validation.constraints.PasswordsMatch.message}"; 16 | 17 | Class[] groups() default {}; 18 | 19 | Class[] payload() default {}; 20 | } 21 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-common-server/src/main/java/acme/twitter/controller/AuthenticationController.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.controller; 2 | 3 | import org.springframework.web.bind.annotation.GetMapping; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | import org.springframework.web.bind.annotation.RestController; 6 | 7 | import java.security.Principal; 8 | 9 | @RestController 10 | @RequestMapping("/api/authentication") 11 | public class AuthenticationController { 12 | /** 13 | * Returns user information. 14 | * 15 | * @param principal principal 16 | * @return user information 17 | */ 18 | @GetMapping("/user") 19 | public Principal getUser(Principal principal) { 20 | return principal; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /twitter-emulation-spring-mvc-jsp/src/main/java/acme/twitter/config/WebInitializer.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.config; 2 | 3 | import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; 4 | 5 | /** 6 | * Spring MVC web initializer. 7 | */ 8 | public class WebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { 9 | @Override 10 | protected Class[] getRootConfigClasses() { 11 | return new Class[]{RootConfig.class}; 12 | } 13 | 14 | @Override 15 | protected Class[] getServletConfigClasses() { 16 | return new Class[]{WebConfig.class}; 17 | } 18 | 19 | @Override 20 | protected String[] getServletMappings() { 21 | return new String[]{"/"}; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /twitter-emulation-common/src/main/java/acme/twitter/service/AccountService.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.service; 2 | 3 | import acme.twitter.dao.exception.AccountExistsException; 4 | import acme.twitter.dao.exception.AccountNotExistsException; 5 | import acme.twitter.domain.Account; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * Account service. 11 | */ 12 | public interface AccountService { 13 | void add(String username, String password, String description) throws AccountExistsException; 14 | 15 | void update(String username, String password, String description); 16 | 17 | void delete(String username); 18 | 19 | Account findByUsername(String username) throws AccountNotExistsException; 20 | 21 | List findByUsernamePart(String usernamePart); 22 | } 23 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/e2e/tests/App.test.ts: -------------------------------------------------------------------------------- 1 | import puppeteer, { Browser, Page } from 'puppeteer'; 2 | import { getPageTitleText, navigateTo } from './app.po'; 3 | 4 | describe('App', () => { 5 | const baseUrl = 'http://localhost:5173'; 6 | let browser: Browser; 7 | let page: Page; 8 | 9 | beforeAll(async () => { 10 | browser = await puppeteer.launch({ 11 | headless: true 12 | }); 13 | page = await browser.newPage(); 14 | }, 30_000); 15 | 16 | afterAll(async () => { 17 | await browser.close(); 18 | }); 19 | 20 | test('should open application', async () => { 21 | await navigateTo(page, baseUrl); 22 | expect(await getPageTitleText(page)).toBe('Twitter (React)'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/message/validation-messages.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { NgForm } from '@angular/forms'; 3 | import { ValidationService } from '../../shared/services/validation.service'; 4 | 5 | @Component({ 6 | selector: 'app-validation-messages', 7 | templateUrl: './validation-messages.component.html', 8 | standalone: false 9 | }) 10 | export class ValidationMessagesComponent { 11 | @Input() public isFormSubmitted: boolean; 12 | @Input() public form: NgForm; 13 | 14 | constructor(private validationService: ValidationService) { 15 | } 16 | 17 | getFormValidationMessages(form: NgForm): string[] { 18 | return this.validationService.getFormValidationMessages(form); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-common-server/src/main/java/acme/twitter/dto/TweetDto.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.dto; 2 | 3 | import acme.twitter.domain.Tweet; 4 | 5 | import java.util.Date; 6 | import java.util.List; 7 | 8 | /** 9 | * Tweet DTO. 10 | */ 11 | public record TweetDto(long id, String username, String description, String text, Date date) { 12 | public static List convertToDto(List tweets) { 13 | return tweets.stream() 14 | .map(t -> new TweetDto( 15 | t.getId(), 16 | t.getAccount().getUsername(), 17 | t.getAccount().getDescription(), 18 | t.getText(), 19 | t.getDate())) 20 | .toList(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/src/common/validation/ValidationMessage.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ContextType } from 'react'; 2 | import { ValidationContext } from './ValidationContext'; 3 | 4 | type ValidationMessageProps = { 5 | field: string; 6 | } 7 | 8 | type ValidationMessageState = {} 9 | 10 | export class ValidationMessage extends Component { 11 | static contextType = ValidationContext; 12 | declare context: ContextType; 13 | 14 | render() { 15 | return this.context.getMessagesForField(this.props.field).map(error => 16 |
17 | {error} 18 |
19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/general/equal-validator.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive, Input } from '@angular/core'; 2 | import { AbstractControl, NG_VALIDATORS, ValidationErrors, Validator } from '@angular/forms'; 3 | 4 | @Directive({ 5 | selector: '[appValidateEqual]', 6 | providers: [{ provide: NG_VALIDATORS, useExisting: EqualValidatorDirective, multi: true }], 7 | standalone: false 8 | }) 9 | export class EqualValidatorDirective implements Validator { 10 | @Input('appValidateEqual') validated: string; 11 | 12 | validate(control: AbstractControl): ValidationErrors | null { 13 | const v = control.value; 14 | const e = control.root.get(this.validated); 15 | 16 | return (e && v !== e.value) ? {appValidateEqual: {validated: this.validated}} : null; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/profile/profile.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | import { RouterModule } from '@angular/router'; 5 | import { ProfileComponent } from './profile.component'; 6 | import { DeleteAccountComponent } from './delete-account.component'; 7 | import { MessageModule } from '../message/message.module'; 8 | import { GeneralModule } from '../general/general.module'; 9 | 10 | @NgModule({ 11 | declarations: [ 12 | DeleteAccountComponent, 13 | ProfileComponent 14 | ], 15 | imports: [ 16 | CommonModule, 17 | FormsModule, 18 | RouterModule, 19 | GeneralModule, 20 | MessageModule 21 | ] 22 | }) 23 | export class ProfileModule { 24 | } 25 | -------------------------------------------------------------------------------- /twitter-emulation-common/src/main/resources/schema-h2.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE account ( 2 | account_id IDENTITY PRIMARY KEY NOT NULL, 3 | username VARCHAR(16) UNIQUE NOT NULL, 4 | password VARCHAR(25) NOT NULL, 5 | description VARCHAR(30) NOT NULL 6 | ); 7 | 8 | CREATE TABLE tweet ( 9 | tweet_id IDENTITY PRIMARY KEY NOT NULL, 10 | account_id BIGINT NOT NULL, 11 | text VARCHAR(140) NOT NULL, 12 | time TIMESTAMP NOT NULL, 13 | FOREIGN KEY (account_id) REFERENCES account(account_id) 14 | ); 15 | 16 | CREATE TABLE follower ( 17 | who_account_id BIGINT NOT NULL, 18 | whom_account_id BIGINT NOT NULL, 19 | PRIMARY KEY (who_account_id, whom_account_id), 20 | FOREIGN KEY (who_account_id) REFERENCES account(account_id), 21 | FOREIGN KEY (whom_account_id) REFERENCES account(account_id), 22 | CHECK (who_account_id <> whom_account_id) 23 | ); 24 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/message/message.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { MessageComponent } from './message.component'; 4 | import { MessageService } from './message.service'; 5 | import { NonValidationMessageComponent } from './non-validation-message.component'; 6 | import { ValidationMessagesComponent } from './validation-messages.component'; 7 | 8 | @NgModule({ 9 | declarations: [ 10 | MessageComponent, 11 | NonValidationMessageComponent, 12 | ValidationMessagesComponent 13 | ], 14 | imports: [CommonModule], 15 | providers: [MessageService], 16 | exports: [ 17 | MessageComponent, 18 | NonValidationMessageComponent, 19 | ValidationMessagesComponent 20 | ] 21 | }) 22 | export class MessageModule { 23 | } 24 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | declare const require: any; 12 | 13 | // First, initialize the Angular testing environment. 14 | getTestBed().initTestEnvironment( 15 | BrowserDynamicTestingModule, 16 | platformBrowserDynamicTesting(), { 17 | teardown: { destroyAfterEach: false } 18 | } 19 | ); 20 | // Then we find all the tests. 21 | const context = require.context('./', true, /\.spec\.ts$/); 22 | // And load the modules. 23 | context.keys().map(context); 24 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-server/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ______ _ _____ _ _ _ _____ _ _ _ 2 | | ___ \ | | |_ _| (_)| | | | | ___| | | | | (_) 3 | | |_/ / ___ __ _ ___ | |_ | | __ __ _ | |_ | |_ ___ _ __ | |__ _ __ ___ _ _ | | __ _ | |_ _ ___ _ __ 4 | | / / _ \ / _` | / __|| __| | | \ \ /\ / /| || __|| __| / _ \| '__|| __| | '_ ` _ \ | | | || | / _` || __|| | / _ \ | '_ \ 5 | | |\ \ | __/| (_| || (__ | |_ | | \ V V / | || |_ | |_ | __/| | | |___ | | | | | || |_| || || (_| || |_ | || (_) || | | | 6 | \_| \_| \___| \__,_| \___| \__| \_/ \_/\_/ |_| \__| \__| \___||_| \____/ |_| |_| |_| \__,_||_| \__,_| \__||_| \___/ |_| |_| 7 | 8 | :: React Twitter Emulation ::${application.formatted-version} 9 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import reactHooks from 'eslint-plugin-react-hooks' 4 | import reactRefresh from 'eslint-plugin-react-refresh' 5 | import tseslint from 'typescript-eslint' 6 | 7 | export default tseslint.config( 8 | { ignores: ['dist'] }, 9 | { 10 | extends: [js.configs.recommended, ...tseslint.configs.recommended], 11 | files: ['**/*.{ts,tsx}'], 12 | languageOptions: { 13 | ecmaVersion: 2020, 14 | globals: globals.browser, 15 | }, 16 | plugins: { 17 | 'react-hooks': reactHooks, 18 | 'react-refresh': reactRefresh, 19 | }, 20 | rules: { 21 | ...reactHooks.configs.recommended.rules, 22 | 'react-refresh/only-export-components': [ 23 | 'warn', 24 | { allowConstantExport: true }, 25 | ], 26 | }, 27 | }, 28 | ) 29 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/tweet/new-tweet.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { NgForm } from '@angular/forms'; 3 | import { Router } from '@angular/router'; 4 | import { TweetService } from '../../shared/services/tweet.service'; 5 | 6 | @Component({ 7 | selector: 'app-new-tweet', 8 | templateUrl: './new-tweet.component.html', 9 | standalone: false 10 | }) 11 | export class NewTweetComponent { 12 | public formSubmitted = false; 13 | public text: string; 14 | 15 | constructor(private tweetService: TweetService, private router: Router) { 16 | } 17 | 18 | submitForm(form: NgForm) { 19 | this.formSubmitted = true; 20 | 21 | if (form.valid) { 22 | this.tweetService.tweet(this.text) 23 | .subscribe(() => { 24 | this.router.navigateByUrl('/account/show'); 25 | }); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | /node 13 | 14 | # profiling files 15 | chrome-profiler-events.json 16 | speed-measure-plugin.json 17 | 18 | # IDEs and editors 19 | /.idea 20 | .project 21 | .classpath 22 | .c9/ 23 | *.launch 24 | .settings/ 25 | *.sublime-workspace 26 | 27 | # IDE - VSCode 28 | .vscode/* 29 | !.vscode/settings.json 30 | !.vscode/tasks.json 31 | !.vscode/launch.json 32 | !.vscode/extensions.json 33 | .history/* 34 | 35 | # misc 36 | /.angular/cache 37 | /.sass-cache 38 | /connect.lock 39 | /coverage 40 | /libpeerconnection.log 41 | npm-debug.log 42 | yarn-error.log 43 | testem.log 44 | /typings 45 | 46 | # System Files 47 | .DS_Store 48 | Thumbs.db 49 | -------------------------------------------------------------------------------- /twitter-emulation-spring-mvc-jsp/src/main/java/acme/twitter/config/ProductionDataConfig.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.Profile; 6 | import org.springframework.jdbc.core.JdbcTemplate; 7 | import org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup; 8 | 9 | import javax.sql.DataSource; 10 | 11 | @Configuration 12 | @Profile("production") 13 | public class ProductionDataConfig { 14 | @Bean 15 | public DataSource dataSource() { 16 | JndiDataSourceLookup dataSourceLookup = new JndiDataSourceLookup(); 17 | 18 | return dataSourceLookup.getDataSource("jdbc/TeDataSource"); 19 | } 20 | 21 | @Bean 22 | public JdbcTemplate jdbcTemplate(DataSource dataSource) { 23 | return new JdbcTemplate(dataSource); 24 | } 25 | } -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-common-server/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | oracle: 2 | url: "jdbc:oracle:thin:@acme.com:1521:sid" 3 | username: "te" 4 | password: "password" 5 | 6 | spring: 7 | jackson: 8 | default-property-inclusion: non_null 9 | boot: 10 | admin: 11 | client: 12 | url: http://localhost:9000 13 | username: "admin" #These two are needed so that the client 14 | password: "password" #can register at the protected server api 15 | instance: 16 | metadata: 17 | user.name: "jsmith" #These two are needed so that the server 18 | user.password: "password" #can access the protected client endpoints 19 | 20 | management: 21 | endpoints: 22 | web: 23 | exposure: 24 | include: "*" 25 | 26 | springdoc: 27 | packages-to-scan: "acme.twitter.controller" 28 | paths-to-match: "/api/**" 29 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v6 13 | - name: Cache Maven packages 14 | uses: actions/cache@v5 15 | with: 16 | path: ~/.m2 17 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 18 | restore-keys: ${{ runner.os }}-m2 19 | - name: Release with Maven 20 | uses: qcastel/github-actions-maven-release@master 21 | with: 22 | release-branch-name: "master" 23 | maven-args: "-Dmaven.javadoc.skip=true -DskipTests -DskipITs -Ddockerfile.skip -DdockerCompose.skip" 24 | git-release-bot-name: "release-bot" 25 | git-release-bot-email: "release-bot@github.com" 26 | access-token: ${{ secrets.GITHUB_TOKEN }} 27 | skip-perform: true 28 | -------------------------------------------------------------------------------- /twitter-emulation-common/src/main/java/acme/twitter/domain/AccountStatistics.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.domain; 2 | 3 | /** 4 | * Account statistics. 5 | */ 6 | public class AccountStatistics { 7 | private int tweetsCount; 8 | private int followingCount; 9 | private int followersCount; 10 | private boolean follow; 11 | 12 | public AccountStatistics(int tweetsCount, int followingCount, int followersCount, boolean follow) { 13 | this.tweetsCount = tweetsCount; 14 | this.followingCount = followingCount; 15 | this.followersCount = followersCount; 16 | this.follow = follow; 17 | } 18 | 19 | public int getTweetsCount() { 20 | return tweetsCount; 21 | } 22 | 23 | public int getFollowingCount() { 24 | return followingCount; 25 | } 26 | 27 | public int getFollowersCount() { 28 | return followersCount; 29 | } 30 | 31 | public boolean isFollow() { 32 | return follow; 33 | } 34 | } -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-common-server/src/main/java/acme/twitter/dto/AccountDto.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.dto; 2 | 3 | import acme.twitter.domain.AbstractAccount; 4 | import acme.twitter.domain.Account; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * Account DTO. 10 | */ 11 | public class AccountDto extends AbstractAccount { 12 | public AccountDto() { 13 | super(0, null, null, null); 14 | } 15 | 16 | public AccountDto(long id, String username, String password, String description) { 17 | super(id, username, password, description); 18 | } 19 | 20 | public AccountDto(long id, String username, String description) { 21 | super(id, username, null, description); 22 | } 23 | 24 | public static List convertToDto(List accounts) { 25 | return accounts.stream() 26 | .map(a -> new AccountDto(a.getId(), a.getUsername(), a.getDescription())) 27 | .toList(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-jsp/src/main/webapp/WEB-INF/views/deleteAccountForm.jsp: -------------------------------------------------------------------------------- 1 | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> 2 | <%@ taglib uri="http://www.springframework.org/tags/form" prefix="sf" %> 3 | <%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %> 4 | <%@ page session="false" contentType="text/html; charset=UTF-8" %> 5 | 6 | 7 | 8 | Twitter 9 | "> 10 | 11 | 12 |
13 |

Delete account

14 | 15 | 16 |

Are you sure to delete account ''?

17 |

18 | 19 | 20 |

21 |
22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /twitter-emulation-spring-mvc-jsp/src/main/webapp/WEB-INF/views/deleteAccountForm.jsp: -------------------------------------------------------------------------------- 1 | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> 2 | <%@ taglib uri="http://www.springframework.org/tags/form" prefix="sf" %> 3 | <%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %> 4 | <%@ page session="false" contentType="text/html; charset=UTF-8" %> 5 | 6 | 7 | 8 | Twitter 9 | "> 10 | 11 | 12 |
13 |

Delete account

14 | 15 | 16 |

Are you sure to delete account ''?

17 |

18 | 19 | 20 |

21 |
22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome', 13 | 'chromeOptions': { 14 | args: ['--headless'] 15 | } 16 | }, 17 | directConnect: true, 18 | baseUrl: 'http://localhost:4200/', 19 | framework: 'jasmine', 20 | jasmineNodeOpts: { 21 | showColors: true, 22 | defaultTimeoutInterval: 30000, 23 | print: function() {} 24 | }, 25 | onPrepare() { 26 | require('ts-node').register({ 27 | project: require('path').join(__dirname, './tsconfig.e2e.json') 28 | }); 29 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-jsp/src/main/webapp/WEB-INF/views/newTweetForm.jsp: -------------------------------------------------------------------------------- 1 | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> 2 | <%@ taglib uri="http://www.springframework.org/tags/form" prefix="sf" %> 3 | <%@ page session="false" contentType="text/html; charset=UTF-8" %> 4 | 5 | 6 | 7 | Twitter 8 | "> 9 | 10 | 11 |
12 |

New tweet

13 | 14 | 15 | 16 | 17 |

18 | 19 | 20 |

21 |
22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /twitter-emulation-spring-mvc-jsp/src/main/webapp/WEB-INF/views/newTweetForm.jsp: -------------------------------------------------------------------------------- 1 | <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> 2 | <%@ taglib uri="http://www.springframework.org/tags/form" prefix="sf" %> 3 | <%@ page session="false" contentType="text/html; charset=UTF-8" %> 4 | 5 | 6 | 7 | Twitter 8 | "> 9 | 10 | 11 |
12 |

New tweet

13 | 14 | 15 | 16 | 17 |

18 | 19 | 20 |

21 |
22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/home/home-tweets.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject } from '@angular/core'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | import { Observer } from 'rxjs'; 4 | import { Tweet } from '../../shared/models/tweet.model'; 5 | import { AuthenticationService } from '../../shared/services/authentication.service'; 6 | import { HomeChild } from './home-child'; 7 | import { USER_STATE, UserState } from '../../shared/models/user-state.model'; 8 | 9 | @Component({ 10 | selector: 'app-home-tweets', 11 | templateUrl: './home-tweets.component.html', 12 | standalone: false 13 | }) 14 | export class HomeTweetsComponent extends HomeChild { 15 | public tweets: Tweet[] = []; 16 | 17 | constructor(authenticationService: AuthenticationService, activatedRoute: ActivatedRoute, router: Router, 18 | @Inject(USER_STATE) observer: Observer) { 19 | super(authenticationService, activatedRoute, router, observer); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-server/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ___ _ _____ _ _ _ _____ _ _ _ 2 | / _ \ | | |_ _| (_)| | | | | ___| | | | | (_) 3 | / /_\ \ _ __ __ _ _ _ | | __ _ _ __ | | __ __ _ | |_ | |_ ___ _ __ | |__ _ __ ___ _ _ | | __ _ | |_ _ ___ _ __ 4 | | _ || '_ \ / _` || | | || | / _` || '__| | | \ \ /\ / /| || __|| __| / _ \| '__|| __| | '_ ` _ \ | | | || | / _` || __|| | / _ \ | '_ \ 5 | | | | || | | || (_| || |_| || || (_| || | | | \ V V / | || |_ | |_ | __/| | | |___ | | | | | || |_| || || (_| || |_ | || (_) || | | | 6 | \_| |_/|_| |_| \__, | \__,_||_| \__,_||_| \_/ \_/\_/ |_| \__| \__| \___||_| \____/ |_| |_| |_| \__,_||_| \__,_| \__||_| \___/ |_| |_| 7 | __/ | 8 | |___/ 9 | :: Angular Twitter Emulation ::${application.formatted-version} 10 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/home/home-account.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject } from '@angular/core'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | import { Observer } from 'rxjs'; 4 | import { Account } from '../../shared/models/account.model'; 5 | import { AuthenticationService } from '../../shared/services/authentication.service'; 6 | import { HomeChild } from './home-child'; 7 | import { USER_STATE, UserState } from '../../shared/models/user-state.model'; 8 | 9 | @Component({ 10 | selector: 'app-home-account', 11 | templateUrl: './home-account.component.html', 12 | standalone: false 13 | }) 14 | export class HomeAccountComponent extends HomeChild { 15 | public accounts: Account[] = []; 16 | 17 | constructor(authenticationService: AuthenticationService, activatedRoute: ActivatedRoute, router: Router, 18 | @Inject(USER_STATE) observer: Observer) { 19 | super(authenticationService, activatedRoute, router, observer); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /twitter-emulation-spring-mvc-jsp/src/main/java/acme/twitter/config/RootConfig.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.config; 2 | 3 | import acme.twitter.config.RootConfig.WebPackage; 4 | import org.springframework.context.annotation.ComponentScan; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.context.annotation.FilterType; 7 | import org.springframework.context.annotation.Import; 8 | import org.springframework.core.type.filter.RegexPatternTypeFilter; 9 | 10 | import java.util.regex.Pattern; 11 | 12 | @Configuration 13 | @Import({DevelopmentDataConfig.class, ProductionDataConfig.class}) 14 | @ComponentScan(basePackages = {"acme.twitter"}, 15 | excludeFilters = { 16 | @ComponentScan.Filter(type = FilterType.CUSTOM, value = WebPackage.class) 17 | }) 18 | public class RootConfig { 19 | public static class WebPackage extends RegexPatternTypeFilter { 20 | public WebPackage() { 21 | super(Pattern.compile("acme\\.twitter\\.controller")); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/message/message.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Observable, Subject } from 'rxjs'; 3 | import { Message } from './message.model'; 4 | 5 | @Injectable() 6 | export class MessageService { 7 | private subject = new Subject(); 8 | 9 | reportMessage(parameter: Message | Response) { 10 | const msg = (parameter instanceof Message) ? 11 | parameter : 12 | new Message(MessageService.getMessageText(parameter), new Date(), true); 13 | 14 | this.subject.next(msg); 15 | } 16 | 17 | get messages(): Observable { 18 | return this.subject; 19 | } 20 | 21 | private static getMessageText(response: Response): string { 22 | const error = response['error']; 23 | 24 | if (error) { 25 | const customMessage = error['customMessage']; 26 | 27 | if (customMessage) { 28 | return customMessage; 29 | } 30 | } 31 | 32 | return `Network Error: ${response.statusText} (${response.status})`; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/sonar.yml: -------------------------------------------------------------------------------- 1 | name: SonarCloud 2 | 3 | on: 4 | push: 5 | branches: [ '*' ] 6 | 7 | jobs: 8 | sonarcloud: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Update Google Chrome 12 | run: | 13 | sudo apt-get update 14 | sudo apt-get --only-upgrade install google-chrome-stable 15 | google-chrome --version 16 | - name: Checkout code 17 | uses: actions/checkout@v6 18 | with: 19 | fetch-depth: 0 20 | - name: Set up JDK 21 | uses: actions/setup-java@v5 22 | with: 23 | distribution: 'oracle' 24 | java-version: 25 25 | - name: Analyze with SonarCloud 26 | run: mvn -B verify sonar:sonar -Dsonar.projectKey=dbelob_twitter-emulation -Dsonar.organization=dbelob -Dsonar.host.url=https://sonarcloud.io -Dsonar.token=$SONAR_TOKEN -Dsonar.coverage.jacoco.xmlReportPaths=./target/site/jacoco/jacoco.xml 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 30 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/src/common/validation/ValidationMessages.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ContextType } from 'react'; 2 | import { ValidationContext } from './ValidationContext'; 3 | 4 | export class ValidationMessages extends Component { 5 | static contextType = ValidationContext; 6 | declare context: ContextType; 7 | 8 | render() { 9 | const errors: string[] = this.context.getMessagesForFields(); 10 | 11 | return ( 12 | <> 13 | {(errors.length > 0) && 14 |
15 | There are problems with the form 16 |
    17 | {errors.map(error => 18 |
  • 19 | {error} 20 |
  • 21 | )} 22 |
23 |
24 | } 25 | 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/src/common/authentication/RequireAuth.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Navigate, useLocation } from 'react-router'; 3 | import { useAuth } from './AuthProvider'; 4 | import Loading from '../../home/Loading'; 5 | 6 | function RequireAuth({children}: { children: React.JSX.Element }) { 7 | const auth = useAuth(); 8 | const location = useLocation(); 9 | 10 | // Redirect them to the /login page, but save the current location they were 11 | // trying to go to when they were redirected. This allows us to send them 12 | // along to that page after they log in, which is a nicer user experience 13 | // than dropping them off on the home page. 14 | return ( 15 | <> 16 | { 17 | auth.loading ? 18 | : 19 | (!auth.username ? 20 | : 21 | children) 22 | } 23 | 24 | ); 25 | } 26 | 27 | export default RequireAuth; 28 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | import { AuthenticationService } from '../../shared/services/authentication.service'; 4 | 5 | @Component({ 6 | templateUrl: './login.component.html', 7 | standalone: false 8 | }) 9 | 10 | export class LoginComponent { 11 | public credentials = {username: '', password: ''}; 12 | public error = false; 13 | public logout = false; 14 | 15 | constructor(private authenticationService: AuthenticationService, private router: Router, route: ActivatedRoute) { 16 | route.queryParams.subscribe(params => { 17 | this.logout = (params.logout === '1'); 18 | }); 19 | } 20 | 21 | login() { 22 | this.logout = false; 23 | 24 | this.authenticationService.authenticate(this.credentials, () => { 25 | this.router.navigate(['/account', 'show', this.credentials.username]); 26 | }, 27 | () => { 28 | this.error = true; 29 | }).subscribe(); 30 | return false; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [ push, pull_request ] 4 | 5 | jobs: 6 | build: 7 | if: ${{ !((github.event_name == 'push') && (github.actor == 'dependabot[bot]')) }} 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Update Google Chrome 11 | run: | 12 | sudo apt-get update 13 | sudo apt-get --only-upgrade install google-chrome-stable 14 | google-chrome --version 15 | - name: Checkout code 16 | uses: actions/checkout@v6 17 | - name: Set up JDK 18 | uses: actions/setup-java@v5 19 | with: 20 | distribution: 'oracle' 21 | java-version: 25 22 | - name: Cache Maven packages 23 | uses: actions/cache@v5 24 | with: 25 | path: ~/.m2 26 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 27 | restore-keys: ${{ runner.os }}-m2 28 | - name: Build with Maven 29 | run: mvn -B clean package 30 | - name: Test coverage 31 | uses: codecov/codecov-action@v5 32 | with: 33 | token: ${{ secrets.CODECOV_TOKEN }} 34 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/message/message.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { MessageService } from './message.service'; 3 | import { Message } from './message.model'; 4 | 5 | @Component({ 6 | selector: "app-message", 7 | templateUrl: "message.component.html", 8 | standalone: false 9 | }) 10 | export class MessageComponent { 11 | public lastMessage: Message; 12 | @Input() private autoHide = false; 13 | @Input() private hidingTime = 10; 14 | 15 | constructor(messageService: MessageService) { 16 | messageService.messages.subscribe(m => this.lastMessage = m); 17 | } 18 | 19 | isVisible(): boolean { 20 | if (this.lastMessage) { 21 | if (this.autoHide) { 22 | if (this.lastMessage.date) { 23 | const timeDifference = Date.now() - this.lastMessage.date.getTime(); 24 | 25 | return timeDifference <= this.hidingTime * 1000; 26 | } else { 27 | return true; 28 | } 29 | } else { 30 | return true; 31 | } 32 | } else { 33 | return false; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/src/home/AccountList.tsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | import { Link } from 'react-router'; 3 | import { Account } from '../common/models/Account'; 4 | 5 | type AccountListProps = { 6 | title: string; 7 | accounts: Account[]; 8 | }; 9 | 10 | type AccountListState = {}; 11 | 12 | export default class AccountList extends Component { 13 | render() { 14 | return ( 15 | <> 16 |
{this.props.title}
17 | 18 | 19 | {this.props.accounts.map(account => 20 | 21 | 24 | 25 | )} 26 | 27 |
22 | {account.description} @{account.username} 23 |
28 | 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/profile/delete-account.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { User } from '../../shared/models/user.model'; 4 | import { AuthenticationService } from '../../shared/services/authentication.service'; 5 | import { AccountService } from '../../shared/services/account.service'; 6 | 7 | @Component({ 8 | selector: 'app-delete-account', 9 | templateUrl: './delete-account.component.html', 10 | standalone: false 11 | }) 12 | export class DeleteAccountComponent { 13 | public user: User = new User(); 14 | 15 | constructor(private authenticationService: AuthenticationService, private accountService: AccountService, private router: Router) { 16 | authenticationService.getUser().subscribe(data => { 17 | this.user = data; 18 | }); 19 | } 20 | 21 | delete() { 22 | this.accountService.deleteAccount(this.user.name) 23 | .subscribe(() => { 24 | this.router.navigate(['/login'], { 25 | queryParams: { 26 | logout: 1 27 | } 28 | }); 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/README.md: -------------------------------------------------------------------------------- 1 | # Client 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 7.3.2. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 28 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/home/top-bar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { UserState } from '../../shared/models/user-state.model'; 4 | import { AuthenticationService } from '../../shared/services/authentication.service'; 5 | 6 | @Component({ 7 | selector: 'app-top-bar', 8 | templateUrl: './top-bar.component.html', 9 | standalone: false 10 | }) 11 | export class TopBarComponent { 12 | @Input() 13 | userState: UserState; 14 | 15 | public searchText: string; 16 | 17 | constructor(private authenticationService: AuthenticationService, private router: Router) { 18 | } 19 | 20 | search() { 21 | this.router.navigate(['/account/search'], { 22 | queryParams: { 23 | searchText: this.searchText 24 | } 25 | }); 26 | } 27 | 28 | login() { 29 | this.router.navigateByUrl('/login'); 30 | } 31 | 32 | logout() { 33 | this.authenticationService.logout(() => { 34 | this.router.navigate(['/login'], { 35 | queryParams: { 36 | logout: 1 37 | } 38 | }); 39 | }); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /twitter-emulation-common/src/main/java/acme/twitter/domain/Tweet.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.domain; 2 | 3 | import java.util.Date; 4 | 5 | /** 6 | * Tweet. 7 | */ 8 | public class Tweet { 9 | private long id; 10 | private Account account; 11 | private String text; 12 | private Date date; 13 | 14 | public Tweet(long id, Account account, String text, Date date) { 15 | this.id = id; 16 | this.account = account; 17 | this.text = text; 18 | this.date = date; 19 | } 20 | 21 | public long getId() { 22 | return id; 23 | } 24 | 25 | public void setId(long id) { 26 | this.id = id; 27 | } 28 | 29 | public Account getAccount() { 30 | return account; 31 | } 32 | 33 | public void setAccount(Account account) { 34 | this.account = account; 35 | } 36 | 37 | public String getText() { 38 | return text; 39 | } 40 | 41 | public void setText(String text) { 42 | this.text = text; 43 | } 44 | 45 | public Date getDate() { 46 | return date; 47 | } 48 | 49 | public void setDate(Date date) { 50 | this.date = date; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/home/account.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject } from '@angular/core'; 2 | import { ActivatedRoute, Router } from '@angular/router'; 3 | import { Observer } from 'rxjs'; 4 | import { AuthenticationService } from '../../shared/services/authentication.service'; 5 | import { TweetService } from '../../shared/services/tweet.service'; 6 | import { HomeTweetsComponent } from './home-tweets.component'; 7 | import { USER_STATE, UserState } from '../../shared/models/user-state.model'; 8 | 9 | @Component({ 10 | selector: 'app-account', 11 | templateUrl: './home-tweets.component.html', 12 | standalone: false 13 | }) 14 | export class AccountComponent extends HomeTweetsComponent { 15 | constructor(authenticationService: AuthenticationService, activatedRoute: ActivatedRoute, router: Router, 16 | @Inject(USER_STATE) observer: Observer, private tweetService: TweetService) { 17 | super(authenticationService, activatedRoute, router, observer); 18 | } 19 | 20 | getData() { 21 | this.tweetService.getTimeline() 22 | .subscribe(data => { 23 | this.tweets = data; 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const eslint = require("@eslint/js"); 3 | const tseslint = require("typescript-eslint"); 4 | const angular = require("angular-eslint"); 5 | 6 | module.exports = tseslint.config( 7 | { 8 | files: ["**/*.ts"], 9 | extends: [ 10 | eslint.configs.recommended, 11 | ...tseslint.configs.recommended, 12 | ...tseslint.configs.stylistic, 13 | ...angular.configs.tsRecommended, 14 | ], 15 | processor: angular.processInlineTemplates, 16 | rules: { 17 | "@angular-eslint/directive-selector": [ 18 | "error", 19 | { 20 | type: "attribute", 21 | prefix: "app", 22 | style: "camelCase", 23 | }, 24 | ], 25 | "@angular-eslint/component-selector": [ 26 | "error", 27 | { 28 | type: "element", 29 | prefix: "app", 30 | style: "kebab-case", 31 | }, 32 | ], 33 | }, 34 | }, 35 | { 36 | files: ["**/*.html"], 37 | extends: [ 38 | ...angular.configs.templateRecommended, 39 | ...angular.configs.templateAccessibility, 40 | ], 41 | rules: {}, 42 | } 43 | ); 44 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage/client'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-angular-web/src/app/modules/tweet/new-tweet.component.html: -------------------------------------------------------------------------------- 1 |
2 |

New tweet

3 | 4 |
5 | 6 | 7 | 8 |
9 | 15 |
16 |
17 | 23 | 24 |
25 |
26 |
27 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-common-server/src/main/java/acme/twitter/controller/RestErrorAttributes.java: -------------------------------------------------------------------------------- 1 | package acme.twitter.controller; 2 | 3 | import org.springframework.boot.web.error.ErrorAttributeOptions; 4 | import org.springframework.boot.webmvc.error.DefaultErrorAttributes; 5 | import org.springframework.stereotype.Component; 6 | import org.springframework.web.context.request.RequestAttributes; 7 | import org.springframework.web.context.request.WebRequest; 8 | 9 | import java.util.Map; 10 | 11 | @Component 12 | public class RestErrorAttributes extends DefaultErrorAttributes { 13 | public static final String CUSTOM_MESSAGE_ATTRIBUTE = "customMessage"; 14 | 15 | @Override 16 | public Map getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) { 17 | Map errorAttributes = super.getErrorAttributes(webRequest, options); 18 | 19 | Object customMessage = webRequest.getAttribute(CUSTOM_MESSAGE_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); 20 | if (customMessage instanceof String) { 21 | errorAttributes.put(CUSTOM_MESSAGE_ATTRIBUTE, customMessage); 22 | } 23 | 24 | return errorAttributes; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /twitter-emulation-spring-boot-react-web/src/common/validation/ValidatableTextarea.tsx: -------------------------------------------------------------------------------- 1 | import { Component, ContextType } from 'react'; 2 | import { ValidationContext } from './ValidationContext'; 3 | 4 | type ValidatableTextareaProps = { 5 | className: string; 6 | rows: number; 7 | id: string; 8 | name: string; 9 | value: string; 10 | onChange: any; 11 | autoFocus: boolean; 12 | } 13 | 14 | type ValidatableTextareaState = {} 15 | 16 | export class ValidatableTextarea extends Component { 17 | static contextType = ValidationContext; 18 | declare context: ContextType; 19 | 20 | public static defaultProps = { 21 | className: '', 22 | autoFocus: false 23 | }; 24 | 25 | render() { 26 | const contextClassNames = this.context.getFieldClasses(this.props.name); 27 | 28 | return