├── .gitignore ├── .gitmodules ├── .settings └── org.eclipse.wst.html.core.prefs ├── CONTRIBUTING.md ├── LICENSE.md ├── NOTICE.txt ├── README.md ├── concourse ├── README.adoc ├── manifests │ ├── sagan-renderer-production.yml │ ├── sagan-renderer-staging.yml │ ├── sagan-site-production.yml │ └── sagan-site-staging.yml ├── pipeline.yml ├── scripts │ ├── build-sagan-renderer.sh │ ├── build-sagan-site.sh │ └── common.sh └── tasks │ ├── build-sagan-renderer.yml │ └── build-sagan-site.yml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── sagan-client ├── .gitignore ├── README.md ├── build.gradle ├── package.json ├── src │ ├── app │ │ ├── admin.js │ │ ├── blog.js │ │ ├── calendar-releases.js │ │ ├── guide.js │ │ ├── main.js │ │ ├── profile.js │ │ ├── project.js │ │ ├── run_prettify.js │ │ ├── team.js │ │ └── theme.js │ ├── css │ │ ├── _prettify.scss │ │ ├── admin.css │ │ ├── blog.css │ │ ├── calendar-releases.scss │ │ ├── dark.scss │ │ ├── guide.css │ │ ├── main.css │ │ ├── profile.css │ │ ├── project.css │ │ └── team.css │ ├── fonts │ │ ├── generator_config.txt │ │ ├── metropolis-bold-demo.html │ │ ├── metropolis-bold-webfont.woff │ │ ├── metropolis-bold-webfont.woff2 │ │ ├── metropolis-extrabold-demo.html │ │ ├── metropolis-extrabold-webfont.woff │ │ ├── metropolis-extrabold-webfont.woff2 │ │ ├── metropolis-regular-demo.html │ │ ├── metropolis-regular-webfont.woff │ │ ├── metropolis-regular-webfont.woff2 │ │ ├── open-sans-v17-latin │ │ │ ├── open-sans-v17-latin-600.eot │ │ │ ├── open-sans-v17-latin-600.svg │ │ │ ├── open-sans-v17-latin-600.ttf │ │ │ ├── open-sans-v17-latin-600.woff │ │ │ ├── open-sans-v17-latin-600.woff2 │ │ │ ├── open-sans-v17-latin-700.eot │ │ │ ├── open-sans-v17-latin-700.svg │ │ │ ├── open-sans-v17-latin-700.ttf │ │ │ ├── open-sans-v17-latin-700.woff │ │ │ ├── open-sans-v17-latin-700.woff2 │ │ │ ├── open-sans-v17-latin-italic.eot │ │ │ ├── open-sans-v17-latin-italic.svg │ │ │ ├── open-sans-v17-latin-italic.ttf │ │ │ ├── open-sans-v17-latin-italic.woff │ │ │ ├── open-sans-v17-latin-italic.woff2 │ │ │ ├── open-sans-v17-latin-regular.eot │ │ │ ├── open-sans-v17-latin-regular.svg │ │ │ ├── open-sans-v17-latin-regular.ttf │ │ │ ├── open-sans-v17-latin-regular.woff │ │ │ └── open-sans-v17-latin-regular.woff2 │ │ ├── specimen_files │ │ │ ├── grid_12-825-55-15.css │ │ │ └── specimen_stylesheet.css │ │ └── work-sans-v5-latin │ │ │ ├── work-sans-v5-latin-700.eot │ │ │ ├── work-sans-v5-latin-700.svg │ │ │ ├── work-sans-v5-latin-700.ttf │ │ │ ├── work-sans-v5-latin-700.woff │ │ │ ├── work-sans-v5-latin-700.woff2 │ │ │ ├── work-sans-v5-latin-regular.eot │ │ │ ├── work-sans-v5-latin-regular.svg │ │ │ ├── work-sans-v5-latin-regular.ttf │ │ │ ├── work-sans-v5-latin-regular.woff │ │ │ └── work-sans-v5-latin-regular.woff2 │ └── images │ │ ├── Eclipse2014_RGB.svg │ │ ├── OG-Spring.png │ │ ├── SpringCloudDataFlow.png │ │ ├── angle.svg │ │ ├── apache-kakfa-dark.svg │ │ ├── apache-kakfa.svg │ │ ├── batch-dark.svg │ │ ├── batch-vod1.jpg │ │ ├── batch-vod2.jpg │ │ ├── batch-vod3.jpg │ │ ├── batch.svg │ │ ├── bg-featured-post-dark.svg │ │ ├── bg-featured-post.svg │ │ ├── big-check.svg │ │ ├── close.svg │ │ ├── cloud-dark.svg │ │ ├── cloud-diagram-dark.svg │ │ ├── cloud-diagram.svg │ │ ├── cloud-res1.jpg │ │ ├── cloud-res3.jpg │ │ ├── cloud.svg │ │ ├── dataflow-stream-details.png │ │ ├── diagram-batch-dark.svg │ │ ├── diagram-batch.svg │ │ ├── diagram-batch2-dark.svg │ │ ├── diagram-batch2.svg │ │ ├── diagram-microservices-dark.svg │ │ ├── diagram-microservices.svg │ │ ├── diagram-microservices1.png │ │ ├── diagram-reactive-dark.svg │ │ ├── diagram-reactive.svg │ │ ├── diagram-serverless-standalone-dark.svg │ │ ├── diagram-serverless-standalone.svg │ │ ├── eclipse-shot.jpg │ │ ├── external.svg │ │ ├── favicon.ico │ │ ├── footer-circles.svg │ │ ├── gradle-enterprise-dark-green-primary.svg │ │ ├── gradle-enterprise-white.svg │ │ ├── guides-dark.svg │ │ ├── guides.svg │ │ ├── icon-github.svg │ │ ├── icon-marker-000.svg │ │ ├── icon-spring-boot.svg │ │ ├── icon-spring-cloud.svg │ │ ├── icon-spring-data-flow.svg │ │ ├── icon-spring-framework.svg │ │ ├── icon-spring.svg │ │ ├── icon-twitter.svg │ │ ├── logo-asciidoctor.svg │ │ ├── logo-browserstack-white.svg │ │ ├── logo-browserstack.svg │ │ ├── logo-cassandra.png │ │ ├── logo-cloud-spanner.png │ │ ├── logo-cosmos.png │ │ ├── logo-eclipse-ide.svg │ │ ├── logo-eclipse-white.svg │ │ ├── logo-eclipse.svg │ │ ├── logo-geode.png │ │ ├── logo-intellij.svg │ │ ├── logo-jetbrains.svg │ │ ├── logo-jfrog-artifactory-white.png │ │ ├── logo-jfrog-artifactory.png │ │ ├── logo-jfrog-bintray-white.png │ │ ├── logo-jfrog-bintray.png │ │ ├── logo-mongodb.png │ │ ├── logo-neo4j.png │ │ ├── logo-redis.png │ │ ├── logo-solr.png │ │ ├── logo-spring-tools-4.svg │ │ ├── logo-spring-tools-gear.svg │ │ ├── logo-spring.svg │ │ ├── logo-stackoverflow.svg │ │ ├── logo-structure101-white.png │ │ ├── logo-structure101.png │ │ ├── logo-yourkit-black.png │ │ ├── logo-yourkit.png │ │ ├── micro-res1.png │ │ ├── micro-res2.png │ │ ├── micro-res3.png │ │ ├── microservices-dark.svg │ │ ├── microservices.svg │ │ ├── pacman2.svg │ │ ├── podcast.jpg │ │ ├── podcast1.png │ │ ├── podcast2.png │ │ ├── podcast3.png │ │ ├── pop-quiz.svg │ │ ├── projects │ │ ├── logo-io-platform.png │ │ ├── logo-session.png │ │ ├── logo-web-services.svg │ │ ├── spring-amqp.svg │ │ ├── spring-android.svg │ │ ├── spring-batch.svg │ │ ├── spring-boot.svg │ │ ├── spring-cloud.svg │ │ ├── spring-data-flow.svg │ │ ├── spring-data.svg │ │ ├── spring-flo.svg │ │ ├── spring-framework.svg │ │ ├── spring-hateoas.svg │ │ ├── spring-integration.svg │ │ ├── spring-kafka.svg │ │ ├── spring-ldap.svg │ │ ├── spring-mobile.svg │ │ ├── spring-restdocs.png │ │ ├── spring-security.svg │ │ ├── spring-shell.svg │ │ ├── spring-social.svg │ │ ├── spring-statemachine.svg │ │ ├── spring-web-flo.svg │ │ ├── spring-xd.svg │ │ └── spring.svg │ │ ├── quick-img-1-dark.png │ │ ├── quick-img-1.png │ │ ├── quick-img2.png │ │ ├── quick-img3.png │ │ ├── quote.svg │ │ ├── reactive-dark.svg │ │ ├── reactive-res1.png │ │ ├── reactive-res2.png │ │ ├── reactive-res3.png │ │ ├── reactive.svg │ │ ├── round-sharp.svg │ │ ├── serverless-dark.svg │ │ ├── serverless-res1.png │ │ ├── serverless-res2.png │ │ ├── serverless-res3.png │ │ ├── serverless.svg │ │ ├── spring-initializr.svg │ │ ├── spring-logo.png │ │ ├── spring-logo.svg │ │ ├── streams-dark.svg │ │ ├── streams-res1.png │ │ ├── streams-res2.png │ │ ├── streams-res3.png │ │ ├── streams.svg │ │ ├── theia-logo.svg │ │ ├── theia-shot.jpg │ │ ├── tools-img1.jpg │ │ ├── tools-img2.jpg │ │ ├── tools-img3.jpg │ │ ├── tools-img4.jpg │ │ ├── tools-left-tri.svg │ │ ├── tools-right-geo.svg │ │ ├── tutorial.png │ │ ├── twitter.svg │ │ ├── vid1.png │ │ ├── vid2.png │ │ ├── vid3.png │ │ ├── video-gursel.jpg │ │ ├── video-jackson.jpg │ │ ├── video-tracing.png │ │ ├── vs-shot.jpg │ │ ├── vscode-logo.svg │ │ ├── webapps-video.png │ │ ├── why-spring-dark.svg │ │ ├── why-spring-mobile.svg │ │ ├── why-spring.svg │ │ ├── wicksell.jpg │ │ ├── world-map.svg │ │ └── youtube.svg └── webpack.config.js ├── sagan-renderer ├── .gitignore ├── build.gradle └── src │ ├── main │ ├── java │ │ └── sagan │ │ │ └── renderer │ │ │ ├── AsciidoctorConfig.java │ │ │ ├── IndexController.java │ │ │ ├── RendererApplication.java │ │ │ ├── RendererProperties.java │ │ │ ├── github │ │ │ ├── GithubClient.java │ │ │ ├── GithubResourceNotFoundException.java │ │ │ └── Repository.java │ │ │ ├── guides │ │ │ ├── GuideContentModel.java │ │ │ ├── GuideImage.java │ │ │ ├── GuideModel.java │ │ │ ├── GuideModelAssembler.java │ │ │ ├── GuideRenderer.java │ │ │ ├── GuideRenderingException.java │ │ │ ├── GuideType.java │ │ │ ├── GuidesController.java │ │ │ └── content │ │ │ │ ├── AsciidoctorGuideContentContributor.java │ │ │ │ ├── GuideContentContributor.java │ │ │ │ ├── ImagesGuideContentContributor.java │ │ │ │ └── PwsGuideContentContributor.java │ │ │ └── markup │ │ │ ├── AsciidoctorRenderer.java │ │ │ ├── MarkdownRenderer.java │ │ │ ├── MarkdownToHtmlSerializer.java │ │ │ ├── MarkupController.java │ │ │ ├── MarkupRenderer.java │ │ │ └── PrettifyVerbatimSerializer.java │ └── resources │ │ └── application.yml │ └── test │ ├── java │ └── sagan │ │ └── renderer │ │ ├── github │ │ └── GithubClientTests.java │ │ ├── guides │ │ ├── GuideModelTests.java │ │ ├── GuideRendererTests.java │ │ └── GuidesControllerTests.java │ │ └── markup │ │ ├── AsciidoctorRendererTests.java │ │ ├── MarkdownRendererTests.java │ │ └── MarkupControllerTests.java │ └── resources │ └── sagan │ └── renderer │ ├── github │ ├── gs-rest-service.json │ ├── gs-rest-service.zip │ ├── spring-guides-repos-page1.json │ └── spring-guides-repos-page2.json │ └── guides │ ├── gs-sample-pws.zip │ └── gs-sample.zip ├── sagan-site ├── .settings │ ├── org.eclipse.jdt.core.prefs │ └── org.eclipse.jdt.ui.prefs ├── build.gradle └── src │ ├── docs │ └── asciidoc │ │ └── index.adoc │ ├── main │ ├── java │ │ └── sagan │ │ │ ├── CacheConfig.java │ │ │ ├── DatabaseConfig.java │ │ │ ├── ModelMapperConfig.java │ │ │ ├── MvcConfig.java │ │ │ ├── SaganApplication.java │ │ │ ├── SaganProfiles.java │ │ │ ├── SecurityConfig.java │ │ │ ├── SiteApplication.java │ │ │ ├── SiteProperties.java │ │ │ ├── ThymeleafViewResolverCustomizer.java │ │ │ ├── UrlRewriterFilterConfig.java │ │ │ ├── site │ │ │ ├── admin │ │ │ │ └── AdminController.java │ │ │ ├── blog │ │ │ │ ├── BlogService.java │ │ │ │ ├── MarkdownService.java │ │ │ │ ├── Post.java │ │ │ │ ├── PostCategory.java │ │ │ │ ├── PostCategoryFormatter.java │ │ │ │ ├── PostContentRenderer.java │ │ │ │ ├── PostForm.java │ │ │ │ ├── PostFormAdapter.java │ │ │ │ ├── PostFormat.java │ │ │ │ ├── PostMovedException.java │ │ │ │ ├── PostNotFoundException.java │ │ │ │ ├── PostRepository.java │ │ │ │ ├── PostSummary.java │ │ │ │ └── support │ │ │ │ │ ├── AtomFeedController.java │ │ │ │ │ ├── AtomFeedView.java │ │ │ │ │ ├── BlogAdminController.java │ │ │ │ │ ├── BlogController.java │ │ │ │ │ ├── PostView.java │ │ │ │ │ └── SiteUrl.java │ │ │ ├── events │ │ │ │ ├── Event.java │ │ │ │ ├── EventDeserializer.java │ │ │ │ ├── EventsCalendarService.java │ │ │ │ ├── EventsController.java │ │ │ │ ├── GoogleCalendar.java │ │ │ │ ├── InvalidCalendarException.java │ │ │ │ └── Period.java │ │ │ ├── guides │ │ │ │ ├── AbstractGuide.java │ │ │ │ ├── DefaultGuideHeader.java │ │ │ │ ├── DocsWebhookController.java │ │ │ │ ├── GettingStartedGuide.java │ │ │ │ ├── GettingStartedGuideController.java │ │ │ │ ├── GettingStartedGuides.java │ │ │ │ ├── Guide.java │ │ │ │ ├── GuideHeader.java │ │ │ │ ├── GuideImage.java │ │ │ │ ├── GuideIndexController.java │ │ │ │ ├── GuidesRepository.java │ │ │ │ ├── Topical.java │ │ │ │ ├── TopicalController.java │ │ │ │ ├── Topicals.java │ │ │ │ ├── Tutorial.java │ │ │ │ ├── TutorialController.java │ │ │ │ ├── Tutorials.java │ │ │ │ └── WebhookAuthenticationException.java │ │ │ ├── learn │ │ │ │ └── LearnController.java │ │ │ ├── projects │ │ │ │ ├── Display.java │ │ │ │ ├── InvalidProjectGenerationDateException.java │ │ │ │ ├── InvalidProjectReleaseException.java │ │ │ │ ├── InvalidVersionException.java │ │ │ │ ├── MarkupDocument.java │ │ │ │ ├── Project.java │ │ │ │ ├── ProjectGeneration.java │ │ │ │ ├── ProjectGenerationsInfo.java │ │ │ │ ├── ProjectGroup.java │ │ │ │ ├── ProjectGroupRepository.java │ │ │ │ ├── ProjectMetadataService.java │ │ │ │ ├── ProjectRepository.java │ │ │ │ ├── ProjectSample.java │ │ │ │ ├── ProjectsController.java │ │ │ │ ├── Release.java │ │ │ │ ├── ReleaseRepository.java │ │ │ │ ├── ReleaseStatus.java │ │ │ │ ├── Repository.java │ │ │ │ ├── SupportStatus.java │ │ │ │ ├── Version.java │ │ │ │ ├── VersionConverter.java │ │ │ │ ├── admin │ │ │ │ │ ├── ProjectAdminController.java │ │ │ │ │ ├── ProjectFormGenerations.java │ │ │ │ │ ├── ProjectFormInfo.java │ │ │ │ │ ├── ProjectFormMetadata.java │ │ │ │ │ ├── ProjectFormReleases.java │ │ │ │ │ └── ProjectFormSamples.java │ │ │ │ ├── badge │ │ │ │ │ ├── BadgeController.java │ │ │ │ │ ├── BadgeSvg.java │ │ │ │ │ └── VersionBadgeService.java │ │ │ │ └── support │ │ │ │ │ └── SupportPolicyProjectGenerationsProcessor.java │ │ │ ├── renderer │ │ │ │ ├── GuideContent.java │ │ │ │ ├── GuideImage.java │ │ │ │ ├── GuideMetadata.java │ │ │ │ ├── GuideType.java │ │ │ │ └── SaganRendererClient.java │ │ │ ├── team │ │ │ │ ├── GeoLocation.java │ │ │ │ ├── Link.java │ │ │ │ ├── MemberProfile.java │ │ │ │ ├── MemberProfileBuilder.java │ │ │ │ ├── TeamLocation.java │ │ │ │ └── support │ │ │ │ │ ├── DefaultTeamImporter.java │ │ │ │ │ ├── GeoLocationFormatter.java │ │ │ │ │ ├── MemberNotFoundException.java │ │ │ │ │ ├── SignInService.java │ │ │ │ │ ├── TeamAdminController.java │ │ │ │ │ ├── TeamController.java │ │ │ │ │ ├── TeamImporter.java │ │ │ │ │ ├── TeamRepository.java │ │ │ │ │ └── TeamService.java │ │ │ ├── tools │ │ │ │ ├── SpringToolsDownload.java │ │ │ │ ├── SpringToolsPlatform.java │ │ │ │ ├── SpringToolsPlatformRepository.java │ │ │ │ └── support │ │ │ │ │ ├── SpringToolsAdminController.java │ │ │ │ │ └── SpringToolsController.java │ │ │ └── webapi │ │ │ │ ├── IndexController.java │ │ │ │ ├── WebApiControllerAdvice.java │ │ │ │ ├── generation │ │ │ │ ├── GenerationMetadata.java │ │ │ │ ├── GenerationMetadataAssembler.java │ │ │ │ └── GenerationMetadataController.java │ │ │ │ ├── legacy │ │ │ │ └── LegacyProjectMetadataController.java │ │ │ │ ├── project │ │ │ │ ├── ProjectMetadata.java │ │ │ │ ├── ProjectMetadataAssembler.java │ │ │ │ └── ProjectMetadataController.java │ │ │ │ ├── release │ │ │ │ ├── InvalidReleaseException.java │ │ │ │ ├── ReleaseMetadata.java │ │ │ │ ├── ReleaseMetadataAssembler.java │ │ │ │ ├── ReleaseMetadataController.java │ │ │ │ └── ReleaseMetadataInput.java │ │ │ │ └── repository │ │ │ │ ├── RepositoryMetadata.java │ │ │ │ ├── RepositoryMetadataAssembler.java │ │ │ │ └── RepositoryMetadataController.java │ │ │ └── support │ │ │ ├── DateFactory.java │ │ │ ├── ResourceNotFoundException.java │ │ │ ├── StaticPagePathFinder.java │ │ │ ├── TuckeyRewriteFilter.java │ │ │ ├── cache │ │ │ ├── CachedRestClient.java │ │ │ ├── JsonRedisTemplate.java │ │ │ └── RedisCacheManager.java │ │ │ ├── github │ │ │ └── GitHubConfig.java │ │ │ └── nav │ │ │ ├── Navigation.java │ │ │ ├── PageElement.java │ │ │ ├── PageElementsBuilder.java │ │ │ ├── PageableFactory.java │ │ │ ├── PaginationInfo.java │ │ │ └── Section.java │ └── resources │ │ ├── META-INF │ │ └── spring-devtools.properties │ │ ├── application.yml │ │ ├── badge │ │ ├── Verdana.ttf │ │ ├── milestone.svg │ │ ├── release.svg │ │ └── snapshot.svg │ │ ├── db │ │ ├── dev │ │ │ └── V99__fixtures.sql │ │ └── migration │ │ │ ├── V1__initialize.sql │ │ │ ├── V2__projectpages.sql │ │ │ ├── V3__springtools.sql │ │ │ ├── V4__projects.sql │ │ │ ├── V5__project_refactoring.sql │ │ │ └── V6__tools_version.sql │ │ ├── elasticsearch │ │ ├── mappings │ │ │ ├── apidoc.json │ │ │ ├── blogpost.json │ │ │ ├── guidedoc.json │ │ │ ├── projectpage.json │ │ │ ├── referencedoc.json │ │ │ └── sitepage.json │ │ └── settings.json │ │ ├── notifications │ │ └── gh-pages-updated.md │ │ ├── templates │ │ ├── admin │ │ │ ├── blog │ │ │ │ ├── _post_form.html │ │ │ │ ├── edit.html │ │ │ │ ├── index.html │ │ │ │ └── new.html │ │ │ ├── layout.html │ │ │ ├── project │ │ │ │ ├── edit-info.html │ │ │ │ ├── edit-metadata.html │ │ │ │ ├── edit-releases.html │ │ │ │ ├── edit-samples.html │ │ │ │ ├── edit-support.html │ │ │ │ └── index.html │ │ │ ├── show.html │ │ │ ├── team │ │ │ │ ├── edit.html │ │ │ │ └── index.html │ │ │ └── tools │ │ │ │ ├── edit.html │ │ │ │ └── index.html │ │ ├── blog │ │ │ ├── _full_post.html │ │ │ ├── _right-pane.html │ │ │ ├── index.html │ │ │ └── show.html │ │ ├── error.html │ │ ├── error │ │ │ ├── 404.html │ │ │ └── 500.html │ │ ├── events │ │ │ ├── community.html │ │ │ └── list.html │ │ ├── guides │ │ │ ├── gs │ │ │ │ └── guide.html │ │ │ └── index.html │ │ ├── layout.html │ │ ├── learn │ │ │ └── index.html │ │ ├── pages │ │ │ ├── batch.html │ │ │ ├── cloud.html │ │ │ ├── event-driven.html │ │ │ ├── index.html │ │ │ ├── microservices.html │ │ │ ├── quickstart.html │ │ │ ├── reactive.html │ │ │ ├── serverless.html │ │ │ ├── services.html │ │ │ ├── signin.html │ │ │ ├── support.html │ │ │ ├── thank-you.html │ │ │ ├── trademarks.html │ │ │ ├── training.html │ │ │ ├── web-applications.html │ │ │ ├── webapps.html │ │ │ └── why-spring.html │ │ ├── projects │ │ │ ├── _project_links.html │ │ │ ├── index.html │ │ │ ├── show.html │ │ │ └── sidebar.html │ │ ├── search │ │ │ ├── _facets.html │ │ │ ├── results.html │ │ │ └── searcherror.html │ │ ├── svg │ │ │ ├── _icons-color.html │ │ │ ├── _icons.html │ │ │ └── _shapes.html │ │ ├── team │ │ │ ├── index.html │ │ │ └── show.html │ │ ├── tools │ │ │ └── list.html │ │ └── understanding │ │ │ └── show.html │ │ └── urlrewrite.xml │ └── test │ ├── java │ └── sagan │ │ ├── GithubAuthenticationSigninAdapterTests.java │ │ ├── SecurityContextAuthenticationFilterTests.java │ │ ├── rewrite │ │ └── support │ │ │ └── RewriteTests.java │ │ ├── site │ │ ├── blog │ │ │ ├── BlogPostContentRendererTests.java │ │ │ ├── BlogService_UpdatePostTests.java │ │ │ ├── BlogService_ValidPostTests.java │ │ │ ├── PostBuilder.java │ │ │ ├── PostCategoryFormatterTests.java │ │ │ ├── PostFormAdapter_CreatePostTests.java │ │ │ ├── PostFormAdapter_UpdatePostTests.java │ │ │ ├── PostSummaryTests.java │ │ │ ├── PostTests.java │ │ │ ├── SetSystemProperty.java │ │ │ └── support │ │ │ │ ├── AtomFeedControllerTests.java │ │ │ │ ├── BlogAdminControllerTests.java │ │ │ │ ├── BlogAtomFeedViewTests.java │ │ │ │ ├── BlogControllerTests.java │ │ │ │ ├── BlogController_BroadcastPostsTests.java │ │ │ │ ├── BlogController_PublishedPostsForCategoryTests.java │ │ │ │ ├── BlogController_PublishedPostsTests.java │ │ │ │ ├── BlogController_ShowTests.java │ │ │ │ └── PostViewTests.java │ │ ├── events │ │ │ └── EventsCalendarServiceTests.java │ │ ├── guides │ │ │ ├── DefaultGuideHeaderTests.java │ │ │ ├── DocsWebhookControllerTests.java │ │ │ ├── GuidesJsonTests.java │ │ │ └── GuidesRepositoryTest.java │ │ ├── projects │ │ │ ├── ProjectGenerationTests.java │ │ │ ├── ProjectsControllerTests.java │ │ │ ├── ReleaseStatusTests.java │ │ │ ├── ReleaseTests.java │ │ │ ├── ReleaseVersionTests.java │ │ │ ├── RepositoryTests.java │ │ │ ├── VersionTests.java │ │ │ ├── badge │ │ │ │ └── BadgeControllerTests.java │ │ │ └── support │ │ │ │ └── SupportPolicyProjectGenerationsProcessorTests.java │ │ ├── team │ │ │ └── support │ │ │ │ ├── GeoLocationFormatterTests.java │ │ │ │ ├── MemberProfileTests.java │ │ │ │ ├── SignInServiceTests.java │ │ │ │ ├── TeamControllerTests.java │ │ │ │ └── TeamServiceTests.java │ │ └── webapi │ │ │ ├── ConstrainedFields.java │ │ │ ├── IndexControllerTests.java │ │ │ ├── MvcTestConfig.java │ │ │ ├── WebApiTest.java │ │ │ ├── generation │ │ │ └── GenerationMetadataControllerTests.java │ │ │ ├── project │ │ │ └── ProjectMetadataControllerTests.java │ │ │ ├── release │ │ │ └── ReleaseMetadataControllerTests.java │ │ │ └── repository │ │ │ └── RepositoryMetadataControllerTests.java │ │ └── support │ │ ├── DateTestUtils.java │ │ ├── Fixtures.java │ │ └── nav │ │ ├── PaginationInfo_PaginationElementsTests.java │ │ └── PaginationInfo_PreviousAndNextControlsTests.java │ └── resources │ ├── fixtures │ ├── github │ │ ├── ghPagesWebhook.json │ │ ├── ghTeamInfo.json │ │ ├── ghUserProfile-asmith.json │ │ └── ghUserProfile-jdoe.json │ ├── images │ │ └── testImage.png │ └── webhooks │ │ ├── docsWebhook.json │ │ └── pingWebhook.json │ ├── logback-test.xml │ ├── org │ └── springframework │ │ └── restdocs │ │ └── templates │ │ └── request-fields.snippet │ └── sagan │ └── site │ ├── events │ ├── html-event.json │ ├── invalid.json │ ├── multi-events.json │ └── single-event.json │ ├── guides │ └── rest-service.json │ └── renderer │ ├── guides.json │ ├── messaging-redis-content.json │ ├── messaging-redis.json │ ├── rest-service-content.json │ ├── rest-service.json │ └── root.json ├── settings.gradle ├── style ├── README.md ├── sagan-format.xml └── sagan.importorder └── util ├── README.md ├── bisect.sh ├── convert-tabs-to-spaces.sh ├── recreate-elasticsearch-index.sh ├── replicate-db.sh └── test-gh-pages-webhook.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Concourse 2 | concourse/credentials.yml 3 | 4 | # Misc 5 | *~ 6 | .#* 7 | dependency-reduced-pom.xml 8 | .DS_Store 9 | data/ 10 | tmp/ 11 | *.sw[op] 12 | classes/ 13 | test.log 14 | logs/ 15 | work/ 16 | .bower-* 17 | lib/ 18 | dist/ 19 | 20 | # Eclipse 21 | # > the following should be created by `./gradlew eclipse` 22 | .classpath 23 | .project 24 | # > ignore all eclipse settings by default 25 | .settings 26 | # > except the following, primarily for formatting purposes 27 | !.settings/org.eclipse.jdt.core.prefs 28 | !.settings/org.eclipse.jdt.ui.prefs 29 | !.settings/org.eclipse.wst.html.core.prefs 30 | # > other misc eclipse artifacts 31 | .springBeans 32 | bin 33 | atlassian-ide-plugin.xml 34 | 35 | # IDEA 36 | .idea 37 | *.iml 38 | *.ipr 39 | *.iws 40 | out 41 | 42 | # Gradle 43 | .gradle 44 | build 45 | /build 46 | 47 | # Structure101 48 | sagan.java.hsp 49 | 50 | # jspm 51 | jspm_packages/ 52 | npm-debug.log 53 | .cram/ 54 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "wiki"] 2 | path = wiki 3 | url = git@github.com:spring-io/sagan.wiki.git 4 | -------------------------------------------------------------------------------- /.settings/org.eclipse.wst.html.core.prefs: -------------------------------------------------------------------------------- 1 | attrDuplicate=-1 2 | attrInvalidName=-1 3 | attrInvalidValue=-1 4 | attrNameMismatch=-1 5 | attrUndefName=-1 6 | attrUndefValue=-1 7 | attrValueEqualsMissing=-1 8 | attrValueMismatch=-1 9 | attrValueUnclosed=-1 10 | cdataInvalidContent=-1 11 | cdataUnclosed=-1 12 | commentInvalidContent=-1 13 | commentUnclosed=-1 14 | docDoctypeUnclosed=-1 15 | docDuplicateTag=-1 16 | docInvalidChar=-1 17 | docInvalidContent=-1 18 | eclipse.preferences.version=1 19 | elemCoexistence=-1 20 | elemDuplicate=-1 21 | elemEndInvalidCase=-1 22 | elemInvalidContent=-1 23 | elemInvalidDirective=-1 24 | elemInvalidEmptyTag=-1 25 | elemInvalidName=-1 26 | elemInvalidText=-1 27 | elemMissingEnd=-1 28 | elemMissingStart=-1 29 | elemStartInvalidCase=-1 30 | elemUnclosedEndTag=-1 31 | elemUnclosedStartTag=-1 32 | elemUnknownName=-1 33 | elemUnnecessaryEnd=-1 34 | piInvalidContent=-1 35 | piUnclosed=-1 36 | piUndefined=-1 37 | refInvalidContent=-1 38 | resourceNotFound=-1 39 | use-project-settings=true 40 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Pivotal Software, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | * Neither the name of Pivotal Software, Inc. nor the 15 | names of its contributors may be used to endorse or promote products 16 | derived from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /concourse/manifests/sagan-renderer-production.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: sagan-renderer 4 | buildpacks: 5 | - java_buildpack 6 | instances: 1 7 | health-check-type: http 8 | health-check-http-endpoint: /actuator/health 9 | memory: 1024M 10 | disk_quota: 1024M 11 | routes: 12 | - route: sagan-renderer.cfapps.io 13 | services: -------------------------------------------------------------------------------- /concourse/manifests/sagan-renderer-staging.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: sagan-renderer 4 | buildpacks: 5 | - java_buildpack 6 | instances: 1 7 | health-check-type: http 8 | health-check-http-endpoint: /actuator/health 9 | memory: 1024M 10 | disk_quota: 1024M 11 | routes: 12 | - route: sagan-renderer-staging.cfapps.io 13 | services: -------------------------------------------------------------------------------- /concourse/manifests/sagan-site-production.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: sagan 4 | buildpacks: 5 | - java_buildpack 6 | instances: 2 7 | health-check-type: http 8 | health-check-http-endpoint: /health 9 | timeout: 120 10 | memory: 2048M 11 | disk_quota: 1024M 12 | routes: 13 | - route: spring.io 14 | - route: www.spring.io 15 | - route: sagan-production.cfapps.io 16 | services: 17 | - papertrail 18 | - sagan-db 19 | - sagan-cache 20 | -------------------------------------------------------------------------------- /concourse/manifests/sagan-site-staging.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: sagan 4 | buildpacks: 5 | - java_buildpack 6 | instances: 1 7 | health-check-type: http 8 | health-check-http-endpoint: /health 9 | memory: 2048M 10 | disk_quota: 1024M 11 | routes: 12 | - route: staging.spring.io 13 | - route: sagan-staging.cfapps.io 14 | services: 15 | - sagan-db 16 | - sagan-cache -------------------------------------------------------------------------------- /concourse/scripts/build-sagan-renderer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e -u -x 3 | source $(dirname $0)/common.sh 4 | pushd sagan-renderer-repo > /dev/null 5 | ./gradlew --console plain :sagan-renderer:build 6 | popd > /dev/null 7 | cp -R sagan-renderer-repo/sagan-renderer/build/* build -------------------------------------------------------------------------------- /concourse/scripts/build-sagan-site.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e -u -x 3 | source $(dirname $0)/common.sh 4 | chown gradle:gradle -R ./sagan-site-repo 5 | pushd sagan-site-repo > /dev/null 6 | su gradle -c "./gradlew --console plain :sagan-site:build" 7 | popd > /dev/null 8 | cp -R sagan-site-repo/sagan-site/build/* build -------------------------------------------------------------------------------- /concourse/scripts/common.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [[ -d $PWD/gradle && ! -d $HOME/.gradle ]]; then 3 | ln -s "$PWD/gradle" "$HOME/.gradle" 4 | fi -------------------------------------------------------------------------------- /concourse/tasks/build-sagan-renderer.yml: -------------------------------------------------------------------------------- 1 | --- 2 | platform: linux 3 | image_resource: 4 | type: docker-image 5 | source: 6 | repository: gradle 7 | tag: jdk8 8 | inputs: 9 | - name: sagan-renderer-repo 10 | outputs: 11 | - name: build 12 | caches: 13 | - path: gradle 14 | run: 15 | path: /bin/bash 16 | args: 17 | - -ec 18 | - | 19 | ${PWD}/sagan-renderer-repo/concourse/scripts/build-sagan-renderer.sh -------------------------------------------------------------------------------- /concourse/tasks/build-sagan-site.yml: -------------------------------------------------------------------------------- 1 | --- 2 | platform: linux 3 | image_resource: 4 | type: docker-image 5 | source: 6 | repository: gradle 7 | tag: jdk8 8 | inputs: 9 | - name: sagan-site-repo 10 | outputs: 11 | - name: build 12 | caches: 13 | - path: gradle 14 | params: 15 | run: 16 | user: root 17 | path: /bin/bash 18 | args: 19 | - -ec 20 | - | 21 | ${PWD}/sagan-site-repo/concourse/scripts/build-sagan-site.sh -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip 6 | -------------------------------------------------------------------------------- /sagan-client/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /sagan-client/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.github.node-gradle.node' version '2.2.3' 3 | id 'java' 4 | } 5 | 6 | node { 7 | version = '14.4.0' 8 | npmVersion = '6.14.4' 9 | download = true 10 | } 11 | 12 | def jsBuildDir = project.buildDir.absolutePath + '/dist' 13 | 14 | jar { 15 | from jsBuildDir 16 | eachFile { details -> 17 | details.path = details.path.startsWith('META-INF') ?: 'static/'+details.path 18 | } 19 | includeEmptyDirs = false 20 | } 21 | 22 | npm_run_build.dependsOn(npm_install) 23 | jar.dependsOn npm_run_build -------------------------------------------------------------------------------- /sagan-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sagan", 3 | "description": "Client application for spring.io", 4 | "private": true, 5 | "repository": { 6 | "type": "git", 7 | "url": "http://github.com/spring-io/sagan.git" 8 | }, 9 | "dependencies": { 10 | "@fancyapps/fancybox": "^3.5.7", 11 | "@fortawesome/fontawesome-free": "^5.12.0", 12 | "bulma": "^0.8.0", 13 | "clipboard": "^2.0.4", 14 | "jquery": "^3.4.1", 15 | "jquery-datetimepicker": "^2.5.21", 16 | "js-search": "^1.4.3", 17 | "local-storage": "^2.0.0", 18 | "luxon": "^1.23.0" 19 | }, 20 | "devDependencies": { 21 | "clean-webpack-plugin": "^3.0.0", 22 | "copy-webpack-plugin": "^5.0.5", 23 | "css-loader": "^3.2.1", 24 | "file-loader": "^5.0.2", 25 | "mini-css-extract-plugin": "^0.8.0", 26 | "node-sass": "^4.13.0", 27 | "optimize-css-assets-webpack-plugin": "^5.0.3", 28 | "robotstxt-webpack-plugin": "^7.0.0", 29 | "sass": "^1.3.0", 30 | "sass-loader": "^8.0.0", 31 | "style-loader": "^1.0.1", 32 | "terser-webpack-plugin": "^2.2.1", 33 | "webpack": "^4.41.2", 34 | "webpack-cli": "^3.3.10", 35 | "webpack-pwa-manifest": "^4.2.0" 36 | }, 37 | "scripts": { 38 | "build": "webpack" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /sagan-client/src/app/admin.js: -------------------------------------------------------------------------------- 1 | import 'bulma/css/bulma.css' 2 | import '@fortawesome/fontawesome-free/css/all.css' 3 | import $ from 'jquery'; 4 | import 'jquery-datetimepicker/jquery.datetimepicker'; 5 | import 'jquery-datetimepicker/jquery.datetimepicker.css'; 6 | import {DateTime} from 'luxon'; 7 | import '../css/admin.css' 8 | 9 | $.datetimepicker.setLocale('en'); 10 | 11 | $.datetimepicker.setDateFormatter({ 12 | parseDate: function (date, format) { 13 | var d = DateTime.fromFormat(date, format); 14 | return d.isValid ? d.toJSDate() : false; 15 | }, 16 | formatDate: function (date, format) { 17 | return DateTime.fromJSDate(date).toFormat(format); 18 | }, 19 | }); 20 | 21 | if ($('#datetimepicker').length > 0) { 22 | $('#datetimepicker').datetimepicker({ 23 | format: 'y-MM-dd HH:mm', 24 | formatDate: 'y-MM-dd', 25 | formatTime: 'HH:mm', 26 | closeOnDateSelect: true, 27 | scrollMonth: false, 28 | defaultTime: '12:00' 29 | }); 30 | } -------------------------------------------------------------------------------- /sagan-client/src/app/blog.js: -------------------------------------------------------------------------------- 1 | import '../css/blog.css' -------------------------------------------------------------------------------- /sagan-client/src/app/guide.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | import * as JsSearch from 'js-search'; 3 | import '../css/guide.css' 4 | 5 | $(document).ready(function () { 6 | var guides = []; 7 | var _guides = $('div.guide-search'); 8 | var noResult = $('#guide-search-no-result'); 9 | if (_guides.length > 0) { 10 | _guides.each(function (index) { 11 | guides.push({ 12 | index: index, 13 | obj: $(this), 14 | str: $(this).attr('data-filterable') 15 | }) 16 | }); 17 | var search = new JsSearch.Search('index'); 18 | search.addIndex('str'); 19 | search.addDocuments(guides); 20 | $('#guide-search').keyup(function () { 21 | noResult.removeClass('show'); 22 | if ($(this).val().trim() === '') { 23 | _guides.removeClass('hide'); 24 | } else { 25 | var result = search.search($(this).val()); 26 | _guides.addClass('hide'); 27 | $.each(result, function (index, value) { 28 | value.obj.removeClass('hide'); 29 | 30 | }); 31 | if (result.length === 0) { 32 | noResult.addClass('show'); 33 | } 34 | } 35 | }) 36 | } 37 | }); -------------------------------------------------------------------------------- /sagan-client/src/app/theme.js: -------------------------------------------------------------------------------- 1 | import {get, set} from 'local-storage'; 2 | 3 | if (!get('theme')) { 4 | const isDark = 5 | window.matchMedia && 6 | window.matchMedia('(prefers-color-scheme: dark)').matches; 7 | set('theme', isDark ? 'dark' : 'light'); 8 | } 9 | 10 | document.body.className = `${document.body.className} ${get('theme')}`; -------------------------------------------------------------------------------- /sagan-client/src/fonts/generator_config.txt: -------------------------------------------------------------------------------- 1 | # Font Squirrel Font-face Generator Configuration File 2 | # Upload this file to the generator to recreate the settings 3 | # you used to create these fonts. 4 | 5 | {"mode":"optimal","formats":["woff","woff2"],"tt_instructor":"default","fix_gasp":"xy","fix_vertical_metrics":"Y","metrics_ascent":"","metrics_descent":"","metrics_linegap":"","add_spaces":"Y","add_hyphens":"Y","fallback":"none","fallback_custom":"100","options_subset":"basic","subset_custom":"","subset_custom_range":"","subset_ot_features_list":"","css_stylesheet":"stylesheet.css","filename_suffix":"-webfont","emsquare":"2048","spacing_adjustment":"0"} -------------------------------------------------------------------------------- /sagan-client/src/fonts/metropolis-bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/fonts/metropolis-bold-webfont.woff -------------------------------------------------------------------------------- /sagan-client/src/fonts/metropolis-bold-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/fonts/metropolis-bold-webfont.woff2 -------------------------------------------------------------------------------- /sagan-client/src/fonts/metropolis-extrabold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/fonts/metropolis-extrabold-webfont.woff -------------------------------------------------------------------------------- /sagan-client/src/fonts/metropolis-extrabold-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/fonts/metropolis-extrabold-webfont.woff2 -------------------------------------------------------------------------------- /sagan-client/src/fonts/metropolis-regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/fonts/metropolis-regular-webfont.woff -------------------------------------------------------------------------------- /sagan-client/src/fonts/metropolis-regular-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/fonts/metropolis-regular-webfont.woff2 -------------------------------------------------------------------------------- /sagan-client/src/fonts/open-sans-v17-latin/open-sans-v17-latin-600.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/fonts/open-sans-v17-latin/open-sans-v17-latin-600.eot -------------------------------------------------------------------------------- /sagan-client/src/fonts/open-sans-v17-latin/open-sans-v17-latin-600.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/fonts/open-sans-v17-latin/open-sans-v17-latin-600.ttf -------------------------------------------------------------------------------- /sagan-client/src/fonts/open-sans-v17-latin/open-sans-v17-latin-600.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/fonts/open-sans-v17-latin/open-sans-v17-latin-600.woff -------------------------------------------------------------------------------- /sagan-client/src/fonts/open-sans-v17-latin/open-sans-v17-latin-600.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/fonts/open-sans-v17-latin/open-sans-v17-latin-600.woff2 -------------------------------------------------------------------------------- /sagan-client/src/fonts/open-sans-v17-latin/open-sans-v17-latin-700.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/fonts/open-sans-v17-latin/open-sans-v17-latin-700.eot -------------------------------------------------------------------------------- /sagan-client/src/fonts/open-sans-v17-latin/open-sans-v17-latin-700.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/fonts/open-sans-v17-latin/open-sans-v17-latin-700.ttf -------------------------------------------------------------------------------- /sagan-client/src/fonts/open-sans-v17-latin/open-sans-v17-latin-700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/fonts/open-sans-v17-latin/open-sans-v17-latin-700.woff -------------------------------------------------------------------------------- /sagan-client/src/fonts/open-sans-v17-latin/open-sans-v17-latin-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/fonts/open-sans-v17-latin/open-sans-v17-latin-700.woff2 -------------------------------------------------------------------------------- /sagan-client/src/fonts/open-sans-v17-latin/open-sans-v17-latin-italic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/fonts/open-sans-v17-latin/open-sans-v17-latin-italic.eot -------------------------------------------------------------------------------- /sagan-client/src/fonts/open-sans-v17-latin/open-sans-v17-latin-italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/fonts/open-sans-v17-latin/open-sans-v17-latin-italic.ttf -------------------------------------------------------------------------------- /sagan-client/src/fonts/open-sans-v17-latin/open-sans-v17-latin-italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/fonts/open-sans-v17-latin/open-sans-v17-latin-italic.woff -------------------------------------------------------------------------------- /sagan-client/src/fonts/open-sans-v17-latin/open-sans-v17-latin-italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/fonts/open-sans-v17-latin/open-sans-v17-latin-italic.woff2 -------------------------------------------------------------------------------- /sagan-client/src/fonts/open-sans-v17-latin/open-sans-v17-latin-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/fonts/open-sans-v17-latin/open-sans-v17-latin-regular.eot -------------------------------------------------------------------------------- /sagan-client/src/fonts/open-sans-v17-latin/open-sans-v17-latin-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/fonts/open-sans-v17-latin/open-sans-v17-latin-regular.ttf -------------------------------------------------------------------------------- /sagan-client/src/fonts/open-sans-v17-latin/open-sans-v17-latin-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/fonts/open-sans-v17-latin/open-sans-v17-latin-regular.woff -------------------------------------------------------------------------------- /sagan-client/src/fonts/open-sans-v17-latin/open-sans-v17-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/fonts/open-sans-v17-latin/open-sans-v17-latin-regular.woff2 -------------------------------------------------------------------------------- /sagan-client/src/fonts/work-sans-v5-latin/work-sans-v5-latin-700.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/fonts/work-sans-v5-latin/work-sans-v5-latin-700.eot -------------------------------------------------------------------------------- /sagan-client/src/fonts/work-sans-v5-latin/work-sans-v5-latin-700.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/fonts/work-sans-v5-latin/work-sans-v5-latin-700.ttf -------------------------------------------------------------------------------- /sagan-client/src/fonts/work-sans-v5-latin/work-sans-v5-latin-700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/fonts/work-sans-v5-latin/work-sans-v5-latin-700.woff -------------------------------------------------------------------------------- /sagan-client/src/fonts/work-sans-v5-latin/work-sans-v5-latin-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/fonts/work-sans-v5-latin/work-sans-v5-latin-700.woff2 -------------------------------------------------------------------------------- /sagan-client/src/fonts/work-sans-v5-latin/work-sans-v5-latin-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/fonts/work-sans-v5-latin/work-sans-v5-latin-regular.eot -------------------------------------------------------------------------------- /sagan-client/src/fonts/work-sans-v5-latin/work-sans-v5-latin-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/fonts/work-sans-v5-latin/work-sans-v5-latin-regular.ttf -------------------------------------------------------------------------------- /sagan-client/src/fonts/work-sans-v5-latin/work-sans-v5-latin-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/fonts/work-sans-v5-latin/work-sans-v5-latin-regular.woff -------------------------------------------------------------------------------- /sagan-client/src/fonts/work-sans-v5-latin/work-sans-v5-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/fonts/work-sans-v5-latin/work-sans-v5-latin-regular.woff2 -------------------------------------------------------------------------------- /sagan-client/src/images/OG-Spring.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/OG-Spring.png -------------------------------------------------------------------------------- /sagan-client/src/images/SpringCloudDataFlow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/SpringCloudDataFlow.png -------------------------------------------------------------------------------- /sagan-client/src/images/angle.svg: -------------------------------------------------------------------------------- 1 | angle -------------------------------------------------------------------------------- /sagan-client/src/images/batch-vod1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/batch-vod1.jpg -------------------------------------------------------------------------------- /sagan-client/src/images/batch-vod2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/batch-vod2.jpg -------------------------------------------------------------------------------- /sagan-client/src/images/batch-vod3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/batch-vod3.jpg -------------------------------------------------------------------------------- /sagan-client/src/images/bg-featured-post-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /sagan-client/src/images/bg-featured-post.svg: -------------------------------------------------------------------------------- 1 | Vector Smart Object411 -------------------------------------------------------------------------------- /sagan-client/src/images/big-check.svg: -------------------------------------------------------------------------------- 1 | big-check -------------------------------------------------------------------------------- /sagan-client/src/images/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | close 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /sagan-client/src/images/cloud-res1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/cloud-res1.jpg -------------------------------------------------------------------------------- /sagan-client/src/images/cloud-res3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/cloud-res3.jpg -------------------------------------------------------------------------------- /sagan-client/src/images/dataflow-stream-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/dataflow-stream-details.png -------------------------------------------------------------------------------- /sagan-client/src/images/diagram-microservices1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/diagram-microservices1.png -------------------------------------------------------------------------------- /sagan-client/src/images/eclipse-shot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/eclipse-shot.jpg -------------------------------------------------------------------------------- /sagan-client/src/images/external.svg: -------------------------------------------------------------------------------- 1 | external -------------------------------------------------------------------------------- /sagan-client/src/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/favicon.ico -------------------------------------------------------------------------------- /sagan-client/src/images/footer-circles.svg: -------------------------------------------------------------------------------- 1 | footer-circles -------------------------------------------------------------------------------- /sagan-client/src/images/icon-github.svg: -------------------------------------------------------------------------------- 1 | icon-github -------------------------------------------------------------------------------- /sagan-client/src/images/icon-marker-000.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sagan-client/src/images/icon-spring-boot.svg: -------------------------------------------------------------------------------- 1 | icon-spring-boot -------------------------------------------------------------------------------- /sagan-client/src/images/icon-spring-cloud.svg: -------------------------------------------------------------------------------- 1 | icon-spring-cloud -------------------------------------------------------------------------------- /sagan-client/src/images/icon-spring.svg: -------------------------------------------------------------------------------- 1 | spring-icon -------------------------------------------------------------------------------- /sagan-client/src/images/icon-twitter.svg: -------------------------------------------------------------------------------- 1 | icon-twitter -------------------------------------------------------------------------------- /sagan-client/src/images/logo-cassandra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/logo-cassandra.png -------------------------------------------------------------------------------- /sagan-client/src/images/logo-cloud-spanner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/logo-cloud-spanner.png -------------------------------------------------------------------------------- /sagan-client/src/images/logo-cosmos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/logo-cosmos.png -------------------------------------------------------------------------------- /sagan-client/src/images/logo-geode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/logo-geode.png -------------------------------------------------------------------------------- /sagan-client/src/images/logo-jfrog-artifactory-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/logo-jfrog-artifactory-white.png -------------------------------------------------------------------------------- /sagan-client/src/images/logo-jfrog-artifactory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/logo-jfrog-artifactory.png -------------------------------------------------------------------------------- /sagan-client/src/images/logo-jfrog-bintray-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/logo-jfrog-bintray-white.png -------------------------------------------------------------------------------- /sagan-client/src/images/logo-jfrog-bintray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/logo-jfrog-bintray.png -------------------------------------------------------------------------------- /sagan-client/src/images/logo-mongodb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/logo-mongodb.png -------------------------------------------------------------------------------- /sagan-client/src/images/logo-neo4j.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/logo-neo4j.png -------------------------------------------------------------------------------- /sagan-client/src/images/logo-redis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/logo-redis.png -------------------------------------------------------------------------------- /sagan-client/src/images/logo-solr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/logo-solr.png -------------------------------------------------------------------------------- /sagan-client/src/images/logo-stackoverflow.svg: -------------------------------------------------------------------------------- 1 | 2 | icon-stackoverflow 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /sagan-client/src/images/logo-structure101-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/logo-structure101-white.png -------------------------------------------------------------------------------- /sagan-client/src/images/logo-structure101.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/logo-structure101.png -------------------------------------------------------------------------------- /sagan-client/src/images/logo-yourkit-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/logo-yourkit-black.png -------------------------------------------------------------------------------- /sagan-client/src/images/logo-yourkit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/logo-yourkit.png -------------------------------------------------------------------------------- /sagan-client/src/images/micro-res1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/micro-res1.png -------------------------------------------------------------------------------- /sagan-client/src/images/micro-res2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/micro-res2.png -------------------------------------------------------------------------------- /sagan-client/src/images/micro-res3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/micro-res3.png -------------------------------------------------------------------------------- /sagan-client/src/images/pacman2.svg: -------------------------------------------------------------------------------- 1 | 2 | pacman2 3 | 4 | 5 | 6 | 7 | 9 | 11 | 13 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /sagan-client/src/images/podcast.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/podcast.jpg -------------------------------------------------------------------------------- /sagan-client/src/images/podcast1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/podcast1.png -------------------------------------------------------------------------------- /sagan-client/src/images/podcast2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/podcast2.png -------------------------------------------------------------------------------- /sagan-client/src/images/podcast3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/podcast3.png -------------------------------------------------------------------------------- /sagan-client/src/images/pop-quiz.svg: -------------------------------------------------------------------------------- 1 | pop-quiz -------------------------------------------------------------------------------- /sagan-client/src/images/projects/logo-io-platform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/projects/logo-io-platform.png -------------------------------------------------------------------------------- /sagan-client/src/images/projects/logo-session.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/projects/logo-session.png -------------------------------------------------------------------------------- /sagan-client/src/images/projects/logo-web-services.svg: -------------------------------------------------------------------------------- 1 | logo-web-services -------------------------------------------------------------------------------- /sagan-client/src/images/projects/spring-amqp.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | logo-batch 9 | 10 | 12 | 14 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /sagan-client/src/images/projects/spring-android.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | logo-batch 9 | 10 | 11 | 12 | 13 | 14 | 15 | 20 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /sagan-client/src/images/projects/spring-batch.svg: -------------------------------------------------------------------------------- 1 | logo-batch -------------------------------------------------------------------------------- /sagan-client/src/images/projects/spring-boot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | icon-spring-boot 9 | 10 | 11 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /sagan-client/src/images/projects/spring-cloud.svg: -------------------------------------------------------------------------------- 1 | icon-spring-cloud -------------------------------------------------------------------------------- /sagan-client/src/images/projects/spring-data.svg: -------------------------------------------------------------------------------- 1 | logo-data -------------------------------------------------------------------------------- /sagan-client/src/images/projects/spring-flo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 12 | 14 | 15 | -------------------------------------------------------------------------------- /sagan-client/src/images/projects/spring-hateoas.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | logo-batch 9 | 10 | 11 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /sagan-client/src/images/projects/spring-integration.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | logo-integration 9 | 11 | 13 | 14 | -------------------------------------------------------------------------------- /sagan-client/src/images/projects/spring-mobile.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | logo-batch 9 | 10 | 13 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /sagan-client/src/images/projects/spring-restdocs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/projects/spring-restdocs.png -------------------------------------------------------------------------------- /sagan-client/src/images/projects/spring-security.svg: -------------------------------------------------------------------------------- 1 | logo-security -------------------------------------------------------------------------------- /sagan-client/src/images/projects/spring-shell.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /sagan-client/src/images/projects/spring-web-flo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | logo-batch 9 | 13 | 14 | -------------------------------------------------------------------------------- /sagan-client/src/images/projects/spring-xd.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | logo-batch 9 | 11 | 12 | -------------------------------------------------------------------------------- /sagan-client/src/images/projects/spring.svg: -------------------------------------------------------------------------------- 1 | spring-icon -------------------------------------------------------------------------------- /sagan-client/src/images/quick-img-1-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/quick-img-1-dark.png -------------------------------------------------------------------------------- /sagan-client/src/images/quick-img-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/quick-img-1.png -------------------------------------------------------------------------------- /sagan-client/src/images/quick-img2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/quick-img2.png -------------------------------------------------------------------------------- /sagan-client/src/images/quick-img3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/quick-img3.png -------------------------------------------------------------------------------- /sagan-client/src/images/quote.svg: -------------------------------------------------------------------------------- 1 | quote -------------------------------------------------------------------------------- /sagan-client/src/images/reactive-res1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/reactive-res1.png -------------------------------------------------------------------------------- /sagan-client/src/images/reactive-res2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/reactive-res2.png -------------------------------------------------------------------------------- /sagan-client/src/images/reactive-res3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/reactive-res3.png -------------------------------------------------------------------------------- /sagan-client/src/images/round-sharp.svg: -------------------------------------------------------------------------------- 1 | round-sharp -------------------------------------------------------------------------------- /sagan-client/src/images/serverless-res1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/serverless-res1.png -------------------------------------------------------------------------------- /sagan-client/src/images/serverless-res2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/serverless-res2.png -------------------------------------------------------------------------------- /sagan-client/src/images/serverless-res3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/serverless-res3.png -------------------------------------------------------------------------------- /sagan-client/src/images/spring-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/spring-logo.png -------------------------------------------------------------------------------- /sagan-client/src/images/streams-res1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/streams-res1.png -------------------------------------------------------------------------------- /sagan-client/src/images/streams-res2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/streams-res2.png -------------------------------------------------------------------------------- /sagan-client/src/images/streams-res3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/streams-res3.png -------------------------------------------------------------------------------- /sagan-client/src/images/theia-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | Theia-Logo 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /sagan-client/src/images/theia-shot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/theia-shot.jpg -------------------------------------------------------------------------------- /sagan-client/src/images/tools-img1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/tools-img1.jpg -------------------------------------------------------------------------------- /sagan-client/src/images/tools-img2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/tools-img2.jpg -------------------------------------------------------------------------------- /sagan-client/src/images/tools-img3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/tools-img3.jpg -------------------------------------------------------------------------------- /sagan-client/src/images/tools-img4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/tools-img4.jpg -------------------------------------------------------------------------------- /sagan-client/src/images/tools-left-tri.svg: -------------------------------------------------------------------------------- 1 | tool-left-tri -------------------------------------------------------------------------------- /sagan-client/src/images/tools-right-geo.svg: -------------------------------------------------------------------------------- 1 | tools-right-geo -------------------------------------------------------------------------------- /sagan-client/src/images/tutorial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/tutorial.png -------------------------------------------------------------------------------- /sagan-client/src/images/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | twitter 9 | 10 | 11 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /sagan-client/src/images/vid1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/vid1.png -------------------------------------------------------------------------------- /sagan-client/src/images/vid2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/vid2.png -------------------------------------------------------------------------------- /sagan-client/src/images/vid3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/vid3.png -------------------------------------------------------------------------------- /sagan-client/src/images/video-gursel.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/video-gursel.jpg -------------------------------------------------------------------------------- /sagan-client/src/images/video-jackson.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/video-jackson.jpg -------------------------------------------------------------------------------- /sagan-client/src/images/video-tracing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/video-tracing.png -------------------------------------------------------------------------------- /sagan-client/src/images/vs-shot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/vs-shot.jpg -------------------------------------------------------------------------------- /sagan-client/src/images/webapps-video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/webapps-video.png -------------------------------------------------------------------------------- /sagan-client/src/images/wicksell.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-client/src/images/wicksell.jpg -------------------------------------------------------------------------------- /sagan-client/src/images/youtube.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | youtube 9 | 10 | 11 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /sagan-renderer/.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | 5 | ### STS ### 6 | .apt_generated 7 | .classpath 8 | .factorypath 9 | .project 10 | .settings 11 | .springBeans 12 | 13 | ### IntelliJ IDEA ### 14 | .idea 15 | *.iws 16 | *.iml 17 | *.ipr 18 | 19 | ### NetBeans ### 20 | nbproject/private/ 21 | build/ 22 | nbbuild/ 23 | dist/ 24 | nbdist/ 25 | .nb-gradle/ -------------------------------------------------------------------------------- /sagan-renderer/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.springframework.boot' version '2.3.4.RELEASE' 3 | id 'io.spring.dependency-management' version '1.0.10.RELEASE' 4 | id 'java' 5 | } 6 | 7 | group = 'io.spring.sagan' 8 | version = "1.0.0.BUILD-SNAPSHOT" 9 | sourceCompatibility = 1.8 10 | 11 | repositories { 12 | mavenCentral() 13 | } 14 | 15 | bootJar { 16 | requiresUnpack '**/jruby-complete-*.jar' 17 | requiresUnpack '**/asciidoctorj-*.jar' 18 | } 19 | 20 | dependencies { 21 | implementation 'org.pegdown:pegdown:1.6.0' 22 | implementation('org.asciidoctor:asciidoctorj:2.4.1') { 23 | exclude group: 'org.jruby' 24 | } 25 | implementation 'org.jruby:jruby-complete:9.2.13.0' 26 | implementation 'org.jsoup:jsoup:1.13.1' 27 | implementation 'org.springframework.boot:spring-boot-starter-web' 28 | implementation 'org.springframework.boot:spring-boot-starter-validation' 29 | implementation 'org.springframework.boot:spring-boot-starter-hateoas' 30 | compileOnly 'org.springframework.boot:spring-boot-configuration-processor' 31 | runtimeOnly 'org.springframework.boot:spring-boot-starter-actuator' 32 | developmentOnly 'org.springframework.boot:spring-boot-devtools' 33 | testImplementation('org.springframework.boot:spring-boot-starter-test') { 34 | exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' 35 | } 36 | } 37 | 38 | test { 39 | useJUnitPlatform() 40 | } 41 | 42 | -------------------------------------------------------------------------------- /sagan-renderer/src/main/java/sagan/renderer/AsciidoctorConfig.java: -------------------------------------------------------------------------------- 1 | package sagan.renderer; 2 | 3 | import org.asciidoctor.Asciidoctor; 4 | 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | /** 9 | * Create the Asciidoctor engine 10 | */ 11 | @Configuration 12 | public class AsciidoctorConfig { 13 | 14 | @Bean 15 | public Asciidoctor asciidoctor() { 16 | return Asciidoctor.Factory.create(); 17 | } 18 | } -------------------------------------------------------------------------------- /sagan-renderer/src/main/java/sagan/renderer/IndexController.java: -------------------------------------------------------------------------------- 1 | package sagan.renderer; 2 | 3 | import sagan.renderer.guides.GuidesController; 4 | import sagan.renderer.markup.MarkupController; 5 | 6 | import org.springframework.hateoas.MediaTypes; 7 | import org.springframework.hateoas.RepresentationModel; 8 | import org.springframework.http.MediaType; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; 13 | import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; 14 | 15 | 16 | /** 17 | * Lists all resources at the root of the application 18 | */ 19 | @RestController 20 | public class IndexController { 21 | 22 | @GetMapping(path = "/", produces = MediaTypes.HAL_JSON_VALUE) 23 | public RepresentationModel index() { 24 | RepresentationModel resource = new RepresentationModel(); 25 | resource.add(linkTo(methodOn(MarkupController.class).renderMarkup(MediaType.TEXT_MARKDOWN, "")) 26 | .withRel("markup")); 27 | resource.add(linkTo(methodOn(GuidesController.class).listGuides()).withRel("guides")); 28 | return resource; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /sagan-renderer/src/main/java/sagan/renderer/RendererApplication.java: -------------------------------------------------------------------------------- 1 | package sagan.renderer; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 6 | 7 | /** 8 | * Application that renders lightweight markup languages 9 | * and Spring guides content into HTML 10 | */ 11 | @SpringBootApplication 12 | @EnableConfigurationProperties(RendererProperties.class) 13 | public class RendererApplication { 14 | 15 | public static void main(String[] args) { 16 | SpringApplication.run(RendererApplication.class, args); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /sagan-renderer/src/main/java/sagan/renderer/RendererProperties.java: -------------------------------------------------------------------------------- 1 | package sagan.renderer; 2 | 3 | import javax.validation.constraints.Pattern; 4 | 5 | import org.springframework.boot.context.properties.ConfigurationProperties; 6 | import org.springframework.validation.annotation.Validated; 7 | 8 | /** 9 | * Configuration properties for the Sagan Renderer application 10 | */ 11 | @ConfigurationProperties("sagan.renderer") 12 | @Validated 13 | public class RendererProperties { 14 | 15 | private final Github github = new Github(); 16 | 17 | private final Guides guides = new Guides(); 18 | 19 | public Github getGithub() { 20 | return this.github; 21 | } 22 | 23 | public Guides getGuides() { 24 | return this.guides; 25 | } 26 | 27 | public static class Github { 28 | 29 | /** 30 | * Access token to query public github endpoints. 31 | * https://developer.github.com/v3/auth/#authenticating-for-saml-sso 32 | */ 33 | @Pattern(regexp = "([0-9a-z]*)?") 34 | private String token; 35 | 36 | public String getToken() { 37 | return this.token; 38 | } 39 | 40 | public void setToken(String token) { 41 | this.token = token; 42 | } 43 | 44 | } 45 | 46 | public static class Guides { 47 | 48 | /** 49 | * Name of the Github organization to fetch guides from. 50 | */ 51 | private String organization = "spring-guides"; 52 | 53 | public String getOrganization() { 54 | return this.organization; 55 | } 56 | 57 | public void setOrganization(String organization) { 58 | this.organization = organization; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /sagan-renderer/src/main/java/sagan/renderer/github/GithubResourceNotFoundException.java: -------------------------------------------------------------------------------- 1 | package sagan.renderer.github; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.NOT_FOUND) 7 | public class GithubResourceNotFoundException extends RuntimeException { 8 | 9 | private final String resourceName; 10 | 11 | public GithubResourceNotFoundException(String orgName, String repositoryName, Throwable cause) { 12 | super("Could not find github repository [" + orgName + "/" + repositoryName + "]", cause); 13 | this.resourceName= "Repository [" + orgName + "/" + repositoryName + "]"; 14 | } 15 | 16 | public GithubResourceNotFoundException(String orgName, Throwable cause) { 17 | super("Could not find github organization [" + orgName + "]", cause); 18 | this.resourceName = "Organization [" + orgName + "]"; 19 | } 20 | 21 | public String getResourceName() { 22 | return this.resourceName; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /sagan-renderer/src/main/java/sagan/renderer/guides/GuideImage.java: -------------------------------------------------------------------------------- 1 | package sagan.renderer.guides; 2 | 3 | public class GuideImage { 4 | 5 | private String name; 6 | 7 | private String encodedContent; 8 | 9 | public GuideImage() { 10 | } 11 | 12 | public GuideImage(String name, String encodedContent) { 13 | this.name = name; 14 | this.encodedContent = encodedContent; 15 | } 16 | 17 | public String getName() { 18 | return name; 19 | } 20 | 21 | public void setName(String name) { 22 | this.name = name; 23 | } 24 | 25 | public String getEncodedContent() { 26 | return encodedContent; 27 | } 28 | 29 | public void setEncodedContent(String encodedContent) { 30 | this.encodedContent = encodedContent; 31 | } 32 | 33 | 34 | } 35 | -------------------------------------------------------------------------------- /sagan-renderer/src/main/java/sagan/renderer/guides/GuideModelAssembler.java: -------------------------------------------------------------------------------- 1 | package sagan.renderer.guides; 2 | 3 | import sagan.renderer.github.Repository; 4 | 5 | 6 | import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport; 7 | 8 | import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn; 9 | import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo; 10 | 11 | class GuideModelAssembler extends RepresentationModelAssemblerSupport { 12 | 13 | GuideModelAssembler() { 14 | super(GuidesController.class, GuideModel.class); 15 | } 16 | 17 | @Override 18 | public GuideModel toModel(Repository repository) { 19 | GuideModel resource = new GuideModel(repository); 20 | resource.add(linkTo(methodOn(GuidesController.class) 21 | .showGuide(resource.getType().getSlug(), resource.getName())).withSelfRel()); 22 | resource.add(linkTo(methodOn(GuidesController.class) 23 | .renderGuide(resource.getType().getSlug(), resource.getName())).withRel("content")); 24 | resource.add(linkTo(methodOn(GuidesController.class).listGuides()).withRel("guides")); 25 | return resource; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /sagan-renderer/src/main/java/sagan/renderer/guides/GuideRenderingException.java: -------------------------------------------------------------------------------- 1 | package sagan.renderer.guides; 2 | 3 | public class GuideRenderingException extends RuntimeException { 4 | 5 | private final String repositoryName; 6 | 7 | public GuideRenderingException(String repositoryName, Throwable cause) { 8 | super("Could not render guide [" + repositoryName + "]", cause); 9 | this.repositoryName = repositoryName; 10 | } 11 | 12 | public String getRepositoryName() { 13 | return repositoryName; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /sagan-renderer/src/main/java/sagan/renderer/guides/GuideType.java: -------------------------------------------------------------------------------- 1 | package sagan.renderer.guides; 2 | 3 | import java.util.Arrays; 4 | 5 | import com.fasterxml.jackson.annotation.JsonValue; 6 | 7 | /** 8 | * Guide Types 9 | */ 10 | enum GuideType { 11 | 12 | GETTING_STARTED("getting-started", "gs-"), TUTORIAL("tutorial", "tut-"), 13 | TOPICAL("topical", "top-"), UNKNOWN("unknown", ""); 14 | 15 | private final String slug; 16 | private final String prefix; 17 | 18 | 19 | GuideType(String slug, String prefix) { 20 | this.slug = slug; 21 | this.prefix = prefix; 22 | } 23 | 24 | public static GuideType fromSlug(String slug) { 25 | return Arrays.stream(GuideType.values()) 26 | .filter(type -> type.getSlug().equals(slug)) 27 | .findFirst().orElse(GuideType.UNKNOWN); 28 | } 29 | 30 | public static GuideType fromRepositoryName(String repositoryName) { 31 | return Arrays.stream(GuideType.values()) 32 | .filter(type -> repositoryName.startsWith(type.getPrefix())) 33 | .findFirst().orElse(GuideType.UNKNOWN); 34 | } 35 | 36 | public String stripPrefix(String repositoryName) { 37 | return repositoryName.replaceFirst(this.prefix, ""); 38 | } 39 | 40 | @JsonValue 41 | public String getSlug() { 42 | return this.slug; 43 | } 44 | 45 | public String getPrefix() { 46 | return this.prefix; 47 | } 48 | 49 | @Override 50 | public String toString() { 51 | return this.slug; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /sagan-renderer/src/main/java/sagan/renderer/guides/content/GuideContentContributor.java: -------------------------------------------------------------------------------- 1 | package sagan.renderer.guides.content; 2 | 3 | import java.io.File; 4 | 5 | import sagan.renderer.guides.GuideContentModel; 6 | 7 | /** 8 | * Contribute information to the Guide content. 9 | */ 10 | public interface GuideContentContributor { 11 | 12 | /** 13 | * Contribute to the guide content by extracting information from the guide repository. 14 | * @param guideContent the guide content to contribute to 15 | * @param repositoryRoot the unzipped repository root folder 16 | */ 17 | void contribute(GuideContentModel guideContent, File repositoryRoot); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /sagan-renderer/src/main/java/sagan/renderer/guides/content/PwsGuideContentContributor.java: -------------------------------------------------------------------------------- 1 | package sagan.renderer.guides.content; 2 | 3 | import java.io.File; 4 | import java.io.FileReader; 5 | import java.io.IOException; 6 | 7 | import sagan.renderer.guides.GuideContentModel; 8 | import sagan.renderer.guides.GuideRenderingException; 9 | 10 | import org.springframework.stereotype.Component; 11 | import org.springframework.util.FileCopyUtils; 12 | 13 | /** 14 | * Looks for a "push-to-pws/button.yml" and contribute its content to the guide 15 | */ 16 | @Component 17 | public class PwsGuideContentContributor implements GuideContentContributor { 18 | 19 | private static final String PWS_METADATA_FILENAME = "push-to-pws" + File.separator + "button.yml"; 20 | 21 | @Override 22 | public void contribute(GuideContentModel guideContent, File repositoryRoot) { 23 | try { 24 | File pushToPwsMetadataFile = new File( 25 | repositoryRoot.getAbsolutePath() + File.separator + PWS_METADATA_FILENAME); 26 | if (pushToPwsMetadataFile.exists()) { 27 | guideContent.setPushToPwsMetadata(FileCopyUtils.copyToString(new FileReader(pushToPwsMetadataFile))); 28 | } 29 | } 30 | catch (IOException e) { 31 | throw new GuideRenderingException(guideContent.getName(), e); 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /sagan-renderer/src/main/java/sagan/renderer/markup/AsciidoctorRenderer.java: -------------------------------------------------------------------------------- 1 | package sagan.renderer.markup; 2 | 3 | import org.asciidoctor.Asciidoctor; 4 | import org.asciidoctor.Attributes; 5 | import org.asciidoctor.OptionsBuilder; 6 | import org.asciidoctor.SafeMode; 7 | 8 | import org.springframework.http.MediaType; 9 | import org.springframework.stereotype.Component; 10 | 11 | /** 12 | * Convert asciidoc into HTML using the Asciidoctor library 13 | */ 14 | @Component 15 | class AsciidoctorRenderer implements MarkupRenderer { 16 | 17 | private static final MediaType TEXT_ASCIIDOC = MediaType.parseMediaType("text/asciidoc"); 18 | 19 | private final Asciidoctor asciidoctor; 20 | 21 | public AsciidoctorRenderer(Asciidoctor asciidoctor) { 22 | this.asciidoctor = asciidoctor; 23 | } 24 | 25 | @Override 26 | public boolean canRender(MediaType mediaType) { 27 | return TEXT_ASCIIDOC.isCompatibleWith(mediaType); 28 | } 29 | 30 | @Override 31 | public String renderToHtml(String markup) { 32 | Attributes attributes = new Attributes(); 33 | attributes.setAllowUriRead(true); 34 | attributes.setSkipFrontMatter(true); 35 | attributes.setAttribute("source-highlighter", "prettify"); 36 | attributes.setAttribute("idprefix", ""); 37 | attributes.setAttribute("idseparator", "-"); 38 | attributes.setAnchors(true); 39 | OptionsBuilder options = OptionsBuilder.options().safe(SafeMode.SAFE) 40 | .attributes(attributes); 41 | return asciidoctor.convert(markup, options); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /sagan-renderer/src/main/java/sagan/renderer/markup/MarkdownRenderer.java: -------------------------------------------------------------------------------- 1 | package sagan.renderer.markup; 2 | 3 | import java.util.Collections; 4 | 5 | import org.pegdown.Extensions; 6 | import org.pegdown.LinkRenderer; 7 | import org.pegdown.PegDownProcessor; 8 | import org.pegdown.VerbatimSerializer; 9 | import org.pegdown.ast.RootNode; 10 | 11 | import org.springframework.http.MediaType; 12 | import org.springframework.stereotype.Component; 13 | 14 | /** 15 | * Convert markdown into HTML using the Pegdown library 16 | */ 17 | @Component 18 | class MarkdownRenderer implements MarkupRenderer { 19 | 20 | private final PegDownProcessor pegdown; 21 | 22 | public MarkdownRenderer() { 23 | this.pegdown = new PegDownProcessor(Extensions.ALL ^ Extensions.ANCHORLINKS); 24 | } 25 | 26 | @Override 27 | public boolean canRender(MediaType mediaType) { 28 | return MediaType.TEXT_MARKDOWN.isCompatibleWith(mediaType); 29 | } 30 | 31 | @Override 32 | public String renderToHtml(String markup) { 33 | // synchronizing on pegdown instance since neither the processor nor the 34 | // underlying parser is thread-safe. 35 | synchronized (this.pegdown) { 36 | RootNode astRoot = this.pegdown.parseMarkdown(markup.toCharArray()); 37 | MarkdownToHtmlSerializer serializer = new MarkdownToHtmlSerializer( 38 | new LinkRenderer(), 39 | Collections.singletonMap(VerbatimSerializer.DEFAULT, 40 | PrettifyVerbatimSerializer.INSTANCE)); 41 | return serializer.toHtml(astRoot); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /sagan-renderer/src/main/java/sagan/renderer/markup/MarkupController.java: -------------------------------------------------------------------------------- 1 | package sagan.renderer.markup; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.http.MediaType; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.stereotype.Controller; 9 | import org.springframework.web.bind.annotation.PostMapping; 10 | import org.springframework.web.bind.annotation.RequestBody; 11 | import org.springframework.web.bind.annotation.RequestHeader; 12 | 13 | /** 14 | * Render lightweight markup into HTML 15 | */ 16 | @Controller 17 | public class MarkupController { 18 | 19 | private final List converters; 20 | 21 | 22 | public MarkupController(List converters) { 23 | this.converters = converters; 24 | } 25 | 26 | @PostMapping(path = "/documents", produces = "text/html") 27 | public ResponseEntity renderMarkup(@RequestHeader("Content-Type") MediaType contentType, 28 | @RequestBody String markup) { 29 | return converters.stream() 30 | .filter(converter -> converter.canRender(contentType)) 31 | .findFirst() 32 | .map(converter -> ResponseEntity.ok(converter.renderToHtml(markup) 33 | + "\n")) 34 | .orElse(ResponseEntity.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE).build()); 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /sagan-renderer/src/main/java/sagan/renderer/markup/MarkupRenderer.java: -------------------------------------------------------------------------------- 1 | package sagan.renderer.markup; 2 | 3 | import org.springframework.http.MediaType; 4 | 5 | /** 6 | * Convert lightweight markup format into HTML 7 | */ 8 | public interface MarkupRenderer { 9 | 10 | String renderToHtml(String markup); 11 | 12 | boolean canRender(MediaType mediaType); 13 | } 14 | -------------------------------------------------------------------------------- /sagan-renderer/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8081 3 | 4 | logging: 5 | level: 6 | ROOT: INFO 7 | org.apache.http: WARN 8 | # always log high-level information about application startup 9 | sagan.renderer: INFO 10 | 11 | sagan: 12 | renderer: 13 | guides: 14 | organization: spring-guides 15 | github: 16 | # This optional property may be assigned using a "personal access token" created 17 | # at https://github.com/settings/applications. It is used for accessing GitHub's 18 | # API, even for operations that do not require authorization (e.g. Getting Started 19 | # Guide repositories). This is done in order to work against higher rate limits 20 | # (5000req/hour vs. the default 60req/hour without a token). When running the app 21 | # locally, this lower default is fine. When running the app in production, the 22 | # token is a must. See http://developer.github.com/v3/#rate-limiting. 23 | token: ${GITHUB_ACCESS_TOKEN:} -------------------------------------------------------------------------------- /sagan-renderer/src/test/resources/sagan/renderer/github/gs-rest-service.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-renderer/src/test/resources/sagan/renderer/github/gs-rest-service.zip -------------------------------------------------------------------------------- /sagan-renderer/src/test/resources/sagan/renderer/guides/gs-sample-pws.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-renderer/src/test/resources/sagan/renderer/guides/gs-sample-pws.zip -------------------------------------------------------------------------------- /sagan-renderer/src/test/resources/sagan/renderer/guides/gs-sample.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-renderer/src/test/resources/sagan/renderer/guides/gs-sample.zip -------------------------------------------------------------------------------- /sagan-site/.settings/org.eclipse.jdt.ui.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | formatter_profile=_sagan 3 | formatter_settings_version=12 4 | org.eclipse.jdt.ui.ignorelowercasenames=true 5 | org.eclipse.jdt.ui.importorder=sagan;java;javax;org;org.springframework;io;com;\#; 6 | org.eclipse.jdt.ui.ondemandthreshold=99 7 | org.eclipse.jdt.ui.staticondemandthreshold=2 8 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/DatabaseConfig.java: -------------------------------------------------------------------------------- 1 | package sagan; 2 | 3 | import javax.sql.DataSource; 4 | 5 | import org.springframework.cloud.Cloud; 6 | import org.springframework.cloud.CloudFactory; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.context.annotation.Profile; 10 | 11 | @Configuration 12 | @Profile(SaganProfiles.CLOUDFOUNDRY) 13 | class CloudFoundryDatabaseConfig { 14 | 15 | @Bean 16 | public Cloud cloud() { 17 | return new CloudFactory().getCloud(); 18 | } 19 | 20 | @Bean 21 | public DataSource dataSource() { 22 | DataSource dataSource = cloud().getServiceConnector("sagan-db", DataSource.class, null); 23 | return dataSource; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/ModelMapperConfig.java: -------------------------------------------------------------------------------- 1 | package sagan; 2 | 3 | import org.modelmapper.ModelMapper; 4 | 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | /** 9 | * This configuration registers a {@link ModelMapper} bean with the application. 10 | * We're using using this library to map domain model classes to/from DTOs. 11 | */ 12 | @Configuration 13 | public class ModelMapperConfig { 14 | 15 | @Bean 16 | public ModelMapper modelMapper() { 17 | return new ModelMapper(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/ThymeleafViewResolverCustomizer.java: -------------------------------------------------------------------------------- 1 | package sagan; 2 | 3 | import org.thymeleaf.spring4.view.ThymeleafViewResolver; 4 | 5 | public class ThymeleafViewResolverCustomizer { 6 | 7 | public ThymeleafViewResolverCustomizer(ThymeleafViewResolver viewResolver, String applicationVersion) { 8 | 9 | viewResolver.addStaticVariable("saganAppVersion", applicationVersion); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/UrlRewriterFilterConfig.java: -------------------------------------------------------------------------------- 1 | package sagan; 2 | 3 | import sagan.support.TuckeyRewriteFilter; 4 | 5 | import org.springframework.boot.web.servlet.FilterRegistrationBean; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | @Configuration 10 | public class UrlRewriterFilterConfig { 11 | 12 | public static final String REWRITE_FILTER_NAME = "rewriteFilter"; 13 | public static final String REWRITE_FILTER_CONF_PATH = "urlrewrite.xml"; 14 | 15 | 16 | @Bean 17 | public FilterRegistrationBean rewriteFilterConfig() { 18 | FilterRegistrationBean reg = new FilterRegistrationBean(); 19 | reg.setName(REWRITE_FILTER_NAME); 20 | reg.setFilter(new TuckeyRewriteFilter()); 21 | reg.addInitParameter("confPath", REWRITE_FILTER_CONF_PATH); 22 | reg.addInitParameter("confReloadCheckInterval", "-1"); 23 | reg.addInitParameter("statusPath", "/redirect"); 24 | reg.addInitParameter("statusEnabledOnHosts", "*"); 25 | reg.addInitParameter("logLevel", "WARN"); 26 | return reg; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/admin/AdminController.java: -------------------------------------------------------------------------------- 1 | package sagan.site.admin; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | 6 | /** 7 | * Controller that handles requests for the root admin page. Administrative operations for 8 | * /blog, /team and /projects subsections are handled by their respective controllers. 9 | * 10 | * @see sagan.site.blog.admin.BlogAdminController 11 | * @see sagan.site.team.admin.TeamAdminController 12 | * @see sagan.site.projects.admin.ProjectAdminController 13 | */ 14 | @Controller 15 | class AdminController { 16 | 17 | @RequestMapping("/admin") 18 | public String adminPage() { 19 | return "admin/show"; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/blog/MarkdownService.java: -------------------------------------------------------------------------------- 1 | package sagan.site.blog; 2 | 3 | /** 4 | * Abstraction representing a service capable of translating markdown to HTML. 5 | */ 6 | interface MarkdownService { 7 | String renderToHtml(String markdownSource); 8 | } 9 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/blog/PostCategory.java: -------------------------------------------------------------------------------- 1 | package sagan.site.blog; 2 | 3 | /** 4 | * Available categories for blog posts. 5 | */ 6 | public enum PostCategory { 7 | 8 | ENGINEERING("Engineering", "engineering"), 9 | RELEASES("Releases", "releases"), 10 | NEWS_AND_EVENTS("News and Events", "news"); 11 | 12 | private String displayName; 13 | private String urlSlug; 14 | 15 | PostCategory(String displayName, String urlSlug) { 16 | this.displayName = displayName; 17 | this.urlSlug = urlSlug; 18 | } 19 | 20 | public String getDisplayName() { 21 | return displayName; 22 | } 23 | 24 | public String getUrlSlug() { 25 | return urlSlug; 26 | } 27 | 28 | public String getId() { 29 | return name(); 30 | } 31 | 32 | @Override 33 | public String toString() { 34 | return getDisplayName(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/blog/PostCategoryFormatter.java: -------------------------------------------------------------------------------- 1 | package sagan.site.blog; 2 | 3 | import java.text.ParseException; 4 | import java.util.HashMap; 5 | import java.util.Locale; 6 | import java.util.Map; 7 | 8 | import org.springframework.format.Formatter; 9 | import org.springframework.stereotype.Component; 10 | 11 | @Component 12 | class PostCategoryFormatter implements Formatter { 13 | private static Map mapping = new HashMap<>(); 14 | { 15 | for (PostCategory category : PostCategory.values()) { 16 | mapping.put(category.getUrlSlug(), category); 17 | mapping.put(category.name(), category); 18 | } 19 | } 20 | 21 | @Override 22 | public PostCategory parse(String text, Locale locale) throws ParseException { 23 | return mapping.get(text.trim()); 24 | } 25 | 26 | @Override 27 | public String print(PostCategory category, Locale locale) { 28 | return category.getUrlSlug(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/blog/PostFormat.java: -------------------------------------------------------------------------------- 1 | package sagan.site.blog; 2 | 3 | /** 4 | * Available formats for blog posts. 5 | */ 6 | public enum PostFormat { 7 | 8 | MARKDOWN("Markdown", "markdown"), ASCIIDOC("Asciidoc", "asciidoc"); 9 | 10 | private String displayName; 11 | private String slug; 12 | 13 | PostFormat(String displayName, String slug) { 14 | this.displayName = displayName; 15 | this.slug = slug; 16 | } 17 | 18 | public String getDisplayName() { 19 | return displayName; 20 | } 21 | 22 | public String getSlug() { 23 | return slug; 24 | } 25 | 26 | public String getId() { 27 | return name(); 28 | } 29 | 30 | @Override 31 | public String toString() { 32 | return getDisplayName(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/blog/PostMovedException.java: -------------------------------------------------------------------------------- 1 | package sagan.site.blog; 2 | 3 | /** 4 | * Exception thrown when requesting a blog post whose slug has changed. See 5 | * {@code BlogController} for handling logic. 6 | * 7 | * @see sagan.site.blog.support.BlogService#getPublishedPost(String) 8 | */ 9 | @SuppressWarnings("serial") 10 | public class PostMovedException extends RuntimeException { 11 | 12 | private String publicSlug; 13 | 14 | public PostMovedException(String publicSlug) { 15 | this.publicSlug = publicSlug; 16 | } 17 | 18 | public String getPublicSlug() { 19 | return publicSlug; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/blog/PostNotFoundException.java: -------------------------------------------------------------------------------- 1 | package sagan.site.blog; 2 | 3 | import sagan.support.ResourceNotFoundException; 4 | 5 | /** 6 | * Exception thrown when requesting a non existent (or no-longer existent) blog post. See 7 | * sagan-site's {@code MvcConfig#handleException} for handling logic. 8 | * 9 | * @see sagan.site.blog.support.BlogService#getPost(Long) 10 | * @see sagan.site.blog.support.BlogService#getPublishedPost(String) 11 | */ 12 | @SuppressWarnings("serial") 13 | public class PostNotFoundException extends ResourceNotFoundException { 14 | 15 | public PostNotFoundException(long id) { 16 | super("Could not find blog post with id " + id); 17 | } 18 | 19 | public PostNotFoundException(String slug) { 20 | super("Could not find blog post with slug '" + slug + "'"); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/blog/PostSummary.java: -------------------------------------------------------------------------------- 1 | package sagan.site.blog; 2 | 3 | import org.jsoup.Jsoup; 4 | import org.jsoup.nodes.Document; 5 | import org.jsoup.nodes.Element; 6 | 7 | import org.springframework.stereotype.Service; 8 | 9 | @Service 10 | class PostSummary { 11 | 12 | public String forContent(String content, int maxLength) { 13 | Document document = Jsoup.parse(content); 14 | StringBuilder builder = new StringBuilder(); 15 | int count = 0; 16 | for (Element element : document.body().children()) { 17 | builder.append(element.outerHtml()); 18 | builder.append("\n"); 19 | count += element.text().length(); 20 | if (count >= maxLength) 21 | break; 22 | } 23 | 24 | return builder.toString(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/blog/support/SiteUrl.java: -------------------------------------------------------------------------------- 1 | package sagan.site.blog.support; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | 5 | import org.springframework.stereotype.Component; 6 | import org.springframework.web.context.request.RequestContextHolder; 7 | import org.springframework.web.context.request.ServletRequestAttributes; 8 | 9 | @Component 10 | class SiteUrl { 11 | 12 | public String getUrl() { 13 | HttpServletRequest request = 14 | ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); 15 | String requestURL = request.getRequestURL().toString(); 16 | String requestURI = request.getRequestURI(); 17 | return requestURL.replace(requestURI, ""); 18 | } 19 | 20 | public String getAbsoluteUrl(String path) { 21 | return getUrl() + path; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/events/EventsController.java: -------------------------------------------------------------------------------- 1 | package sagan.site.events; 2 | 3 | import java.time.LocalDate; 4 | import java.util.List; 5 | 6 | import org.springframework.stereotype.Controller; 7 | import org.springframework.ui.Model; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | 10 | @Controller 11 | public class EventsController { 12 | 13 | private static final int NEXT_SIX_MONTHS = 180; 14 | 15 | private final EventsCalendarService calendarService; 16 | 17 | public EventsController(EventsCalendarService calendarService) { 18 | this.calendarService = calendarService; 19 | } 20 | 21 | @GetMapping("/community") 22 | public String community(Model model) { 23 | List events = calendarService.findEvents(Period.of(LocalDate.now().toString(), NEXT_SIX_MONTHS)); 24 | int count = Math.min(events.size(), 6); 25 | model.addAttribute("events", events.subList(0, count)); 26 | return "events/community"; 27 | } 28 | 29 | @GetMapping("/events") 30 | public String events(Model model) { 31 | List events = calendarService.findEvents(Period.of(LocalDate.now().toString(), NEXT_SIX_MONTHS)); 32 | model.addAttribute("events", events); 33 | return "events/list"; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/events/GoogleCalendar.java: -------------------------------------------------------------------------------- 1 | package sagan.site.events; 2 | 3 | import java.util.List; 4 | 5 | import com.fasterxml.jackson.annotation.JsonCreator; 6 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 7 | import com.fasterxml.jackson.annotation.JsonProperty; 8 | 9 | @JsonIgnoreProperties(ignoreUnknown = true) 10 | public class GoogleCalendar { 11 | 12 | private String summary; 13 | 14 | private List events; 15 | 16 | @JsonCreator 17 | public GoogleCalendar(@JsonProperty("summary") String summary, @JsonProperty("items") List events) { 18 | this.summary = summary; 19 | this.events = events; 20 | } 21 | 22 | public String getSummary() { 23 | return summary; 24 | } 25 | 26 | public void setSummary(String summary) { 27 | this.summary = summary; 28 | } 29 | 30 | public List getEvents() { 31 | return events; 32 | } 33 | 34 | public void setEvents(List events) { 35 | this.events = events; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/events/InvalidCalendarException.java: -------------------------------------------------------------------------------- 1 | package sagan.site.events; 2 | 3 | @SuppressWarnings("serial") 4 | public class InvalidCalendarException extends RuntimeException { 5 | 6 | public InvalidCalendarException(String message) { 7 | super(message); 8 | } 9 | 10 | public InvalidCalendarException(String message, Throwable cause) { 11 | super(message, cause); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/events/Period.java: -------------------------------------------------------------------------------- 1 | package sagan.site.events; 2 | 3 | import java.time.LocalDate; 4 | import java.time.LocalTime; 5 | import java.time.ZoneId; 6 | import java.time.ZonedDateTime; 7 | import java.util.function.Predicate; 8 | 9 | import org.springframework.util.Assert; 10 | 11 | public class Period { 12 | 13 | private final ZonedDateTime startDateTime; 14 | 15 | private final int days; 16 | 17 | private Period(ZonedDateTime startDateTime, int days) { 18 | this.startDateTime = startDateTime; 19 | this.days = days; 20 | } 21 | 22 | public static Period of(String startDate, int days) { 23 | Assert.isTrue(days > 0, "days should be a positive integer"); 24 | ZonedDateTime startDateTime = ZonedDateTime.of(LocalDate.parse(startDate), LocalTime.MIDNIGHT, ZoneId.systemDefault()); 25 | return new Period(startDateTime, days); 26 | } 27 | 28 | public ZonedDateTime getStartDateTime() { 29 | return this.startDateTime; 30 | } 31 | 32 | public int getDays() { 33 | return days; 34 | } 35 | 36 | protected Predicate toCalendarFilter() { 37 | return event -> { 38 | return event.getStartTime().isAfter(this.startDateTime) 39 | && event.getStartTime().isBefore(this.startDateTime.plusDays(this.days)); 40 | }; 41 | } 42 | 43 | @Override 44 | public String toString() { 45 | return "Period{" + 46 | "startDateTime=" + startDateTime + 47 | ", days=" + days + 48 | '}'; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/guides/GettingStartedGuide.java: -------------------------------------------------------------------------------- 1 | package sagan.site.guides; 2 | 3 | import sagan.site.renderer.GuideContent; 4 | 5 | public class GettingStartedGuide extends AbstractGuide { 6 | 7 | private final static String TYPE_LABEL = "Getting Started"; 8 | 9 | // only used for JSON serialization 10 | public GettingStartedGuide() { 11 | this.setTypeLabel(TYPE_LABEL); 12 | } 13 | 14 | public GettingStartedGuide(GuideHeader header, GuideContent content) { 15 | super(TYPE_LABEL, header, content); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/guides/Guide.java: -------------------------------------------------------------------------------- 1 | package sagan.site.guides; 2 | 3 | import java.util.Optional; 4 | 5 | import com.fasterxml.jackson.annotation.JsonTypeInfo; 6 | 7 | @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) 8 | public interface Guide extends GuideHeader { 9 | 10 | String getContent(); 11 | 12 | String getTableOfContents(); 13 | 14 | Optional getImageContent(String imageName); 15 | 16 | String getPushToPwsUrl(); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/guides/GuideHeader.java: -------------------------------------------------------------------------------- 1 | package sagan.site.guides; 2 | 3 | import java.util.Set; 4 | 5 | import com.fasterxml.jackson.annotation.JsonTypeInfo; 6 | 7 | @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) 8 | public interface GuideHeader { 9 | 10 | String getName(); 11 | 12 | String getRepositoryName(); 13 | 14 | String getTitle(); 15 | 16 | String getDescription(); 17 | 18 | String getGithubUrl(); 19 | 20 | String getGitUrl(); 21 | 22 | String getSshUrl(); 23 | 24 | String getCloneUrl(); 25 | 26 | Set getProjects(); 27 | 28 | String getZipUrl(); 29 | 30 | String getCiStatusImageUrl(); 31 | 32 | String getCiLatestUrl(); 33 | 34 | } 35 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/guides/GuideImage.java: -------------------------------------------------------------------------------- 1 | package sagan.site.guides; 2 | 3 | public class GuideImage { 4 | 5 | private String name; 6 | 7 | private String encodedContent; 8 | 9 | public GuideImage() { 10 | } 11 | 12 | public GuideImage(String name, String encodedContent) { 13 | this.name = name; 14 | this.encodedContent = encodedContent; 15 | } 16 | 17 | public String getName() { 18 | return name; 19 | } 20 | 21 | public void setName(String name) { 22 | this.name = name; 23 | } 24 | 25 | public String getEncodedContent() { 26 | return encodedContent; 27 | } 28 | 29 | public void setEncodedContent(String encodedContent) { 30 | this.encodedContent = encodedContent; 31 | } 32 | 33 | 34 | } 35 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/guides/GuideIndexController.java: -------------------------------------------------------------------------------- 1 | package sagan.site.guides; 2 | 3 | import java.util.Arrays; 4 | 5 | import sagan.support.nav.Navigation; 6 | import sagan.support.nav.Section; 7 | 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Controller; 10 | import org.springframework.ui.Model; 11 | import org.springframework.web.bind.annotation.GetMapping; 12 | 13 | /** 14 | * Controller that handles requests for the index page for all guide docs at /guides. 15 | * 16 | * @see GettingStartedGuideController 17 | * @see TutorialController 18 | * @see TopicalController 19 | */ 20 | @Controller 21 | @Navigation(Section.GUIDES) 22 | class GuideIndexController { 23 | 24 | private final GettingStartedGuides gsGuides; 25 | 26 | private final Tutorials tutorials; 27 | 28 | private final Topicals topicals; 29 | 30 | @Autowired 31 | public GuideIndexController(GettingStartedGuides gsGuides, Tutorials tutorials, Topicals topicals) { 32 | this.gsGuides = gsGuides; 33 | this.tutorials = tutorials; 34 | this.topicals = topicals; 35 | } 36 | 37 | @GetMapping("/guides") 38 | public String viewIndex(Model model) { 39 | model.addAttribute("guides", Arrays.asList(gsGuides.findAll())); 40 | model.addAttribute("tutorials", Arrays.asList(tutorials.findAll())); 41 | model.addAttribute("topicals", Arrays.asList(topicals.findAll())); 42 | return "guides/index"; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/guides/GuidesRepository.java: -------------------------------------------------------------------------------- 1 | package sagan.site.guides; 2 | 3 | import java.util.Optional; 4 | 5 | import sagan.site.projects.Project; 6 | 7 | public interface GuidesRepository { 8 | 9 | GuideHeader[] findAll(); 10 | 11 | Optional findGuideHeaderByName(String name); 12 | 13 | Optional findByName(String name); 14 | 15 | GuideHeader[] findByProject(Project project); 16 | } 17 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/guides/Topical.java: -------------------------------------------------------------------------------- 1 | package sagan.site.guides; 2 | 3 | import sagan.site.renderer.GuideContent; 4 | 5 | public class Topical extends AbstractGuide { 6 | 7 | private final static String TYPE_LABEL = "Topical Guide"; 8 | 9 | // only used for JSON serialization 10 | public Topical() { 11 | this.setTypeLabel(TYPE_LABEL); 12 | } 13 | 14 | public Topical(GuideHeader metadata, GuideContent content) { 15 | super(TYPE_LABEL, metadata, content); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/guides/Tutorial.java: -------------------------------------------------------------------------------- 1 | package sagan.site.guides; 2 | 3 | import sagan.site.renderer.GuideContent; 4 | 5 | public class Tutorial extends AbstractGuide { 6 | 7 | private final static String TYPE_LABEL = "Tutorial"; 8 | 9 | // only used for JSON serialization 10 | public Tutorial() { 11 | this.setTypeLabel(TYPE_LABEL); 12 | } 13 | 14 | public Tutorial(GuideHeader header, GuideContent content) { 15 | super(TYPE_LABEL, header, content); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/guides/WebhookAuthenticationException.java: -------------------------------------------------------------------------------- 1 | package sagan.site.guides; 2 | 3 | /** 4 | * Exception raised when a github webhook message is received but its 5 | * HMAC signature does not match the one computed with the shared secret. 6 | */ 7 | public class WebhookAuthenticationException extends RuntimeException { 8 | 9 | public WebhookAuthenticationException(String expected, String actual) { 10 | super(String.format("Could not verify signature: '%s', expected '%s'", actual, expected)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/learn/LearnController.java: -------------------------------------------------------------------------------- 1 | package sagan.site.learn; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import sagan.site.blog.Post; 7 | import sagan.site.blog.support.PostView; 8 | import sagan.site.blog.BlogService; 9 | import sagan.support.DateFactory; 10 | import sagan.support.nav.PageableFactory; 11 | 12 | import org.springframework.data.domain.Page; 13 | import org.springframework.stereotype.Controller; 14 | import org.springframework.ui.Model; 15 | import org.springframework.web.bind.annotation.GetMapping; 16 | 17 | @Controller 18 | public class LearnController { 19 | 20 | private final BlogService blogService; 21 | 22 | private final DateFactory dateFactory; 23 | 24 | public LearnController(BlogService blogService, DateFactory dateFactory) { 25 | this.blogService = blogService; 26 | this.dateFactory = dateFactory; 27 | } 28 | 29 | @GetMapping("/learn") 30 | public String learn(Model model) { 31 | Page page = this.blogService.getPublishedPosts(PageableFactory.first(4)); 32 | Page postViewPage = PostView.pageOf(page, dateFactory); 33 | List posts = new ArrayList<>(postViewPage.getContent()); 34 | model.addAttribute("newestPost", posts.remove(0)); 35 | model.addAttribute("posts", posts); 36 | return "learn/index"; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/projects/Display.java: -------------------------------------------------------------------------------- 1 | package sagan.site.projects; 2 | 3 | import javax.persistence.Embeddable; 4 | 5 | /** 6 | * Project information useful for displaying the project on the website 7 | */ 8 | @Embeddable 9 | public class Display { 10 | 11 | private String siteUrl; 12 | 13 | private boolean featured; 14 | 15 | private String tagLine; 16 | 17 | private int sortOrder = Integer.MAX_VALUE; 18 | 19 | private String imagePath; 20 | 21 | public String getSiteUrl() { 22 | return this.siteUrl; 23 | } 24 | 25 | public void setSiteUrl(String siteUrl) { 26 | this.siteUrl = siteUrl; 27 | } 28 | 29 | public boolean isFeatured() { 30 | return this.featured; 31 | } 32 | 33 | public void setFeatured(boolean featured) { 34 | this.featured = featured; 35 | } 36 | 37 | public String getTagLine() { 38 | return this.tagLine; 39 | } 40 | 41 | public void setTagLine(String tagLine) { 42 | this.tagLine = tagLine; 43 | } 44 | 45 | public int getSortOrder() { 46 | return this.sortOrder; 47 | } 48 | 49 | public void setSortOrder(int sortOrder) { 50 | this.sortOrder = sortOrder; 51 | } 52 | 53 | public String getImagePath() { 54 | return this.imagePath; 55 | } 56 | 57 | public void setImagePath(String imagePath) { 58 | this.imagePath = imagePath; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/projects/InvalidProjectGenerationDateException.java: -------------------------------------------------------------------------------- 1 | package sagan.site.projects; 2 | 3 | import java.time.LocalDate; 4 | 5 | public class InvalidProjectGenerationDateException extends RuntimeException { 6 | 7 | private final LocalDate invalidDate; 8 | 9 | InvalidProjectGenerationDateException(LocalDate invalidDate, String message) { 10 | super(message); 11 | this.invalidDate = invalidDate; 12 | } 13 | 14 | static InvalidProjectGenerationDateException nullReleaseDate() { 15 | return new InvalidProjectGenerationDateException(null, "Initial release date should not be null"); 16 | } 17 | 18 | static InvalidProjectGenerationDateException endOfSupportBeforeReleaseDate(LocalDate supportEndDate) { 19 | return new InvalidProjectGenerationDateException(supportEndDate, "End of support should be after initial release"); 20 | } 21 | 22 | static InvalidProjectGenerationDateException commercialSupportEndsBeforeOssSupport(LocalDate supportEndDate) { 23 | return new InvalidProjectGenerationDateException(supportEndDate, "Commercial support should not end before OSS support."); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/projects/InvalidProjectReleaseException.java: -------------------------------------------------------------------------------- 1 | package sagan.site.projects; 2 | 3 | public class InvalidProjectReleaseException extends RuntimeException { 4 | 5 | public InvalidProjectReleaseException(String message) { 6 | super(message); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/projects/InvalidVersionException.java: -------------------------------------------------------------------------------- 1 | package sagan.site.projects; 2 | 3 | public class InvalidVersionException extends RuntimeException { 4 | 5 | public InvalidVersionException(String message) { 6 | super(message); 7 | } 8 | 9 | } 10 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/projects/MarkupDocument.java: -------------------------------------------------------------------------------- 1 | package sagan.site.projects; 2 | 3 | import javax.persistence.Embeddable; 4 | 5 | /** 6 | * Document written using a markup language (such as Asciidoctor) 7 | */ 8 | @Embeddable 9 | public class MarkupDocument { 10 | 11 | /** 12 | * Source of the document 13 | */ 14 | private String source; 15 | 16 | /** 17 | * Rendered document in HTML 18 | */ 19 | private String html; 20 | 21 | public String getSource() { 22 | return source; 23 | } 24 | 25 | public void setSource(String source) { 26 | this.source = source; 27 | } 28 | 29 | public String getHtml() { 30 | return html; 31 | } 32 | 33 | public void setHtml(String html) { 34 | this.html = html; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/projects/ProjectGenerationsInfo.java: -------------------------------------------------------------------------------- 1 | package sagan.site.projects; 2 | 3 | import java.time.LocalDateTime; 4 | import java.time.OffsetDateTime; 5 | import java.time.ZoneOffset; 6 | import java.util.SortedSet; 7 | import java.util.TreeSet; 8 | 9 | import javax.persistence.CascadeType; 10 | import javax.persistence.Embeddable; 11 | import javax.persistence.OneToMany; 12 | 13 | import org.hibernate.annotations.SortNatural; 14 | 15 | @Embeddable 16 | public class ProjectGenerationsInfo { 17 | 18 | /** 19 | * Set of available {@link ProjectGeneration} sorted by their name. 20 | */ 21 | @OneToMany(mappedBy = "project", cascade = CascadeType.ALL, orphanRemoval = true) 22 | @SortNatural 23 | private SortedSet generations = new TreeSet<>(); 24 | 25 | /** 26 | * Last modification date of the set of generations. 27 | */ 28 | private OffsetDateTime lastModified = LocalDateTime.now().atOffset(ZoneOffset.UTC); 29 | 30 | public SortedSet getGenerations() { 31 | return generations; 32 | } 33 | 34 | public void setGenerations(SortedSet generations) { 35 | this.generations = generations; 36 | } 37 | 38 | public OffsetDateTime getLastModified() { 39 | return lastModified; 40 | } 41 | 42 | public void setLastModified(OffsetDateTime lastModified) { 43 | this.lastModified = lastModified; 44 | } 45 | 46 | public void recordModification() { 47 | this.lastModified = LocalDateTime.now().atOffset(ZoneOffset.UTC); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/projects/ProjectGroupRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package sagan.site.projects; 17 | 18 | import org.springframework.data.jpa.repository.JpaRepository; 19 | import org.springframework.stereotype.Repository; 20 | 21 | @Repository 22 | public interface ProjectGroupRepository extends JpaRepository { 23 | 24 | ProjectGroup findByNameIgnoreCase(String name); 25 | } 26 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/projects/ProjectRepository.java: -------------------------------------------------------------------------------- 1 | package sagan.site.projects; 2 | 3 | import java.util.List; 4 | 5 | import org.springframework.data.domain.Sort; 6 | import org.springframework.data.jpa.repository.EntityGraph; 7 | import org.springframework.data.jpa.repository.JpaRepository; 8 | import org.springframework.data.jpa.repository.Query; 9 | import org.springframework.data.repository.query.Param; 10 | import org.springframework.stereotype.Repository; 11 | 12 | @Repository 13 | public interface ProjectRepository extends JpaRepository { 14 | 15 | @Query("SELECT DISTINCT p FROM Project p LEFT JOIN FETCH p.releases r LEFT JOIN FETCH p.generationsInfo g LEFT JOIN FETCH p.samples s WHERE p.id =:id") 16 | Project fetchFullProject(@Param("id") String id); 17 | 18 | @EntityGraph(value = "Project.tree", type = EntityGraph.EntityGraphType.LOAD) 19 | List findDistinctByStatusAndParentProjectIsNull(SupportStatus status, Sort sort); 20 | 21 | @Query("SELECT DISTINCT p FROM Project p LEFT JOIN FETCH p.groups WHERE p.parentProject = NULL ORDER BY p.display.sortOrder ASC") 22 | List findTopLevelProjectsWithGroup(); 23 | } 24 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/projects/ReleaseRepository.java: -------------------------------------------------------------------------------- 1 | package sagan.site.projects; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.data.jpa.repository.Query; 5 | import org.springframework.data.repository.query.Param; 6 | import org.springframework.stereotype.Repository; 7 | 8 | @Repository 9 | public interface ReleaseRepository extends JpaRepository { 10 | 11 | @Query("SELECT r FROM Release r LEFT JOIN FETCH r.project p WHERE p.id =:projectId AND r.version =:version") 12 | Release findRelease(@Param("projectId") String projectId, @Param("version") Version version); 13 | 14 | @Query("SELECT r FROM Release r LEFT JOIN FETCH r.project p WHERE p.id =:projectId AND r.isCurrent = TRUE") 15 | Release findCurrentRelease(@Param("projectId") String projectId); 16 | } 17 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/projects/ReleaseStatus.java: -------------------------------------------------------------------------------- 1 | package sagan.site.projects; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | import org.springframework.util.Assert; 6 | 7 | /** 8 | * Status of a {@link Release} 9 | */ 10 | public enum ReleaseStatus { 11 | /** 12 | * Unstable version with limited support 13 | */ 14 | SNAPSHOT, 15 | /** 16 | * Pre-Release version meant to be tested by the community 17 | */ 18 | PRERELEASE, 19 | /** 20 | * Release Generally Available on public artifact repositories and enjoying full support from maintainers 21 | */ 22 | GENERAL_AVAILABILITY; 23 | 24 | private static final Pattern PRERELEASE_PATTERN = Pattern.compile("[A-Za-z0-9\\.\\-]+?(M|RC)\\d+"); 25 | 26 | private static final String SNAPSHOT_SUFFIX = "SNAPSHOT"; 27 | 28 | /** 29 | * Deduce the {@link ReleaseStatus status} of a release given its {@link Version} 30 | * @param version a project version 31 | * @return the release status for this version 32 | */ 33 | public static ReleaseStatus getFromVersion(Version version) { 34 | Assert.notNull(version, "Version must not be null"); 35 | if (version.toString().endsWith(SNAPSHOT_SUFFIX)) { 36 | return SNAPSHOT; 37 | } 38 | if (PRERELEASE_PATTERN.matcher(version.toString()).matches()) { 39 | return PRERELEASE; 40 | } 41 | return GENERAL_AVAILABILITY; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/projects/SupportStatus.java: -------------------------------------------------------------------------------- 1 | package sagan.site.projects; 2 | 3 | /** 4 | * Support status for Spring projects 5 | */ 6 | public enum SupportStatus { 7 | 8 | /** 9 | * Project is incubating and is not supported for production use 10 | */ 11 | INCUBATING("Incubating"), 12 | /** 13 | * Project is actively supported by the Spring team 14 | */ 15 | ACTIVE("Active"), 16 | /** 17 | * Project is actively supported by the Spring community 18 | */ 19 | COMMUNITY("Community"), 20 | /** 21 | * Project is not supported anymore 22 | */ 23 | END_OF_LIFE("End Of Life"); 24 | 25 | private final String label; 26 | 27 | SupportStatus(String label) { 28 | this.label = label; 29 | } 30 | 31 | public String getLabel() { 32 | return label; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/projects/VersionConverter.java: -------------------------------------------------------------------------------- 1 | package sagan.site.projects; 2 | 3 | import javax.persistence.AttributeConverter; 4 | import javax.persistence.Converter; 5 | 6 | /** 7 | * JPA {@link AttributeConverter} for converting {@link Version} to/from its {@code String} representation. 8 | * @see Release 9 | */ 10 | @Converter(autoApply = true) 11 | public class VersionConverter implements AttributeConverter { 12 | 13 | @Override 14 | public String convertToDatabaseColumn(Version version) { 15 | return version.toString(); 16 | } 17 | 18 | @Override 19 | public Version convertToEntityAttribute(String rawVersion) { 20 | return Version.of(rawVersion); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/projects/admin/ProjectFormMetadata.java: -------------------------------------------------------------------------------- 1 | package sagan.site.projects.admin; 2 | 3 | public class ProjectFormMetadata { 4 | 5 | String id; 6 | 7 | String name; 8 | 9 | String parentProjectId; 10 | 11 | String repoUrl; 12 | 13 | String status; 14 | 15 | String stackOverflowTags; 16 | 17 | public ProjectFormMetadata() { 18 | } 19 | 20 | public ProjectFormMetadata(String id, String name) { 21 | this.id = id; 22 | this.name = name; 23 | } 24 | 25 | public String getId() { 26 | return id; 27 | } 28 | 29 | public void setId(String id) { 30 | this.id = id; 31 | } 32 | 33 | public String getName() { 34 | return name; 35 | } 36 | 37 | public void setName(String name) { 38 | this.name = name; 39 | } 40 | 41 | public String getParentProjectId() { 42 | return parentProjectId; 43 | } 44 | 45 | public void setParentProjectId(String parentProjectId) { 46 | this.parentProjectId = parentProjectId; 47 | } 48 | 49 | public String getRepoUrl() { 50 | return repoUrl; 51 | } 52 | 53 | public void setRepoUrl(String repoUrl) { 54 | this.repoUrl = repoUrl; 55 | } 56 | 57 | public String getStatus() { 58 | return status; 59 | } 60 | 61 | public void setStatus(String status) { 62 | this.status = status; 63 | } 64 | 65 | public String getStackOverflowTags() { 66 | return stackOverflowTags; 67 | } 68 | 69 | public void setStackOverflowTags(String stackOverflowTags) { 70 | this.stackOverflowTags = stackOverflowTags; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/renderer/GuideContent.java: -------------------------------------------------------------------------------- 1 | package sagan.site.renderer; 2 | 3 | import java.util.List; 4 | 5 | import com.fasterxml.jackson.annotation.JsonCreator; 6 | import com.fasterxml.jackson.annotation.JsonProperty; 7 | 8 | public class GuideContent { 9 | 10 | private String repositoryName; 11 | 12 | private String tableOfContents; 13 | 14 | private String content; 15 | 16 | private String pushToPwsMetadata; 17 | 18 | private List images; 19 | 20 | @JsonCreator 21 | public GuideContent(@JsonProperty("repositoryName") String repositoryName, 22 | @JsonProperty("tableOfContents") String tableOfContents, 23 | @JsonProperty("content") String content, 24 | @JsonProperty("pushToPwsMetadata") String pushToPwsMetadata, 25 | @JsonProperty("images") List images) { 26 | this.repositoryName = repositoryName; 27 | this.tableOfContents = tableOfContents; 28 | this.content = content; 29 | this.pushToPwsMetadata = pushToPwsMetadata; 30 | this.images = images; 31 | } 32 | 33 | public String getRepositoryName() { 34 | return repositoryName; 35 | } 36 | 37 | public String getTableOfContents() { 38 | return tableOfContents; 39 | } 40 | 41 | public String getContent() { 42 | return content; 43 | } 44 | 45 | public String getPushToPwsMetadata() { 46 | return pushToPwsMetadata; 47 | } 48 | 49 | public List getImages() { 50 | return images; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/renderer/GuideImage.java: -------------------------------------------------------------------------------- 1 | package sagan.site.renderer; 2 | 3 | public class GuideImage { 4 | 5 | private String name; 6 | 7 | private String encodedContent; 8 | 9 | public GuideImage() { 10 | } 11 | 12 | public GuideImage(String name, String encodedContent) { 13 | this.name = name; 14 | this.encodedContent = encodedContent; 15 | } 16 | 17 | public String getName() { 18 | return name; 19 | } 20 | 21 | public void setName(String name) { 22 | this.name = name; 23 | } 24 | 25 | public String getEncodedContent() { 26 | return encodedContent; 27 | } 28 | 29 | public void setEncodedContent(String encodedContent) { 30 | this.encodedContent = encodedContent; 31 | } 32 | 33 | 34 | } 35 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/renderer/GuideType.java: -------------------------------------------------------------------------------- 1 | package sagan.site.renderer; 2 | 3 | import java.util.Arrays; 4 | 5 | import com.fasterxml.jackson.annotation.JsonCreator; 6 | 7 | /** 8 | * Guide Types 9 | */ 10 | public enum GuideType { 11 | 12 | GETTING_STARTED("getting-started"), TUTORIAL("tutorial"), 13 | TOPICAL("topical"), UNKNOWN("unknown"); 14 | 15 | private final String name; 16 | 17 | 18 | GuideType(String name) { 19 | this.name = name; 20 | } 21 | 22 | public String getName() { 23 | return name; 24 | } 25 | 26 | @Override 27 | public String toString() { 28 | return this.name; 29 | } 30 | 31 | @JsonCreator 32 | public static GuideType fromName(String name) { 33 | return Arrays.stream(GuideType.values()) 34 | .filter(type -> type.getName().equals(name)) 35 | .findFirst().orElse(GuideType.UNKNOWN); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/team/GeoLocation.java: -------------------------------------------------------------------------------- 1 | package sagan.site.team; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Embeddable; 5 | 6 | @Embeddable 7 | public class GeoLocation { 8 | 9 | @Column(name = "latitude") 10 | private float latitude; 11 | 12 | @Column(name = "longitude") 13 | private float longitude; 14 | 15 | public GeoLocation() { 16 | } 17 | 18 | public GeoLocation(float latitude, float longitude) { 19 | this.latitude = latitude; 20 | this.longitude = longitude; 21 | } 22 | 23 | public float getLatitude() { 24 | return latitude; 25 | } 26 | 27 | public void setLatitude(float latitude) { 28 | this.latitude = latitude; 29 | } 30 | 31 | public float getLongitude() { 32 | return longitude; 33 | } 34 | 35 | public void setLongitude(float longitude) { 36 | this.longitude = longitude; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/team/Link.java: -------------------------------------------------------------------------------- 1 | package sagan.site.team; 2 | 3 | public class Link { 4 | 5 | String href; 6 | String text; 7 | 8 | public Link(String href, String text) { 9 | this.href = href; 10 | this.text = text; 11 | } 12 | 13 | public String getHref() { 14 | return href; 15 | } 16 | 17 | public String getText() { 18 | return text; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/team/TeamLocation.java: -------------------------------------------------------------------------------- 1 | package sagan.site.team; 2 | 3 | public class TeamLocation { 4 | private String name; 5 | private float latitude; 6 | private float longitude; 7 | private Long memberId; 8 | 9 | public TeamLocation() { 10 | } 11 | 12 | public TeamLocation(String name, float latitude, float longitude, Long memberId) { 13 | this.name = name; 14 | this.latitude = latitude; 15 | this.longitude = longitude; 16 | this.memberId = memberId; 17 | } 18 | 19 | public String getName() { 20 | return name; 21 | } 22 | 23 | public void setName(String name) { 24 | this.name = name; 25 | } 26 | 27 | public float getLatitude() { 28 | return latitude; 29 | } 30 | 31 | public void setLatitude(float latitude) { 32 | this.latitude = latitude; 33 | } 34 | 35 | public float getLongitude() { 36 | return longitude; 37 | } 38 | 39 | public void setLongitude(float longitude) { 40 | this.longitude = longitude; 41 | } 42 | 43 | public Long getMemberId() { 44 | return memberId; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/team/support/GeoLocationFormatter.java: -------------------------------------------------------------------------------- 1 | package sagan.site.team.support; 2 | 3 | import sagan.site.team.GeoLocation; 4 | 5 | import java.text.ParseException; 6 | import java.util.Locale; 7 | import java.util.regex.Matcher; 8 | import java.util.regex.Pattern; 9 | 10 | import org.springframework.format.Formatter; 11 | import org.springframework.stereotype.Component; 12 | 13 | @Component 14 | class GeoLocationFormatter implements Formatter { 15 | 16 | public static final Pattern PATTERN = Pattern.compile("(-?\\d+(?:\\.\\d+)?)\\s*,\\s*(-?\\d+(?:\\.\\d+)?)"); 17 | 18 | @Override 19 | public GeoLocation parse(String text, Locale locale) throws ParseException { 20 | Matcher m = PATTERN.matcher(text); 21 | if (!m.find()) { 22 | throw new ParseException(text, 0); 23 | } 24 | float latitude = Float.valueOf(m.group(1)); 25 | float longitude = Float.valueOf(m.group(2)); 26 | return new GeoLocation(latitude, longitude); 27 | } 28 | 29 | @Override 30 | public String print(GeoLocation location, Locale locale) { 31 | return String.format(Locale.US, "%f,%f", location.getLatitude(), location.getLongitude()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/team/support/MemberNotFoundException.java: -------------------------------------------------------------------------------- 1 | package sagan.site.team.support; 2 | 3 | import org.springframework.web.bind.annotation.ResponseStatus; 4 | 5 | import static org.springframework.http.HttpStatus.NOT_FOUND; 6 | 7 | /** 8 | * Exception raised when an unknown or otherwise hidden team member page is requested, 9 | * e.g. /team/{unknown} or /admin/team/{unknown}. 10 | * 11 | * Note that because this class is marked with {@code @ResponseStatus(NOT_FOUND)}, the 12 | * site-wide 404 page will be displayed when this exception is handled. See 13 | * {@link sagan.MvcConfig.ErrorConfig} for details, and contrast the approach used here 14 | * with {@link sagan.MvcConfig#handleException(sagan.support.ResourceNotFoundException)} 15 | */ 16 | @ResponseStatus(NOT_FOUND) 17 | @SuppressWarnings("serial") 18 | class MemberNotFoundException extends RuntimeException { 19 | 20 | public MemberNotFoundException(String username) { 21 | this("Could not find member profile with username '%s'", username); 22 | } 23 | 24 | public MemberNotFoundException(String message, Object... args) { 25 | super(String.format(message, args)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/team/support/TeamImporter.java: -------------------------------------------------------------------------------- 1 | package sagan.site.team.support; 2 | 3 | import org.springframework.social.github.api.GitHub; 4 | 5 | interface TeamImporter { 6 | void importTeamMembers(GitHub gitHub); 7 | } 8 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/team/support/TeamRepository.java: -------------------------------------------------------------------------------- 1 | package sagan.site.team.support; 2 | 3 | import sagan.site.team.MemberProfile; 4 | 5 | import java.util.List; 6 | 7 | import org.springframework.data.jpa.repository.JpaRepository; 8 | import org.springframework.data.jpa.repository.Modifying; 9 | import org.springframework.data.jpa.repository.Query; 10 | import org.springframework.data.repository.query.Param; 11 | import org.springframework.stereotype.Repository; 12 | 13 | @Repository 14 | public interface TeamRepository extends JpaRepository { 15 | MemberProfile findById(Long id); 16 | 17 | MemberProfile findByGithubId(Long githubId); 18 | 19 | MemberProfile findByUsername(String username); 20 | 21 | List findByHiddenOrderByNameAsc(boolean hidden); 22 | 23 | @Modifying(clearAutomatically = true) 24 | @Query("update MemberProfile p set p.hidden = true where (p.githubId not in :ids or p.githubId = null)") 25 | int hideTeamMembersNotInIds(@Param("ids") List ids); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/tools/SpringToolsPlatform.java: -------------------------------------------------------------------------------- 1 | package sagan.site.tools; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import javax.persistence.ElementCollection; 6 | import javax.persistence.Entity; 7 | import javax.persistence.Id; 8 | 9 | /** 10 | * Spring Tools supported platform 11 | */ 12 | @Entity 13 | @SuppressWarnings("serial") 14 | public class SpringToolsPlatform { 15 | 16 | @Id 17 | private String id; 18 | 19 | @ElementCollection 20 | private List downloads = new ArrayList<>(); 21 | 22 | @SuppressWarnings("unused") 23 | private SpringToolsPlatform() { 24 | } 25 | 26 | public SpringToolsPlatform(String id) { 27 | this.id = id; 28 | } 29 | 30 | public SpringToolsPlatform(String id, List downloads) { 31 | this.id = id; 32 | this.downloads = downloads; 33 | } 34 | 35 | public void setId(String id) { 36 | this.id = id; 37 | } 38 | 39 | public String getId() { 40 | return id; 41 | } 42 | 43 | public List getDownloads() { 44 | return downloads; 45 | } 46 | 47 | public void setDownloads(List downloads) { 48 | this.downloads = downloads; 49 | } 50 | 51 | @Override 52 | public String toString() { 53 | return "SpringToolsPlatform{" + 54 | "id='" + id + '\'' + 55 | ", downloads=" + downloads + 56 | '}'; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/tools/SpringToolsPlatformRepository.java: -------------------------------------------------------------------------------- 1 | package sagan.site.tools; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.stereotype.Repository; 5 | 6 | @Repository 7 | public interface SpringToolsPlatformRepository extends JpaRepository { 8 | 9 | } 10 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/tools/support/SpringToolsController.java: -------------------------------------------------------------------------------- 1 | package sagan.site.tools.support; 2 | 3 | import sagan.site.tools.SpringToolsPlatformRepository; 4 | 5 | import org.springframework.stereotype.Controller; 6 | import org.springframework.ui.Model; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | 10 | /** 11 | * Controller for Spring Tools page 12 | */ 13 | @Controller 14 | @RequestMapping("/tools") 15 | public class SpringToolsController { 16 | 17 | private final SpringToolsPlatformRepository repository; 18 | 19 | public SpringToolsController(SpringToolsPlatformRepository repository) { 20 | this.repository = repository; 21 | } 22 | 23 | @GetMapping 24 | public String listDownloads(Model model) { 25 | this.repository.findAll().forEach(platform -> { 26 | model.addAttribute(platform.getId(), platform); 27 | }); 28 | return "tools/list"; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/webapi/IndexController.java: -------------------------------------------------------------------------------- 1 | package sagan.site.webapi; 2 | 3 | import sagan.site.webapi.project.ProjectMetadataController; 4 | import sagan.site.webapi.repository.RepositoryMetadataController; 5 | 6 | import org.springframework.hateoas.MediaTypes; 7 | import org.springframework.hateoas.ResourceSupport; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; 12 | import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn; 13 | 14 | /** 15 | * Lists all resources at the root of the Web API 16 | */ 17 | @RestController 18 | class IndexController { 19 | 20 | @GetMapping(path = "/api", produces = MediaTypes.HAL_JSON_VALUE) 21 | public ResourceSupport index() { 22 | ResourceSupport resource = new ResourceSupport(); 23 | resource.add(linkTo(methodOn(ProjectMetadataController.class).listProjects()).withRel("projects")); 24 | resource.add(linkTo(methodOn(RepositoryMetadataController.class).listRepositories()).withRel("repositories")); 25 | return resource; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/webapi/WebApiControllerAdvice.java: -------------------------------------------------------------------------------- 1 | package sagan.site.webapi; 2 | 3 | import sagan.site.webapi.release.InvalidReleaseException; 4 | 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.web.bind.annotation.ControllerAdvice; 7 | import org.springframework.web.bind.annotation.ExceptionHandler; 8 | 9 | /** 10 | * 11 | */ 12 | @ControllerAdvice(basePackageClasses = IndexController.class) 13 | public class WebApiControllerAdvice { 14 | 15 | @ExceptionHandler(InvalidReleaseException.class) 16 | public ResponseEntity handleInvalidReleases(InvalidReleaseException ex) { 17 | return ResponseEntity.badRequest().body(ex.getMessage()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/webapi/project/ProjectMetadata.java: -------------------------------------------------------------------------------- 1 | package sagan.site.webapi.project; 2 | 3 | import sagan.site.projects.SupportStatus; 4 | 5 | import org.springframework.hateoas.ResourceSupport; 6 | import org.springframework.hateoas.core.Relation; 7 | 8 | /** 9 | * 10 | */ 11 | @Relation(collectionRelation = "projects") 12 | public class ProjectMetadata extends ResourceSupport { 13 | 14 | private String name; 15 | 16 | private String slug; 17 | 18 | private String repositoryUrl; 19 | 20 | private SupportStatus status; 21 | 22 | public String getName() { 23 | return name; 24 | } 25 | 26 | public void setName(String name) { 27 | this.name = name; 28 | } 29 | 30 | public String getSlug() { 31 | return slug; 32 | } 33 | 34 | public void setSlug(String slug) { 35 | this.slug = slug; 36 | } 37 | 38 | public String getRepositoryUrl() { 39 | return repositoryUrl; 40 | } 41 | 42 | public void setRepositoryUrl(String repositoryUrl) { 43 | this.repositoryUrl = repositoryUrl; 44 | } 45 | 46 | public SupportStatus getStatus() { 47 | return status; 48 | } 49 | 50 | public void setStatus(SupportStatus status) { 51 | this.status = status; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/webapi/release/InvalidReleaseException.java: -------------------------------------------------------------------------------- 1 | package sagan.site.webapi.release; 2 | 3 | /** 4 | * 5 | */ 6 | @SuppressWarnings("serial") 7 | public class InvalidReleaseException extends RuntimeException { 8 | 9 | public InvalidReleaseException(String message) { 10 | super(message); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/webapi/release/ReleaseMetadata.java: -------------------------------------------------------------------------------- 1 | package sagan.site.webapi.release; 2 | 3 | import sagan.site.projects.ReleaseStatus; 4 | 5 | import org.springframework.hateoas.ResourceSupport; 6 | import org.springframework.hateoas.core.Relation; 7 | 8 | /** 9 | * 10 | */ 11 | @Relation(collectionRelation = "releases") 12 | public class ReleaseMetadata extends ResourceSupport { 13 | 14 | private String version; 15 | 16 | private ReleaseStatus status; 17 | 18 | private boolean isCurrent; 19 | 20 | private String referenceDocUrl; 21 | 22 | private String apiDocUrl; 23 | 24 | public String getVersion() { 25 | return version; 26 | } 27 | 28 | public void setVersion(String version) { 29 | this.version = version; 30 | } 31 | 32 | public ReleaseStatus getStatus() { 33 | return status; 34 | } 35 | 36 | public void setStatus(ReleaseStatus status) { 37 | this.status = status; 38 | } 39 | 40 | public boolean isCurrent() { 41 | return isCurrent; 42 | } 43 | 44 | public void setCurrent(boolean current) { 45 | isCurrent = current; 46 | } 47 | 48 | public String getReferenceDocUrl() { 49 | return referenceDocUrl; 50 | } 51 | 52 | public void setReferenceDocUrl(String referenceDocUrl) { 53 | this.referenceDocUrl = referenceDocUrl; 54 | } 55 | 56 | public String getApiDocUrl() { 57 | return apiDocUrl; 58 | } 59 | 60 | public void setApiDocUrl(String apiDocUrl) { 61 | this.apiDocUrl = apiDocUrl; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/webapi/release/ReleaseMetadataInput.java: -------------------------------------------------------------------------------- 1 | package sagan.site.webapi.release; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import org.hibernate.validator.constraints.NotBlank; 6 | import org.hibernate.validator.constraints.URL; 7 | 8 | public class ReleaseMetadataInput { 9 | 10 | @NotBlank 11 | private final String version; 12 | 13 | @URL 14 | private final String referenceDocUrl; 15 | 16 | @URL 17 | private final String apiDocUrl; 18 | 19 | @JsonCreator 20 | public ReleaseMetadataInput(@JsonProperty("version") String version, 21 | @JsonProperty("isCurrent") boolean isCurrent, 22 | @JsonProperty("referenceDocUrl") String referenceDocUrl, @JsonProperty("apiDocUrl") String apiDocUrl) { 23 | this.version = version; 24 | this.referenceDocUrl = referenceDocUrl; 25 | this.apiDocUrl = apiDocUrl; 26 | } 27 | 28 | public String getVersion() { 29 | return this.version; 30 | } 31 | 32 | public String getReferenceDocUrl() { 33 | return this.referenceDocUrl; 34 | } 35 | 36 | public String getApiDocUrl() { 37 | return this.apiDocUrl; 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/webapi/repository/RepositoryMetadata.java: -------------------------------------------------------------------------------- 1 | package sagan.site.webapi.repository; 2 | 3 | import org.springframework.hateoas.ResourceSupport; 4 | import org.springframework.hateoas.core.Relation; 5 | 6 | /** 7 | * 8 | */ 9 | @Relation(collectionRelation = "repositories") 10 | public class RepositoryMetadata extends ResourceSupport { 11 | 12 | private String identifier; 13 | 14 | private String name; 15 | 16 | private String url; 17 | 18 | private boolean snapshotsEnabled; 19 | 20 | public RepositoryMetadata() { 21 | } 22 | 23 | public RepositoryMetadata(String identifier, String name, String url, boolean snapshotsEnabled) { 24 | this.identifier = identifier; 25 | this.name = name; 26 | this.url = url; 27 | this.snapshotsEnabled = snapshotsEnabled; 28 | } 29 | 30 | public String getIdentifier() { 31 | return identifier; 32 | } 33 | 34 | public void setIdentifier(String identifier) { 35 | this.identifier = identifier; 36 | } 37 | 38 | public String getName() { 39 | return name; 40 | } 41 | 42 | public void setName(String name) { 43 | this.name = name; 44 | } 45 | 46 | public String getUrl() { 47 | return url; 48 | } 49 | 50 | public void setUrl(String url) { 51 | this.url = url; 52 | } 53 | 54 | public boolean isSnapshotsEnabled() { 55 | return snapshotsEnabled; 56 | } 57 | 58 | public void setSnapshotsEnabled(boolean snapshotsEnabled) { 59 | this.snapshotsEnabled = snapshotsEnabled; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/site/webapi/repository/RepositoryMetadataAssembler.java: -------------------------------------------------------------------------------- 1 | package sagan.site.webapi.repository; 2 | 3 | import sagan.site.projects.Repository; 4 | 5 | import org.springframework.hateoas.mvc.ResourceAssemblerSupport; 6 | import org.springframework.stereotype.Component; 7 | 8 | import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; 9 | import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn; 10 | 11 | /** 12 | * 13 | */ 14 | @Component 15 | class RepositoryMetadataAssembler extends ResourceAssemblerSupport { 16 | 17 | public RepositoryMetadataAssembler() { 18 | super(RepositoryMetadataController.class, RepositoryMetadata.class); 19 | } 20 | 21 | @Override 22 | public RepositoryMetadata toResource(Repository repository) { 23 | RepositoryMetadata repositoryMetadata = new RepositoryMetadata(repository.getId(), repository.getName(), repository.getUrl(), repository.isSnapshotsEnabled()); 24 | repositoryMetadata.add(linkTo(methodOn(RepositoryMetadataController.class).showRepository(repository.getId())).withSelfRel()); 25 | return repositoryMetadata; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/support/DateFactory.java: -------------------------------------------------------------------------------- 1 | package sagan.support; 2 | 3 | import java.util.Date; 4 | import java.util.TimeZone; 5 | 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component 9 | public class DateFactory { 10 | 11 | public static final TimeZone DEFAULT_TIME_ZONE = TimeZone.getTimeZone("UTC"); 12 | 13 | public Date now() { 14 | return new Date(); 15 | } 16 | 17 | public TimeZone timeZone() { 18 | return DEFAULT_TIME_ZONE; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/support/ResourceNotFoundException.java: -------------------------------------------------------------------------------- 1 | package sagan.support; 2 | 3 | @SuppressWarnings("serial") 4 | public class ResourceNotFoundException extends RuntimeException { 5 | 6 | public ResourceNotFoundException(String message) { 7 | super(message); 8 | } 9 | 10 | public ResourceNotFoundException(String message, Throwable ex) { 11 | super(message, ex); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/support/TuckeyRewriteFilter.java: -------------------------------------------------------------------------------- 1 | package sagan.support; 2 | 3 | import org.tuckey.web.filters.urlrewrite.Conf; 4 | 5 | import javax.servlet.FilterConfig; 6 | import javax.servlet.ServletContext; 7 | import javax.servlet.ServletException; 8 | import java.io.InputStream; 9 | import java.net.URL; 10 | 11 | /** 12 | * Subclass of {@link org.tuckey.web.filters.urlrewrite.UrlRewriteFilter} 13 | * that overrides the configuration file loading mechanism. 14 | * 15 | * @author Brian Clozel 16 | */ 17 | public class TuckeyRewriteFilter extends org.tuckey.web.filters.urlrewrite.UrlRewriteFilter { 18 | 19 | @Override 20 | protected void loadUrlRewriter(FilterConfig filterConfig) throws ServletException { 21 | String confPath = filterConfig.getInitParameter("confPath"); 22 | ServletContext context = filterConfig.getServletContext(); 23 | try { 24 | final URL confUrl = getClass().getClassLoader().getResource(confPath); 25 | final InputStream config = getClass().getClassLoader().getResourceAsStream(confPath); 26 | Conf conf = new Conf(context, config, confPath, confUrl.toString(), false); 27 | checkConf(conf); 28 | } catch (Throwable e) { 29 | throw new ServletException(e); 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/support/cache/CachedRestClient.java: -------------------------------------------------------------------------------- 1 | package sagan.support.cache; 2 | 3 | import org.springframework.cache.annotation.Cacheable; 4 | import org.springframework.http.HttpEntity; 5 | import org.springframework.stereotype.Component; 6 | import org.springframework.web.client.RestOperations; 7 | 8 | @Component 9 | public class CachedRestClient { 10 | 11 | public static final String CACHE_NAME = "cache.network"; 12 | public static final String CACHE_TTL = "${cache.network.timetolive:300}"; 13 | 14 | @Cacheable(value = CACHE_NAME, key = "#url") 15 | public T get(RestOperations operations, String url, Class clazz) { 16 | return operations.getForObject(url, clazz); 17 | } 18 | 19 | public T post(RestOperations operations, String url, Class clazz, String body) { 20 | HttpEntity requestEntity = new HttpEntity<>(body); 21 | return operations.postForObject(url, requestEntity, clazz); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/support/cache/JsonRedisTemplate.java: -------------------------------------------------------------------------------- 1 | package sagan.support.cache; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | 5 | import org.springframework.data.redis.connection.RedisConnectionFactory; 6 | import org.springframework.data.redis.core.RedisTemplate; 7 | import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; 8 | import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; 9 | 10 | public class JsonRedisTemplate extends RedisTemplate { 11 | 12 | public JsonRedisTemplate(RedisConnectionFactory connectionFactory, ObjectMapper objectMapper, Class valueType) { 13 | JdkSerializationRedisSerializer jdkSerializer = new JdkSerializationRedisSerializer(); 14 | setKeySerializer(jdkSerializer); 15 | setHashKeySerializer(jdkSerializer); 16 | setHashValueSerializer(jdkSerializer); 17 | Jackson2JsonRedisSerializer jsonRedisSerializer = new Jackson2JsonRedisSerializer<>(valueType); 18 | jsonRedisSerializer.setObjectMapper(objectMapper); 19 | setValueSerializer(jsonRedisSerializer); 20 | setConnectionFactory(connectionFactory); 21 | afterPropertiesSet(); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/support/nav/Navigation.java: -------------------------------------------------------------------------------- 1 | package sagan.support.nav; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Annotation to be used at the class level on Controllers to indicate which 10 | * {@link Section} of the site-wide navigation should be highlighted. 11 | * 12 | * @see sagan.support.nav.Section 13 | * @see sagan.MvcConfig#addInterceptors(org.springframework.web.servlet.config.annotation.InterceptorRegistry) 14 | */ 15 | @Target(ElementType.TYPE) 16 | @Retention(RetentionPolicy.RUNTIME) 17 | public @interface Navigation { 18 | 19 | Section value(); 20 | } 21 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/support/nav/PageableFactory.java: -------------------------------------------------------------------------------- 1 | package sagan.support.nav; 2 | 3 | import org.springframework.data.domain.PageRequest; 4 | import org.springframework.data.domain.Pageable; 5 | import org.springframework.data.domain.Sort; 6 | 7 | /** 8 | * Utility methods for use when creating requests for pages of information, e.g. when 9 | * rendering lists of posts on the blog index or individual team member pages. 10 | */ 11 | public abstract class PageableFactory { 12 | 13 | public static Pageable all() { 14 | return build(0, Integer.MAX_VALUE); 15 | } 16 | 17 | public static Pageable first(int count) { 18 | return build(0, count); 19 | } 20 | 21 | public static Pageable forLists(int page) { 22 | return build(page - 1, 10); 23 | } 24 | 25 | public static Pageable forDashboard(int page) { 26 | return build(page - 1, 30); 27 | } 28 | 29 | public static Pageable forFeeds() { 30 | return build(0, 20); 31 | } 32 | 33 | public static Pageable forSearch(int page) { 34 | return new PageRequest(page - 1, 10); 35 | } 36 | 37 | private static Pageable build(int page, int pageSize) { 38 | return new PageRequest(page, pageSize, Sort.Direction.DESC, "publishAt"); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /sagan-site/src/main/java/sagan/support/nav/Section.java: -------------------------------------------------------------------------------- 1 | package sagan.support.nav; 2 | 3 | /** 4 | * Used in conjunction with {@link sagan.support.nav.Navigation} to indicate which section 5 | * of the site-wide navigation is currently active and highlighted. 6 | */ 7 | public enum Section { 8 | DOCS, GUIDES, PROJECTS, BLOG 9 | } 10 | -------------------------------------------------------------------------------- /sagan-site/src/main/resources/META-INF/spring-devtools.properties: -------------------------------------------------------------------------------- 1 | restart.include.modelmapper=/modelmapper-(.*).jar -------------------------------------------------------------------------------- /sagan-site/src/main/resources/badge/Verdana.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-site/src/main/resources/badge/Verdana.ttf -------------------------------------------------------------------------------- /sagan-site/src/main/resources/badge/milestone.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /sagan-site/src/main/resources/badge/release.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /sagan-site/src/main/resources/badge/snapshot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /sagan-site/src/main/resources/db/migration/V2__projectpages.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE project ADD raw_boot_config VARCHAR; 2 | ALTER TABLE project ADD rendered_boot_config VARCHAR; 3 | 4 | ALTER TABLE project ADD raw_overview VARCHAR DEFAULT ''; 5 | ALTER TABLE project ADD rendered_overview VARCHAR DEFAULT ''; 6 | 7 | ALTER TABLE project DROP COLUMN is_aggregator; 8 | ALTER TABLE project ADD parent_project_id CHARACTER VARYING(255) DEFAULT NULL; 9 | 10 | ALTER TABLE project ADD display_order INT NOT NULL DEFAULT 255; 11 | 12 | CREATE TABLE project_sample_list ( 13 | title VARCHAR, 14 | description VARCHAR, 15 | url VARCHAR, 16 | display_order INT NOT NULL, 17 | project_id CHARACTER VARYING(255) NOT NULL, 18 | PRIMARY KEY (project_id, display_order) 19 | ); -------------------------------------------------------------------------------- /sagan-site/src/main/resources/db/migration/V3__springtools.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE spring_tools_platform 2 | ( 3 | id CHARACTER VARYING(255) NOT NULL PRIMARY KEY 4 | ); 5 | 6 | CREATE TABLE spring_tools_platform_downloads 7 | ( 8 | spring_tools_platform_id CHARACTER VARYING(255) NOT NULL, 9 | download_url CHARACTER VARYING(255) NOT NULL, 10 | variant CHARACTER VARYING(255) NOT NULL, 11 | label CHARACTER VARYING(255) NOT NULL, 12 | PRIMARY KEY (spring_tools_platform_id, variant) 13 | ); -------------------------------------------------------------------------------- /sagan-site/src/main/resources/db/migration/V4__projects.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE project ADD tag_line CHARACTER VARYING(255) DEFAULT ''; 2 | ALTER TABLE project ADD featured BOOLEAN; 3 | ALTER TABLE project ADD image_path CHARACTER VARYING(255) DEFAULT ''; 4 | 5 | UPDATE project SET featured = FALSE WHERE featured IS NULL; 6 | 7 | -- create a new groups reference table 8 | CREATE TABLE project_groups 9 | ( 10 | id SERIAL NOT NULL PRIMARY KEY, 11 | name VARCHAR(255) UNIQUE, 12 | label VARCHAR(255) 13 | ); 14 | 15 | -- relation table between project and project_groups tables 16 | create table project_groups_rel 17 | ( 18 | project_id VARCHAR(255) REFERENCES project (id), 19 | group_id INT REFERENCES project_groups (id), 20 | CONSTRAINT id PRIMARY KEY (project_id, group_id) 21 | ); -------------------------------------------------------------------------------- /sagan-site/src/main/resources/db/migration/V6__tools_version.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- Add version column to springtools 3 | -- 4 | ALTER TABLE spring_tools_platform_downloads 5 | ADD COLUMN version CHARACTER VARYING(64) DEFAULT ''; -------------------------------------------------------------------------------- /sagan-site/src/main/resources/elasticsearch/mappings/blogpost.json: -------------------------------------------------------------------------------- 1 | { 2 | "blogpost": { 3 | "properties": { 4 | "path": { 5 | "type": "string", 6 | "store": "yes", 7 | "index": "no" 8 | }, 9 | "title": { 10 | "type": "string", 11 | "store": "yes", 12 | "analyzer": "simple" 13 | }, 14 | "subTitle": { 15 | "type": "string", 16 | "store": "yes", 17 | "index": "no" 18 | }, 19 | "summary": { 20 | "type": "string", 21 | "store": "yes", 22 | "index": "no" 23 | }, 24 | "rawContent": { 25 | "type": "string", 26 | "store": "yes", 27 | "term_vector": "with_positions_offsets", 28 | "analyzer": "fulltext" 29 | }, 30 | "facetPaths": { 31 | "type": "string", 32 | "store": "no", 33 | "index": "not_analyzed" 34 | }, 35 | "publishAt": { 36 | "type": "date", 37 | "store": "yes", 38 | "index": "not_analyzed" 39 | }, 40 | "author": { 41 | "type": "string", 42 | "store": "yes", 43 | "index": "not_analyzed" 44 | } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /sagan-site/src/main/resources/elasticsearch/mappings/guidedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "guidedoc": { 3 | "properties": { 4 | "path": { 5 | "type": "string", 6 | "store": "yes", 7 | "index": "no" 8 | }, 9 | "title": { 10 | "type": "string", 11 | "store": "yes", 12 | "analyzer": "simple" 13 | }, 14 | "subTitle": { 15 | "type": "string", 16 | "store": "yes", 17 | "index": "no" 18 | }, 19 | "summary": { 20 | "type": "string", 21 | "store": "yes", 22 | "index": "no" 23 | }, 24 | "rawContent": { 25 | "type": "string", 26 | "store": "yes", 27 | "term_vector": "with_positions_offsets", 28 | "analyzer": "fulltext" 29 | }, 30 | "facetPaths": { 31 | "type": "string", 32 | "store": "no", 33 | "index": "not_analyzed" 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /sagan-site/src/main/resources/elasticsearch/mappings/projectpage.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectpage": { 3 | "properties": { 4 | "path": { 5 | "type": "string", 6 | "store": "yes", 7 | "index": "no" 8 | }, 9 | "title": { 10 | "type": "string", 11 | "store": "yes", 12 | "analyzer": "simple" 13 | }, 14 | "subTitle": { 15 | "type": "string", 16 | "store": "yes", 17 | "index": "no" 18 | }, 19 | "summary": { 20 | "type": "string", 21 | "store": "yes", 22 | "index": "no" 23 | }, 24 | "rawContent": { 25 | "type": "string", 26 | "store": "yes", 27 | "term_vector": "with_positions_offsets", 28 | "analyzer": "fulltext" 29 | }, 30 | "facetPaths": { 31 | "type": "string", 32 | "store": "no", 33 | "index": "not_analyzed" 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /sagan-site/src/main/resources/elasticsearch/mappings/referencedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "referencedoc": { 3 | "properties": { 4 | "path": { 5 | "type": "string", 6 | "store": "yes", 7 | "index": "no" 8 | }, 9 | "title": { 10 | "type": "string", 11 | "store": "yes", 12 | "analyzer": "simple" 13 | }, 14 | "subTitle": { 15 | "type": "string", 16 | "store": "yes", 17 | "index": "no" 18 | }, 19 | "summary": { 20 | "type": "string", 21 | "store": "yes", 22 | "index": "no" 23 | }, 24 | "rawContent": { 25 | "type": "string", 26 | "store": "yes", 27 | "term_vector": "with_positions_offsets", 28 | "analyzer": "fulltext" 29 | }, 30 | "facetPaths": { 31 | "type": "string", 32 | "store": "no", 33 | "index": "not_analyzed" 34 | }, 35 | "current": { 36 | "type": "boolean", 37 | "store": "yes", 38 | "index": "not_analyzed" 39 | }, 40 | "version": { 41 | "type": "string", 42 | "store": "yes", 43 | "index": "not_analyzed" 44 | }, 45 | "projectId": { 46 | "type": "string", 47 | "store": "yes", 48 | "index": "not_analyzed" 49 | } 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /sagan-site/src/main/resources/elasticsearch/mappings/sitepage.json: -------------------------------------------------------------------------------- 1 | { 2 | "sitepage": { 3 | "properties": { 4 | "path": { 5 | "type": "string", 6 | "store": "yes", 7 | "index": "no" 8 | }, 9 | "title": { 10 | "type": "string", 11 | "store": "yes", 12 | "analyzer": "simple" 13 | }, 14 | "subTitle": { 15 | "type": "string", 16 | "store": "yes", 17 | "index": "no" 18 | }, 19 | "summary": { 20 | "type": "string", 21 | "store": "yes", 22 | "index": "no" 23 | }, 24 | "rawContent": { 25 | "type": "string", 26 | "store": "yes", 27 | "term_vector": "with_positions_offsets", 28 | "analyzer": "fulltext" 29 | }, 30 | "facetPaths": { 31 | "type": "string", 32 | "store": "no", 33 | "index": "not_analyzed" 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /sagan-site/src/main/resources/elasticsearch/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "index.analysis.analyzer.fulltext.tokenizer": "standard", 3 | "index.analysis.analyzer.fulltext.char_filter.0": "html_strip", 4 | "index.analysis.analyzer.fulltext.filter.0": "standard", 5 | "index.analysis.analyzer.fulltext.filter.1": "stop", 6 | "index.analysis.analyzer.fulltext.filter.2": "lowercase", 7 | "index.analysis.analyzer.fulltext.filter.3": "asciifolding", 8 | "index.analysis.analyzer.fulltext.filter.4": "porter_stem", 9 | "index.analysis.analyzer.code_autocomplete.tokenizer": "code_ngram", 10 | "index.analysis.tokenizer.code_ngram.type": "edgeNGram", 11 | "index.analysis.tokenizer.code_ngram.min_gram": 2, 12 | "index.analysis.tokenizer.code_ngram.max_gram": 20 13 | } -------------------------------------------------------------------------------- /sagan-site/src/main/resources/templates/admin/blog/edit.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | Admin | Blog | [[${post.title}]] 6 | 7 | 8 | 15 |
16 |
18 |
19 | 20 | 21 |
22 | 23 |
24 |
25 |
26 | 27 | -------------------------------------------------------------------------------- /sagan-site/src/main/resources/templates/admin/blog/new.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | Admin | Blog | New Blog Post 6 | 7 | 8 | 15 |
16 |
18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /sagan-site/src/main/resources/templates/admin/show.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | Admin 8 | 9 | 10 |
11 |
12 |
13 |
14 |

15 | Welcome, Emily 16 |

17 |

18 | to the spring.io admin website. 19 |

20 |
21 |
22 |
23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /sagan-site/src/main/resources/templates/blog/_right-pane.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 |
7 |
8 |
9 |

Get the Spring newsletter

10 | 11 |
12 | 21 |
22 |
23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /sagan-site/src/main/resources/templates/error.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | Error 6 | 7 | 8 |

Unexpected Error

9 | 10 |
11 |
12 | There was an unexpected error on the server: Error (888). 13 |
14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /sagan-site/src/main/resources/templates/error/404.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | 404 6 | 7 | 8 | 9 |
10 |
11 |
12 |

404

13 |

Oops! The page can’t be found.

14 |

Go to the homepage.

15 |
16 |
17 |
18 | 19 | -------------------------------------------------------------------------------- /sagan-site/src/main/resources/templates/error/500.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | Server Error 6 | 7 | 8 |
9 | 10 |
11 |
12 |
13 | 14 |
Internal server error
15 | 21 | 22 | @springcentral 23 | 24 |
25 |
26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /sagan-site/src/main/resources/templates/pages/signin.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | Sign In 6 | 7 | 8 |
9 | 31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /sagan-site/src/main/resources/templates/projects/_project_links.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /sagan-site/src/main/resources/templates/projects/sidebar.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | $Title$ 6 | 7 | 8 | $END$ 9 | 10 | -------------------------------------------------------------------------------- /sagan-site/src/main/resources/templates/search/searcherror.html: -------------------------------------------------------------------------------- 1 | 4 | 5 | Search 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 |
14 |
15 |

Search Error

16 |

17 | Your application may not be properly configured for search.
18 | You should try to enable search locally or 19 | on your Cloud Foundry hosted application. 20 |

21 |
22 |
23 |
24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /sagan-site/src/main/resources/templates/understanding/show.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | Understanding [[${doc.subject}]] 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /sagan-site/src/test/java/sagan/site/blog/PostCategoryFormatterTests.java: -------------------------------------------------------------------------------- 1 | package sagan.site.blog; 2 | 3 | import java.text.ParseException; 4 | 5 | import org.junit.Test; 6 | 7 | import static org.hamcrest.MatcherAssert.assertThat; 8 | import static org.hamcrest.Matchers.equalTo; 9 | 10 | public class PostCategoryFormatterTests { 11 | private PostCategoryFormatter formatter = new PostCategoryFormatter(); 12 | 13 | @Test 14 | public void itConvertsUrlSlugStringsToPostCategories() throws ParseException { 15 | assertThat(formatter.parse(PostCategory.ENGINEERING.getUrlSlug(), null), equalTo(PostCategory.ENGINEERING)); 16 | } 17 | 18 | @Test 19 | public void itConvertsEnumNameStringsToPostCategories() throws ParseException { 20 | assertThat(formatter.parse(PostCategory.ENGINEERING.name(), null), equalTo(PostCategory.ENGINEERING)); 21 | } 22 | 23 | @Test 24 | public void itPrintsAStringThatCanBeParsed() throws ParseException { 25 | assertThat(formatter.parse(formatter.print(PostCategory.ENGINEERING, null), null), 26 | equalTo(PostCategory.ENGINEERING)); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /sagan-site/src/test/java/sagan/site/blog/SetSystemProperty.java: -------------------------------------------------------------------------------- 1 | package sagan.site.blog; 2 | 3 | import org.junit.rules.ExternalResource; 4 | 5 | public class SetSystemProperty extends ExternalResource { 6 | public String originalValue; 7 | private String key; 8 | private String value; 9 | 10 | public SetSystemProperty(String key, String value) { 11 | this.key = key; 12 | this.value = value; 13 | } 14 | 15 | @Override 16 | protected void before() throws Throwable { 17 | originalValue = System.getProperty(key); 18 | System.setProperty(key, value); 19 | } 20 | 21 | @Override 22 | protected void after() { 23 | if (originalValue == null) { 24 | System.clearProperty(key); 25 | } else { 26 | System.setProperty(key, originalValue); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /sagan-site/src/test/java/sagan/site/blog/support/BlogController_ShowTests.java: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-site/src/test/java/sagan/site/blog/support/BlogController_ShowTests.java -------------------------------------------------------------------------------- /sagan-site/src/test/java/sagan/site/team/support/GeoLocationFormatterTests.java: -------------------------------------------------------------------------------- 1 | package sagan.site.team.support; 2 | 3 | import sagan.site.team.GeoLocation; 4 | 5 | import java.text.ParseException; 6 | 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | 10 | import static org.hamcrest.MatcherAssert.assertThat; 11 | import static org.hamcrest.Matchers.equalTo; 12 | 13 | public class GeoLocationFormatterTests { 14 | private GeoLocationFormatter formatter; 15 | 16 | @Before 17 | public void setup() { 18 | formatter = new GeoLocationFormatter(); 19 | } 20 | 21 | @Test 22 | public void testParse() throws Exception { 23 | assertLatLon("1,1", 1f, 1f); 24 | assertLatLon("1.1,1.1", 1.1f, 1.1f); 25 | assertLatLon("-90.0,-180", -90f, -180f); 26 | assertLatLon("1.1 , 1.1", 1.1f, 1.1f); 27 | } 28 | 29 | @Test(expected = ParseException.class) 30 | public void testNoParse() throws Exception { 31 | formatter.parse("afslk", null); 32 | } 33 | 34 | private void assertLatLon(String latLon, float lat, float lon) throws ParseException { 35 | GeoLocation location = formatter.parse(latLon, null); 36 | assertThat(location.getLatitude(), equalTo(lat)); 37 | assertThat(location.getLongitude(), equalTo(lon)); 38 | } 39 | 40 | @Test 41 | public void testPrint() throws Exception { 42 | assertThat(formatter.print(new GeoLocation(-10.3f, 87.42f), null), equalTo("-10.300000,87.419998")); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /sagan-site/src/test/java/sagan/site/webapi/ConstrainedFields.java: -------------------------------------------------------------------------------- 1 | package sagan.site.webapi; 2 | 3 | import org.springframework.restdocs.constraints.ConstraintDescriptions; 4 | import org.springframework.restdocs.payload.FieldDescriptor; 5 | import org.springframework.util.StringUtils; 6 | 7 | import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; 8 | import static org.springframework.restdocs.snippet.Attributes.key; 9 | 10 | public class ConstrainedFields { 11 | 12 | private final ConstraintDescriptions constraintDescriptions; 13 | 14 | public static ConstrainedFields constraintsOn(Class input) { 15 | return new ConstrainedFields(input); 16 | } 17 | 18 | public ConstrainedFields(Class input) { 19 | this.constraintDescriptions = new ConstraintDescriptions(input); 20 | } 21 | 22 | public FieldDescriptor withPath(String path) { 23 | return fieldWithPath(path).attributes(key("constraints").value(StringUtils 24 | .collectionToDelimitedString(this.constraintDescriptions 25 | .descriptionsForProperty(path), ". "))); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /sagan-site/src/test/java/sagan/site/webapi/MvcTestConfig.java: -------------------------------------------------------------------------------- 1 | package sagan.site.webapi; 2 | 3 | import org.springframework.boot.test.context.TestConfiguration; 4 | import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer; 5 | import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; 6 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 7 | 8 | /** 9 | * Test Spring MVC configuration 10 | */ 11 | @TestConfiguration 12 | public class MvcTestConfig extends WebMvcConfigurerAdapter { 13 | 14 | @Override 15 | public void configurePathMatch(PathMatchConfigurer matcher) { 16 | matcher.setUseSuffixPatternMatch(false); 17 | } 18 | 19 | @Override 20 | public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { 21 | configurer.favorPathExtension(false); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /sagan-site/src/test/java/sagan/site/webapi/WebApiTest.java: -------------------------------------------------------------------------------- 1 | package sagan.site.webapi; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | import sagan.ModelMapperConfig; 9 | 10 | import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; 11 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 12 | import org.springframework.context.annotation.Import; 13 | import org.springframework.core.annotation.AliasFor; 14 | 15 | /** 16 | * 17 | */ 18 | @Target(ElementType.TYPE) 19 | @Retention(RetentionPolicy.RUNTIME) 20 | @AutoConfigureRestDocs(outputDir = "build/generated-snippets", uriScheme = "https", uriHost = "spring.io", uriPort = 443) 21 | @WebMvcTest 22 | @Import({ModelMapperConfig.class, MvcTestConfig.class}) 23 | public @interface WebApiTest { 24 | 25 | @AliasFor(annotation = WebMvcTest.class) 26 | Class[] value() default {}; 27 | 28 | } 29 | -------------------------------------------------------------------------------- /sagan-site/src/test/java/sagan/support/DateTestUtils.java: -------------------------------------------------------------------------------- 1 | package sagan.support; 2 | 3 | import java.text.ParseException; 4 | import java.text.SimpleDateFormat; 5 | import java.util.Date; 6 | 7 | public class DateTestUtils { 8 | 9 | private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm"); 10 | 11 | public static Date getDate(String dateString) { 12 | try { 13 | return DATE_FORMAT.parse(dateString); 14 | } catch (ParseException e) { 15 | throw new RuntimeException(e); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /sagan-site/src/test/java/sagan/support/Fixtures.java: -------------------------------------------------------------------------------- 1 | package sagan.support; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.nio.charset.Charset; 6 | 7 | import org.springframework.core.io.ClassPathResource; 8 | import org.springframework.util.StreamUtils; 9 | 10 | public class Fixtures { 11 | 12 | public static String load(String path) { 13 | try { 14 | InputStream stream = new ClassPathResource(path, Fixtures.class).getInputStream(); 15 | return StreamUtils.copyToString(stream, Charset.forName("UTF-8")); 16 | } catch (IOException e) { 17 | throw new RuntimeException(e); 18 | } 19 | } 20 | 21 | public static byte[] loadData(String path) { 22 | try { 23 | InputStream stream = new ClassPathResource(path, Fixtures.class).getInputStream(); 24 | return StreamUtils.copyToByteArray(stream); 25 | } catch (IOException e) { 26 | throw new RuntimeException(e); 27 | } 28 | } 29 | 30 | public static String githubRepoJson() { 31 | return load("/fixtures/github/githubRepo.json"); 32 | } 33 | 34 | public static String githubRepoListJson() { 35 | return load("/fixtures/github/githubRepoList.json"); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /sagan-site/src/test/resources/fixtures/github/ghUserProfile-asmith.json: -------------------------------------------------------------------------------- 1 | { 2 | "login": "asmith", 3 | "id": 987, 4 | "avatar_url": "https://example.com/image.png", 5 | "gravatar_id": "098754321acbe", 6 | "url": "https://api.github.com/users/asmith", 7 | "name": "Adam Smith", 8 | "company": "Spring", 9 | "blog": "https://spring.io/blog", 10 | "location": "Antartica", 11 | "email": "asmith@example.com", 12 | "hireable": false, 13 | "bio": "Hi there", 14 | "public_repos": 2, 15 | "public_gists": 1, 16 | "followers": 20, 17 | "following": 0, 18 | "html_url": "https://github.com/asmith", 19 | "created_at": "2008-01-14T04:33:35Z", 20 | "type": "User", 21 | "plan": { 22 | "name": "free", 23 | "space": 307200, 24 | "collaborators": 0, 25 | "private_repos": 0 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /sagan-site/src/test/resources/fixtures/github/ghUserProfile-jdoe.json: -------------------------------------------------------------------------------- 1 | { 2 | "login": "jdoe", 3 | "id": 123, 4 | "avatar_url": "https://example.com/image.png", 5 | "gravatar_id": "123456abcde", 6 | "url": "https://api.github.com/users/jdoe", 7 | "name": "John Doe", 8 | "company": "Spring", 9 | "blog": "https://spring.io/blog", 10 | "location": "Antartica", 11 | "email": "jdoe@example.com", 12 | "hireable": false, 13 | "bio": "Hi there", 14 | "public_repos": 2, 15 | "public_gists": 1, 16 | "followers": 20, 17 | "following": 0, 18 | "html_url": "https://github.com/jdoe", 19 | "created_at": "2008-01-14T04:33:35Z", 20 | "type": "User", 21 | "plan": { 22 | "name": "free", 23 | "space": 307200, 24 | "collaborators": 0, 25 | "private_repos": 0 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /sagan-site/src/test/resources/fixtures/images/testImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RameshMF/sagan/46cf738855c745a11961ddc3fee67bd5d7c9755e/sagan-site/src/test/resources/fixtures/images/testImage.png -------------------------------------------------------------------------------- /sagan-site/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /sagan-site/src/test/resources/org/springframework/restdocs/templates/request-fields.snippet: -------------------------------------------------------------------------------- 1 | |=== 2 | |Path|Type|Description|Constraints 3 | 4 | {{#fields}} 5 | |{{path}} 6 | |{{type}} 7 | |{{description}} 8 | |{{constraints}} 9 | 10 | {{/fields}} 11 | |=== -------------------------------------------------------------------------------- /sagan-site/src/test/resources/sagan/site/events/html-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "calendar#events", 3 | "etag": "\"etagvalue\"", 4 | "summary": "Spring Events Shared Calendar", 5 | "updated": "2020-06-09T00:18:44.279Z", 6 | "timeZone": "America/Los_Angeles", 7 | "accessRole": "reader", 8 | "defaultReminders": [], 9 | "nextSyncToken": "some-token", 10 | "items": [ 11 | { 12 | "kind": "calendar#event", 13 | "etag": "\"etagvalue\"", 14 | "id": "some-id-1", 15 | "status": "confirmed", 16 | "htmlLink": "https://www.google.com/calendar/event?eid=some-id-1", 17 | "created": "2020-05-27T00:16:23.000Z", 18 | "updated": "2020-05-27T00:16:23.479Z", 19 | "summary": "Spring Live", 20 | "description": "\u003ca href=\"https://events.example.org/event.html\"\u003ehttps://events.example.org/event.html\u003c/a\u003e", 21 | "location": "Virtual", 22 | "creator": { 23 | "email": "user@example.org" 24 | }, 25 | "organizer": { 26 | "email": "organizer@example.com", 27 | "displayName": "Spring Events Shared Calendar", 28 | "self": true 29 | }, 30 | "start": { 31 | "dateTime": "2020-03-19T09:00:00-05:00" 32 | }, 33 | "end": { 34 | "dateTime": "2020-03-21T18:00:00-05:00" 35 | }, 36 | "iCalUID": "some-id-1@google.com", 37 | "sequence": 0 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /sagan-site/src/test/resources/sagan/site/events/invalid.json: -------------------------------------------------------------------------------- 1 | INVALID CALENDAR -------------------------------------------------------------------------------- /sagan-site/src/test/resources/sagan/site/events/single-event.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "calendar#events", 3 | "etag": "\"etagvalue\"", 4 | "summary": "Spring Events Shared Calendar", 5 | "updated": "2020-06-09T00:18:44.279Z", 6 | "timeZone": "America/Los_Angeles", 7 | "accessRole": "reader", 8 | "defaultReminders": [], 9 | "nextSyncToken": "some-token", 10 | "items": [ 11 | { 12 | "kind": "calendar#event", 13 | "etag": "\"etagvalue\"", 14 | "id": "some-id-1", 15 | "status": "confirmed", 16 | "htmlLink": "https://www.google.com/calendar/event?eid=some-id-1", 17 | "created": "2020-05-27T00:16:23.000Z", 18 | "updated": "2020-05-27T00:16:23.479Z", 19 | "summary": "Spring IO conference", 20 | "description": "https://springio.net", 21 | "location": "Barcelona, Spain", 22 | "creator": { 23 | "email": "user@example.org" 24 | }, 25 | "organizer": { 26 | "email": "organizer@example.com", 27 | "displayName": "Spring Events Shared Calendar", 28 | "self": true 29 | }, 30 | "start": { 31 | "dateTime": "2020-05-14T09:00:00+01:00" 32 | }, 33 | "end": { 34 | "dateTime": "2020-05-16T18:00:00+01:00" 35 | }, 36 | "iCalUID": "some-id-1@google.com", 37 | "sequence": 0 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /sagan-site/src/test/resources/sagan/site/guides/rest-service.json: -------------------------------------------------------------------------------- 1 | { 2 | "@class": "sagan.site.guides.GettingStartedGuide", 3 | "header": { 4 | "@class": "sagan.site.guides.DefaultGuideHeader", 5 | "name": "rest-service", 6 | "repositoryName": "spring-guides/gs-rest-service", 7 | "title": "Rest Service Title", 8 | "description": "Description", 9 | "githubUrl": "https://github.com/spring-guides/gs-rest-service", 10 | "gitUrl": "git://github.com/spring-guides/gs-rest-service.git", 11 | "sshUrl": "git@github.com:spring-guides/gs-rest-service.git", 12 | "cloneUrl": "https://github.com/spring-guides/gs-rest-service.git", 13 | "projects": [ 14 | "spring-boot" 15 | ] 16 | }, 17 | "content": "CONTENT", 18 | "tableOfContents": "TOC", 19 | "typeLabel": "Getting Started", 20 | "images": [ 21 | { 22 | "name": "image.jpg", 23 | "encodedContent": "encodedContent" 24 | } 25 | ], 26 | "pushToPwsUrl": null 27 | } -------------------------------------------------------------------------------- /sagan-site/src/test/resources/sagan/site/renderer/messaging-redis-content.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "messaging-redis", 3 | "tableOfContents": "", 4 | "content": "
\n

TThis guide walks you through the process of using Spring Data Redis to publish and subscribe to messages sent via Redis.

\n
", 5 | "pushToPwsMetadata": "repository: https://github.com/spring-guides/gs-rest-service.git\ndirectory: complete\npath: /test", 6 | "images": [ 7 | { 8 | "name": "hello.png", 9 | "encodedContent": "base64ImageContent" 10 | } 11 | ], 12 | "_links": { 13 | "self": { 14 | "href": "/guides/getting-started/rest-service/content" 15 | }, 16 | "guide": { 17 | "href": "/guides/getting-started/rest-service" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /sagan-site/src/test/resources/sagan/site/renderer/messaging-redis.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "messaging-redis", 3 | "repositoryName": "spring-guides/gs-messaging-redis", 4 | "title": "Messaging with Redis", 5 | "description": "Learn how to use Redis as a message broker.", 6 | "type": "getting-started", 7 | "githubUrl": "https://github.com/spring-guides/gs-messaging-redis", 8 | "gitUrl": "git://github.com/spring-guides/gs-messaging-redis.git", 9 | "sshUrl": "git@github.com:spring-guides/gs-messaging-redis.git", 10 | "cloneUrl": "https://github.com/spring-guides/gs-messaging-redis.git", 11 | "projects": [ 12 | 13 | ], 14 | "_links": { 15 | "self": { 16 | "href": "/guides/getting-started/messaging-redis" 17 | }, 18 | "content": { 19 | "href": "/guides/getting-started/messaging-redis/content" 20 | }, 21 | "guides": { 22 | "href": "/guides/" 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /sagan-site/src/test/resources/sagan/site/renderer/rest-service-content.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rest-service", 3 | "tableOfContents": "", 4 | "content": "
\n

This guide walks you through the process of creating a \"hello world\" RESTful web service with Spring.

\n
", 5 | "pushToPwsMetadata": null, 6 | "images": null, 7 | "_links": { 8 | "self": { 9 | "href": "/guides/getting-started/rest-service/content" 10 | }, 11 | "guide": { 12 | "href": "/guides/getting-started/rest-service" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /sagan-site/src/test/resources/sagan/site/renderer/rest-service.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rest-service", 3 | "repositoryName": "spring-guides/gs-rest-service", 4 | "title": "Building a RESTful Web Service", 5 | "description": "Learn how to create a RESTful web service with Spring.", 6 | "type": "getting-started", 7 | "githubUrl": "https://github.com/spring-guides/gs-rest-service", 8 | "gitUrl": "git://github.com/spring-guides/gs-rest-service.git", 9 | "sshUrl": "git@github.com:spring-guides/gs-rest-service.git", 10 | "cloneUrl": "https://github.com/spring-guides/gs-rest-service.git", 11 | "projects": [ 12 | "spring-boot" 13 | ], 14 | "_links": { 15 | "self": { 16 | "href": "/guides/getting-started/rest-service" 17 | }, 18 | "content": { 19 | "href": "/guides/getting-started/rest-service/content" 20 | }, 21 | "guides": { 22 | "href": "/guides/" 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /sagan-site/src/test/resources/sagan/site/renderer/root.json: -------------------------------------------------------------------------------- 1 | { 2 | "_links": { 3 | "markup": { 4 | "href": "/documents" 5 | }, 6 | "guides": { 7 | "href": "/guides/" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'sagan' 2 | 3 | include 'sagan-site', 'sagan-client', 'sagan-renderer' -------------------------------------------------------------------------------- /style/README.md: -------------------------------------------------------------------------------- 1 | This directory contains configuration files for source code formatting. Everything is based on Eclipse's formatting and import order rules. IDEA users can work directly with these files using the [Eclipse code formatter plugin for IDEA][1] 2 | 3 | [1]: https://code.google.com/p/eclipse-code-formatter-intellij-plugin/wiki/HowTo 4 | -------------------------------------------------------------------------------- /style/sagan.importorder: -------------------------------------------------------------------------------- 1 | #Organize Import Order 2 | #Wed Mar 12 15:16:07 CET 2014 3 | 6=com 4 | 5=io 5 | 4=org.springframework 6 | 3=org 7 | 2=javax 8 | 1=java 9 | 0=sagan 10 | 7=\# 11 | -------------------------------------------------------------------------------- /util/README.md: -------------------------------------------------------------------------------- 1 | This directory contains various utility scripts that have been useful in the development and operation of the spring.io site and sagan repository. -------------------------------------------------------------------------------- /util/bisect.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | BAD=962958c 3 | GOOD=b742859 4 | git bisect start $BAD $GOOD -- 5 | git bisect run ./gradlew clean build -x gitProperties 6 | git bisect reset 7 | -------------------------------------------------------------------------------- /util/convert-tabs-to-spaces.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | git ls-files | xargs file | grep text | cut -d":" -f1 | xargs -Ifile tab2space -lf file file 3 | -------------------------------------------------------------------------------- /util/replicate-db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ $# != 2 ]; then cat << EOM 4 | usage: $0 DB_URL_SOURCE DB_URL_DESTINATION 5 | EOM 6 | exit 7 | fi 8 | 9 | echo "Creating dump from $1" 10 | pg_dump -a -O $1 > /tmp/dump.sql 11 | 12 | echo "Replicating into $2" 13 | psql $2 -c "truncate memberprofile restart identity cascade" 14 | psql $2 -c "truncate databasechangelog" 15 | psql $2 -c "truncate databasechangeloglock" 16 | psql $2 < /tmp/dump.sql 17 | -------------------------------------------------------------------------------- /util/test-gh-pages-webhook.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Poor man's test until this can be more properly run in an integration test 4 | # without actually creating new GH issues every time. 5 | # 6 | # Run from root of project. 7 | curl -X POST -d @src/test/resources/fixtures/github/ghPagesWebhook.json http://localhost:8080/webhook/gh-pages/default 8 | --------------------------------------------------------------------------------