diff --git a/.travis.yml b/.travis.yml index f71db5e..bf34de0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,6 @@ language: java +dist: trusty +sudo: required jdk: - oraclejdk7 deploy: diff --git a/build.gradle b/build.gradle index fcf904b..a16277c 100644 --- a/build.gradle +++ b/build.gradle @@ -65,8 +65,8 @@ subprojects { dependencyManagement { imports { - mavenBom 'org.springframework.cloud:spring-cloud-starter-parent:Camden.SR4' - mavenBom 'org.springframework.boot:spring-boot-starter-parent:1.4.3.RELEASE' + mavenBom 'org.springframework.cloud:spring-cloud-starter-parent:Camden.SR6' + mavenBom 'org.springframework.boot:spring-boot-starter-parent:1.5.2.RELEASE' } } @@ -77,6 +77,10 @@ subprojects { all*.exclude module: "spring-boot-starter-tomcat" } + test { + testLogging.showStandardStreams = true + } + task sourcesJar(type: Jar, dependsOn: classes) { classifier = 'sources' from sourceSets.main.allSource diff --git a/rcloud-gist-service/build.gradle b/rcloud-gist-service/build.gradle index f2cfebb..961b0bc 100644 --- a/rcloud-gist-service/build.gradle +++ b/rcloud-gist-service/build.gradle @@ -5,12 +5,6 @@ * *******************************************************************************/ -repositories { - maven { - url "http://gitblit.github.io/gitblit-maven/" - } -} - project.description = "RCloud Gist service implementation on a git repository." dependencies { @@ -37,6 +31,10 @@ dependencies { testCompile 'junit:junit' testCompile 'org.mockito:mockito-all:1.10.8' testCompile 'org.springframework.boot:spring-boot-starter-test' + testCompile 'org.springframework.security:spring-security-test' + testCompile 'org.apache.commons:commons-math3:3.6.1' + + } diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/CacheConfiguration.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/CacheConfiguration.java index d1def8f..d057e3b 100644 --- a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/CacheConfiguration.java +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/CacheConfiguration.java @@ -59,10 +59,8 @@ private void createAndApplyCacheConfiguration(HazelcastInstance hazelcastInstanc logger.warn("Altering existing configuration of cache config {}", mapConfigs.get(cacheName)); } MapConfig mapConfig = new MapConfig(cacheName);//config.getMapConfig(cacheConfig.getName()); - mapConfig.setEvictionPercentage(cacheConfig.getEvictionPercentage()); mapConfig.setEvictionPolicy(cacheConfig.getEvictionPolicy()); mapConfig.setTimeToLiveSeconds(cacheConfig.getTtl()); - mapConfig.setMinEvictionCheckMillis(cacheConfig.getEvictionCheck()); config.addMapConfig(mapConfig); logger.info("Configured cache {} with with settings: {}", cacheName, mapConfig); } diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/CacheConfigurationProperties.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/CacheConfigurationProperties.java index 31df5b4..d31746e 100644 --- a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/CacheConfigurationProperties.java +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/CacheConfigurationProperties.java @@ -28,10 +28,8 @@ public void setCaches(List caches) { public static class GistCacheConfiguration { private String name; - private int evictionPercentage = 25; private EvictionPolicy evictionPolicy = EvictionPolicy.LFU; private int ttl = 300; - private int evictionCheck = 500; public String getName() { return name; @@ -39,12 +37,6 @@ public String getName() { public void setName(String name) { this.name = name; } - public int getEvictionPercentage() { - return evictionPercentage; - } - public void setEvictionPercentage(int evictionPercentage) { - this.evictionPercentage = evictionPercentage; - } public EvictionPolicy getEvictionPolicy() { return evictionPolicy; } @@ -57,13 +49,6 @@ public int getTtl() { public void setTtl(int ttl) { this.ttl = ttl; } - public int getEvictionCheck() { - return evictionCheck; - } - public void setEvictionCheck(int evictionCheck) { - this.evictionCheck = evictionCheck; - } - } diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/ContentNegotiationConfiguration.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/ContentNegotiationConfiguration.java new file mode 100644 index 0000000..d16375b --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/ContentNegotiationConfiguration.java @@ -0,0 +1,81 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; +import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; + +@Configuration +@EnableConfigurationProperties(GistServiceProperties.class) +public class ContentNegotiationConfiguration extends WebMvcConfigurerAdapter { + + private static final List DEFAULT_GITHUB_MEDIA_TYPES = Arrays.asList( + MediaType.parseMediaType("application/vnd.github.beta+json"), + MediaType.parseMediaType("application/vnd.github.beta"), + MediaType.parseMediaType("application/vnd.github.v3+json"), + MediaType.parseMediaType("application/vnd.github.v3") + ); + + @Autowired + private GistServiceProperties serviceProperties; + + @Override + public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { + configurer.ignoreAcceptHeader(true); + configurer.defaultContentType(MediaType.APPLICATION_JSON_UTF8); + } + + @Override + public void extendMessageConverters(List> converters) { + super.extendMessageConverters(converters); + for (HttpMessageConverter converter : converters) { + if (isJsonConverter(converter)) { + updateMediaTypes((AbstractJackson2HttpMessageConverter) converter); + } + } + } + + private void updateMediaTypes(AbstractJackson2HttpMessageConverter converter) { + Collection gistMediaTypes = getMediaTypes(converter); + List mediaTypes = new ArrayList<>(gistMediaTypes); + converter.setSupportedMediaTypes(mediaTypes); + } + + private Collection getMediaTypes(AbstractJackson2HttpMessageConverter converter) { + Set mediaTypes = new LinkedHashSet<>(converter.getSupportedMediaTypes()); + for(String mediaType: serviceProperties.getMediatypes()) { + mediaTypes.add(MediaType.parseMediaType(mediaType)); + } + mediaTypes.addAll(DEFAULT_GITHUB_MEDIA_TYPES); + return mediaTypes; + } + + private boolean isJsonConverter(HttpMessageConverter converter) { + if(AbstractJackson2HttpMessageConverter.class.isAssignableFrom(converter.getClass())) { + for (MediaType mediaType : converter.getSupportedMediaTypes()) { + if (mediaType.equals(MediaType.APPLICATION_JSON)) { + return true; + } + } + } + return false; + } + +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/GistServiceConfiguration.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/GistServiceConfiguration.java index 6f26bee..48aa580 100644 --- a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/GistServiceConfiguration.java +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/GistServiceConfiguration.java @@ -8,20 +8,24 @@ import java.io.IOException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cache.CacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.CommonsRequestLoggingFilter; import com.fasterxml.jackson.databind.ObjectMapper; -import com.hazelcast.config.Config; import com.hazelcast.core.HazelcastInstance; -import com.hazelcast.spring.cache.HazelcastCacheManager; import com.mangosolutions.rcloud.rawgist.repository.GistIdGenerator; +import com.mangosolutions.rcloud.rawgist.repository.GistRepositoryFactory; import com.mangosolutions.rcloud.rawgist.repository.GistRepositoryService; -import com.mangosolutions.rcloud.rawgist.repository.GitGistRepositoryService; -import com.mangosolutions.rcloud.rawgist.repository.UUIDGistIdGenerator; +import com.mangosolutions.rcloud.rawgist.repository.GistSecurityManager; +import com.mangosolutions.rcloud.rawgist.repository.git.GitGistRepositoryService; +import com.mangosolutions.rcloud.rawgist.repository.git.PermissiveGistSecurityManager; +import com.mangosolutions.rcloud.rawgist.repository.git.SimpleGistSecurityManager; +import com.mangosolutions.rcloud.rawgist.repository.git.UUIDGistIdGenerator; /** * Main Spring configuration @@ -31,6 +35,8 @@ @EnableConfigurationProperties(GistServiceProperties.class) public class GistServiceConfiguration { + private final Logger logger = LoggerFactory.getLogger(GistServiceConfiguration.class); + @Autowired private GistServiceProperties serviceProperties; @@ -39,19 +45,43 @@ public class GistServiceConfiguration { @Autowired private ObjectMapper objectMapper; - + + @Autowired + private GistRepositoryFactory repositoryFactory; + @Bean public GistRepositoryService getGistRepository() throws IOException { GitGistRepositoryService repo = new GitGistRepositoryService(serviceProperties.getRoot(), - this.getGistIdGenerator(), hazelcastInstance, objectMapper); + this.getGistIdGenerator(), hazelcastInstance); repo.setLockTimeout(serviceProperties.getLockTimeout()); + repo.setSecurityManager(getGistSecurityManager()); + repo.setGistRepositoryFactory(repositoryFactory); return repo; } + + @Bean + public GistSecurityManager getGistSecurityManager() { + if(GistServiceProperties.STRICT_SECURITY_MANAGER.equals(serviceProperties.getSecurity())) { + logger.info("Using strict gist security manager."); + return new SimpleGistSecurityManager(); + } else { + logger.info("Using permissive gist security manager."); + return new PermissiveGistSecurityManager(); + } + } @Bean public GistIdGenerator getGistIdGenerator() { return new UUIDGistIdGenerator(); } + @Bean + public CommonsRequestLoggingFilter requestLoggingFilter() { + CommonsRequestLoggingFilter crlf = new CommonsRequestLoggingFilter(); + crlf.setIncludeClientInfo(true); + crlf.setIncludeQueryString(true); + crlf.setIncludePayload(true); + return crlf; + } } diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/GistServiceProperties.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/GistServiceProperties.java index 7af9c80..d8bfaba 100644 --- a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/GistServiceProperties.java +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/GistServiceProperties.java @@ -6,11 +6,18 @@ *******************************************************************************/ package com.mangosolutions.rcloud.rawgist; +import java.util.ArrayList; +import java.util.List; + import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "gists") public class GistServiceProperties { - + + public static String STRICT_SECURITY_MANAGER = "strict"; + + private String security = "permissive"; + private String root; private String cache = "gists"; @@ -20,6 +27,8 @@ public class GistServiceProperties { private String sessionKeyServerUrl = null; private String sessionKeyServerRealm = "rcloud"; + + private List mediatypes = new ArrayList<>(); public String getSessionKeyServerRealm() { return sessionKeyServerRealm; @@ -61,4 +70,20 @@ public void setSessionKeyServerUrl(String sessionKeyServerUrl) { this.sessionKeyServerUrl = sessionKeyServerUrl; } + public List getMediatypes() { + return mediatypes; + } + + public void setMediatypes(List mediatypes) { + this.mediatypes = mediatypes; + } + + public String getSecurity() { + return security; + } + + public void setSecurity(String securityManager) { + this.security = securityManager; + } + } diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/RequestParameterAuthenticationFilter.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/RequestParameterAuthenticationFilter.java new file mode 100644 index 0000000..606112c --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/RequestParameterAuthenticationFilter.java @@ -0,0 +1,77 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedCredentialsNotFoundException; +import org.springframework.util.Assert; + +public class RequestParameterAuthenticationFilter extends AbstractPreAuthenticatedProcessingFilter { + + private String principalRequestParameter = "session_token"; + private String credentialsRequestParameter; + private boolean exceptionIfHeaderMissing = true; + + /** + * Read and returns the header named by {@code principalRequestHeader} from the + * request. + * + * @throws PreAuthenticatedCredentialsNotFoundException if the header is missing and + * {@code exceptionIfHeaderMissing} is set to {@code true}. + */ + @Override + protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) { + String principal = request.getParameter(principalRequestParameter); + + if (principal == null && exceptionIfHeaderMissing) { + throw new PreAuthenticatedCredentialsNotFoundException(principalRequestParameter + + " parameter not found in request."); + } + + return principal; + } + + /** + * Credentials aren't usually applicable, but if a {@code credentialsRequestHeader} is + * set, this will be read and used as the credentials value. Otherwise a dummy value + * will be used. + */ + @Override + protected Object getPreAuthenticatedCredentials(HttpServletRequest request) { + if (credentialsRequestParameter != null) { + return request.getParameter(credentialsRequestParameter); + } + + return "N/A"; + } + + public void setPrincipalRequestParameter(String principalRequestHeader) { + Assert.hasText(principalRequestHeader, + "principalRequestHeader must not be empty or null"); + this.principalRequestParameter = principalRequestHeader; + } + + public void setCredentialsRequestParameter(String credentialsRequestHeader) { + Assert.hasText(credentialsRequestHeader, + "credentialsRequestHeader must not be empty or null"); + this.credentialsRequestParameter = credentialsRequestHeader; + } + + /** + * Defines whether an exception should be raised if the principal header is missing. + * Defaults to {@code true}. + * + * @param exceptionIfHeaderMissing set to {@code false} to override the default + * behaviour and allow the request to proceed if no header is found. + */ + public void setExceptionIfHeaderMissing(boolean exceptionIfHeaderMissing) { + this.exceptionIfHeaderMissing = exceptionIfHeaderMissing; + } + +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/SessionKeyServerProperties.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/SessionKeyServerProperties.java index 4cc97f5..421cc2d 100644 --- a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/SessionKeyServerProperties.java +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/SessionKeyServerProperties.java @@ -11,9 +11,12 @@ @ConfigurationProperties(prefix = "gists.keyserver") public class SessionKeyServerProperties { - private String url = null;//"http://127.0.0.1:4301/valid?token={token}&realm={realm}"; + private String url = null; - private String realm = null;//"rcloud"; + private String realm = "rcloud"; + + private String token = "access_token"; + public String getUrl() { return url; @@ -31,6 +34,14 @@ public void setRealm(String realm) { this.realm = realm; } + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + } diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/SessionKeyServerSecurityConfiguration.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/SessionKeyServerSecurityConfiguration.java index d570e3a..22747dc 100644 --- a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/SessionKeyServerSecurityConfiguration.java +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/SessionKeyServerSecurityConfiguration.java @@ -36,12 +36,10 @@ @EnableConfigurationProperties(SessionKeyServerProperties.class) public class SessionKeyServerSecurityConfiguration extends WebSecurityConfigurerAdapter { - private static Logger logger = LoggerFactory.getLogger(SecurityConfig.class); + private static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class); @Autowired private SessionKeyServerProperties keyserverProperties; - - @Override protected void configure(HttpSecurity http) throws Exception { @@ -70,7 +68,7 @@ public UserDetailsService getSessionKeyServerUserDetailsService() { } String realm = keyserverProperties.getRealm(); if(!StringUtils.isEmpty(realm)) { - logger.info("Setting the session key URL to {}", serverUrl); + logger.info("Setting the session key realm to {}", realm); service.setRealm(realm.trim()); } return service; @@ -93,10 +91,10 @@ public PreAuthenticatedAuthenticationProvider preauthAuthProvider() { } @Bean - public RequestHeaderAuthenticationFilter ssoFilter() throws Exception { - RequestHeaderAuthenticationFilter filter = new RequestHeaderAuthenticationFilter(); + public RequestParameterAuthenticationFilter ssoFilter() throws Exception { + RequestParameterAuthenticationFilter filter = new RequestParameterAuthenticationFilter(); filter.setAuthenticationManager(authenticationManager()); - filter.setPrincipalRequestHeader("x-sessionkey-token"); + filter.setPrincipalRequestParameter(keyserverProperties.getToken()); return filter; } diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/api/ApplicationErrorsControllerAdvice.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/api/ApplicationErrorsControllerAdvice.java index 04e52d0..e14dcb1 100644 --- a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/api/ApplicationErrorsControllerAdvice.java +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/api/ApplicationErrorsControllerAdvice.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.mangosolutions.rcloud.rawgist.repository.GistAccessDeniedException; import com.mangosolutions.rcloud.rawgist.repository.GistError; import com.mangosolutions.rcloud.rawgist.repository.GistRepositoryError; import com.mangosolutions.rcloud.rawgist.repository.GistRepositoryException; @@ -34,7 +35,6 @@ public class ApplicationErrorsControllerAdvice { @ResponseBody @ExceptionHandler(GistRepositoryException.class) - //TODO map this error properly @ResponseStatus(HttpStatus.BAD_REQUEST) String handle(GistRepositoryException ex) { logger.error(ex.getMessage(), ex); @@ -47,9 +47,23 @@ String handle(GistRepositoryException ex) { } } + + @ResponseBody + @ExceptionHandler(GistAccessDeniedException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + String handle(GistAccessDeniedException ex) { + logger.error(ex.getMessage(), ex); + GistError gistError = ex.getGistError(); + VndError error = new VndError(gistError.getCode().toString(), gistError.toString()); + try { + return objectMapper.writeValueAsString(error); + } catch (JsonProcessingException e) { + throw new RuntimeException(gistError.getFormattedMessage()); + } + } + @ResponseBody @ExceptionHandler(GistRepositoryError.class) - //TODO map this error properly @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) String handle(GistRepositoryError ex) { logger.error(ex.getMessage(), ex); diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/api/GistCommentRestController.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/api/GistCommentRestController.java index ec4f7f5..3251d0e 100644 --- a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/api/GistCommentRestController.java +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/api/GistCommentRestController.java @@ -31,7 +31,10 @@ import com.mangosolutions.rcloud.rawgist.repository.GistRepositoryService; @RestController() -@RequestMapping(value = "/gists/{gistId}/comments", produces={ MediaType.APPLICATION_JSON_VALUE }) +@RequestMapping(value = "/gists/{gistId}/comments", produces={ + MediaType.APPLICATION_JSON_VALUE, + "application/vnd.github.beta+json", + "application/vnd.github.v3+json" }) @CacheConfig(cacheNames="comments") public class GistCommentRestController { diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/api/GistRestController.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/api/GistRestController.java index 42c8012..00ded58 100644 --- a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/api/GistRestController.java +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/api/GistRestController.java @@ -9,10 +9,13 @@ import java.net.URI; import java.net.URISyntaxException; import java.util.Collection; +import java.util.Collections; import java.util.List; import javax.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CachePut; @@ -30,14 +33,20 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import com.mangosolutions.rcloud.rawgist.model.Fork; import com.mangosolutions.rcloud.rawgist.model.GistRequest; import com.mangosolutions.rcloud.rawgist.model.GistResponse; import com.mangosolutions.rcloud.rawgist.repository.GistRepositoryService; @RestController() -@RequestMapping(value = "/gists", produces = { MediaType.APPLICATION_JSON_VALUE }) +@RequestMapping(value = "/gists", produces = { + MediaType.APPLICATION_JSON_VALUE, + "application/vnd.github.beta+json", + "application/vnd.github.v3+json" }) public class GistRestController { + private final Logger logger = LoggerFactory.getLogger(GistRestController.class); + @Autowired private GistRepositoryService repository; @@ -45,17 +54,15 @@ public class GistRestController { private ControllerUrlResolver resolver; @RequestMapping(method = RequestMethod.GET) - public List listAllGists(@AuthenticationPrincipal User activeUser) { + public List listAllGistsForUser(@AuthenticationPrincipal User activeUser) { List responses = repository.listGists(activeUser); decorateUrls(responses, activeUser); return responses; } @RequestMapping(value = "/public", method = RequestMethod.GET) - public List listPublicGists(@AuthenticationPrincipal User activeUser) { - List responses = repository.listGists(activeUser); - decorateUrls(responses, activeUser); - return responses; + public List listAllPublicGists() { + return Collections.emptyList(); } @RequestMapping(value = "/{gistId}", method = RequestMethod.GET) @@ -84,9 +91,33 @@ public GistResponse createGist(@RequestBody GistRequest request, HttpServletRequ decorateUrls(response, activeUser); return response; } - + + @RequestMapping(value = "/{gistId}/forks", method = RequestMethod.GET) + @ResponseStatus(HttpStatus.OK) + public List getForks(@PathVariable("gistId") String gistId, + @AuthenticationPrincipal User activeUser) { + + List forks = repository.getForks(gistId, activeUser); + + decorateUrls(forks, activeUser); + return forks; + } + + /* + * Legacy github mapping + */ + @RequestMapping(value = "/{gistId}/fork", method = RequestMethod.GET) + @ResponseStatus(HttpStatus.OK) + @Deprecated + public List legacyGetForks(@PathVariable("gistId") String gistId, + @AuthenticationPrincipal User activeUser) { + return this.getForks(gistId, activeUser); + } + + @RequestMapping(value = "/{gistId}/forks", method = RequestMethod.POST) @ResponseStatus(HttpStatus.CREATED) + @CacheEvict(cacheNames = "gists", key="#gistId") public ResponseEntity forkGist(@PathVariable("gistId") String gistId, @AuthenticationPrincipal User activeUser) { // TODO need to add Location header to response for the new Gist @@ -96,8 +127,7 @@ public ResponseEntity forkGist(@PathVariable("gistId") String gist try { headers.setLocation(new URI(location)); } catch (URISyntaxException e) { - // TODO Auto-generated catch block - e.printStackTrace(); + logger.warn("Unable to set the location header with value {} for fork with id {} with error {}.", location, gistId, e.getMessage()); } decorateUrls(response, activeUser); ResponseEntity responseEntity = new ResponseEntity<>(response, headers, HttpStatus.CREATED); @@ -105,11 +135,12 @@ public ResponseEntity forkGist(@PathVariable("gistId") String gist return responseEntity; } - /** + /* * Legacy github mapping */ @RequestMapping(value = "/{gistId}/fork", method = RequestMethod.POST) @ResponseStatus(HttpStatus.CREATED) + @CacheEvict(cacheNames = "gists", key="#gistId") @Deprecated public ResponseEntity legacyForkGist(@PathVariable("gistId") String gistId, @AuthenticationPrincipal User activeUser) { @@ -147,5 +178,13 @@ private void decorateUrls(GistResponse gistResponse, User activeUser) { gistResponse.setForksUrl(resolver.getForksUrl(gistResponse.getId(), activeUser)); } } + + private void decorateUrls(List forks, User activeUser) { + for(Fork fork: forks) { + String forkUrl = resolver.getGistUrl(fork.getId(), activeUser); + fork.setUrl(forkUrl); + } + + } } diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/api/RedirectController.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/api/RedirectController.java new file mode 100644 index 0000000..05736f5 --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/api/RedirectController.java @@ -0,0 +1,42 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.api; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Controller; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; +import org.springframework.web.servlet.view.RedirectView; + + +@Controller +@RequestMapping("${gists.route.redirect.from}") +public class RedirectController { + + @Value("${gists.route.redirect.to}") + private String redirectToUrl; + + @Value("${gists.route.redirect.copyparams:true}") + private boolean propgateParams = true; + + @RequestMapping(method={RequestMethod.GET, RequestMethod.POST}) + public RedirectView performRedirect(RedirectAttributes attributes) { + RedirectView redirectView = new RedirectView(); + if(StringUtils.isEmpty(redirectToUrl)) { + redirectView.setStatusCode(HttpStatus.NOT_FOUND); + } else { + redirectView.setUrl(redirectToUrl); + redirectView.setPropagateQueryParams(propgateParams); + + } + return redirectView; + } + +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/api/UserRestController.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/api/UserRestController.java new file mode 100644 index 0000000..07898ed --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/api/UserRestController.java @@ -0,0 +1,43 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.api; + +import java.util.Collections; +import java.util.List; + +import org.springframework.http.MediaType; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.userdetails.User; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import com.mangosolutions.rcloud.rawgist.model.GistIdentity; +import com.mangosolutions.rcloud.rawgist.model.GistResponse; + +@RestController() +@RequestMapping(value = "/user", produces = { + MediaType.APPLICATION_JSON_VALUE, + "application/vnd.github.beta+json", + "application/vnd.github.v3+json" + }) +public class UserRestController { + + @RequestMapping(method = RequestMethod.GET) + public GistIdentity getUser(@AuthenticationPrincipal User activeUser) { + GistIdentity response = new GistIdentity(); + String username = activeUser.getUsername(); + response.setLogin(username); + return response; + } + + @RequestMapping(value = "/{username}/gists", method = RequestMethod.GET) + public List getUsersPublicGists() { + return Collections.emptyList(); + } + +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/model/FileContent.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/model/FileContent.java index 0a98843..1587222 100644 --- a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/model/FileContent.java +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/model/FileContent.java @@ -127,4 +127,78 @@ public void setAdditionalProperty(String name, Object value) { this.additionalProperties.put(name, value); } + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((additionalProperties == null) ? 0 : additionalProperties.hashCode()); + result = prime * result + ((content == null) ? 0 : content.hashCode()); + result = prime * result + ((filename == null) ? 0 : filename.hashCode()); + result = prime * result + ((language == null) ? 0 : language.hashCode()); + result = prime * result + ((rawUrl == null) ? 0 : rawUrl.hashCode()); + result = prime * result + ((size == null) ? 0 : size.hashCode()); + result = prime * result + ((truncated == null) ? 0 : truncated.hashCode()); + result = prime * result + ((type == null) ? 0 : type.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + FileContent other = (FileContent) obj; + if (additionalProperties == null) { + if (other.additionalProperties != null) + return false; + } else if (!additionalProperties.equals(other.additionalProperties)) + return false; + if (content == null) { + if (other.content != null) + return false; + } else if (!content.equals(other.content)) + return false; + if (filename == null) { + if (other.filename != null) + return false; + } else if (!filename.equals(other.filename)) + return false; + if (language == null) { + if (other.language != null) + return false; + } else if (!language.equals(other.language)) + return false; + if (rawUrl == null) { + if (other.rawUrl != null) + return false; + } else if (!rawUrl.equals(other.rawUrl)) + return false; + if (size == null) { + if (other.size != null) + return false; + } else if (!size.equals(other.size)) + return false; + if (truncated == null) { + if (other.truncated != null) + return false; + } else if (!truncated.equals(other.truncated)) + return false; + if (type == null) { + if (other.type != null) + return false; + } else if (!type.equals(other.type)) + return false; + return true; + } + + @Override + public String toString() { + return "FileContent [size=" + size + ", rawUrl=" + rawUrl + ", type=" + type + ", language=" + language + + ", truncated=" + truncated + ", content=" + content + ", filename=" + filename + + ", additionalProperties=" + additionalProperties + "]"; + } + } diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/model/GistHistory.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/model/GistHistory.java index ccdb809..17ae025 100644 --- a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/model/GistHistory.java +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/model/GistHistory.java @@ -70,4 +70,59 @@ public void setCommittedAt(DateTime committedAt) { this.committedAt = committedAt; } + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((changeStatus == null) ? 0 : changeStatus.hashCode()); + result = prime * result + ((committedAt == null) ? 0 : committedAt.hashCode()); + result = prime * result + ((url == null) ? 0 : url.hashCode()); + result = prime * result + ((user == null) ? 0 : user.hashCode()); + result = prime * result + ((version == null) ? 0 : version.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + GistHistory other = (GistHistory) obj; + if (changeStatus == null) { + if (other.changeStatus != null) + return false; + } else if (!changeStatus.equals(other.changeStatus)) + return false; + if (committedAt == null) { + if (other.committedAt != null) + return false; + } else if (!committedAt.equals(other.committedAt)) + return false; + if (url == null) { + if (other.url != null) + return false; + } else if (!url.equals(other.url)) + return false; + if (user == null) { + if (other.user != null) + return false; + } else if (!user.equals(other.user)) + return false; + if (version == null) { + if (other.version != null) + return false; + } else if (!version.equals(other.version)) + return false; + return true; + } + + @Override + public String toString() { + return "GistHistory [url=" + url + ", version=" + version + ", user=" + user + ", changeStatus=" + changeStatus + + ", committedAt=" + committedAt + "]"; + } + } diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/model/GistIdentity.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/model/GistIdentity.java index 1fe7166..d0808a2 100644 --- a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/model/GistIdentity.java +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/model/GistIdentity.java @@ -49,4 +49,40 @@ public void setAdditionalProperty(String name, Object value) { this.additionalProperties.put(name, value); } + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((additionalProperties == null) ? 0 : additionalProperties.hashCode()); + result = prime * result + ((login == null) ? 0 : login.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + GistIdentity other = (GistIdentity) obj; + if (additionalProperties == null) { + if (other.additionalProperties != null) + return false; + } else if (!additionalProperties.equals(other.additionalProperties)) + return false; + if (login == null) { + if (other.login != null) + return false; + } else if (!login.equals(other.login)) + return false; + return true; + } + + @Override + public String toString() { + return "GistIdentity [login=" + login + ", additionalProperties=" + additionalProperties + "]"; + } + } diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/model/GistResponse.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/model/GistResponse.java index 13bc143..13c9315 100644 --- a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/model/GistResponse.java +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/model/GistResponse.java @@ -242,4 +242,129 @@ public void addAdditionalProperties(Map properties) { this.additionalProperties.putAll(properties); } + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((_public == null) ? 0 : _public.hashCode()); + result = prime * result + ((additionalProperties == null) ? 0 : additionalProperties.hashCode()); + result = prime * result + ((comments == null) ? 0 : comments.hashCode()); + result = prime * result + ((commentsUrl == null) ? 0 : commentsUrl.hashCode()); + result = prime * result + ((commitsUrl == null) ? 0 : commitsUrl.hashCode()); + result = prime * result + ((createdAt == null) ? 0 : createdAt.hashCode()); + result = prime * result + ((description == null) ? 0 : description.hashCode()); + result = prime * result + ((files == null) ? 0 : files.hashCode()); + result = prime * result + ((forksUrl == null) ? 0 : forksUrl.hashCode()); + result = prime * result + ((history == null) ? 0 : history.hashCode()); + result = prime * result + ((id == null) ? 0 : id.hashCode()); + result = prime * result + ((owner == null) ? 0 : owner.hashCode()); + result = prime * result + ((truncated == null) ? 0 : truncated.hashCode()); + result = prime * result + ((updatedAt == null) ? 0 : updatedAt.hashCode()); + result = prime * result + ((url == null) ? 0 : url.hashCode()); + result = prime * result + ((user == null) ? 0 : user.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + GistResponse other = (GistResponse) obj; + if (_public == null) { + if (other._public != null) + return false; + } else if (!_public.equals(other._public)) + return false; + if (additionalProperties == null) { + if (other.additionalProperties != null) + return false; + } else if (!additionalProperties.equals(other.additionalProperties)) + return false; + if (comments == null) { + if (other.comments != null) + return false; + } else if (!comments.equals(other.comments)) + return false; + if (commentsUrl == null) { + if (other.commentsUrl != null) + return false; + } else if (!commentsUrl.equals(other.commentsUrl)) + return false; + if (commitsUrl == null) { + if (other.commitsUrl != null) + return false; + } else if (!commitsUrl.equals(other.commitsUrl)) + return false; + if (createdAt == null) { + if (other.createdAt != null) + return false; + } else if (!createdAt.equals(other.createdAt)) + return false; + if (description == null) { + if (other.description != null) + return false; + } else if (!description.equals(other.description)) + return false; + if (files == null) { + if (other.files != null) + return false; + } else if (!files.equals(other.files)) + return false; + if (forksUrl == null) { + if (other.forksUrl != null) + return false; + } else if (!forksUrl.equals(other.forksUrl)) + return false; + if (history == null) { + if (other.history != null) + return false; + } else if (!history.equals(other.history)) + return false; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + if (owner == null) { + if (other.owner != null) + return false; + } else if (!owner.equals(other.owner)) + return false; + if (truncated == null) { + if (other.truncated != null) + return false; + } else if (!truncated.equals(other.truncated)) + return false; + if (updatedAt == null) { + if (other.updatedAt != null) + return false; + } else if (!updatedAt.equals(other.updatedAt)) + return false; + if (url == null) { + if (other.url != null) + return false; + } else if (!url.equals(other.url)) + return false; + if (user == null) { + if (other.user != null) + return false; + } else if (!user.equals(other.user)) + return false; + return true; + } + + @Override + public String toString() { + return "GistResponse [url=" + url + ", commitsUrl=" + commitsUrl + ", forksUrl=" + forksUrl + ", id=" + id + + ", description=" + description + ", _public=" + _public + ", owner=" + owner + ", user=" + user + + ", files=" + files + ", truncated=" + truncated + ", comments=" + comments + ", commentsUrl=" + + commentsUrl + ", createdAt=" + createdAt + ", updatedAt=" + updatedAt + ", history=" + history + + ", additionalProperties=" + additionalProperties + "]"; + } + + } diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/model/GitChangeStatus.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/model/GitChangeStatus.java index 562f49e..18f65c5 100644 --- a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/model/GitChangeStatus.java +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/model/GitChangeStatus.java @@ -51,5 +51,38 @@ public void setTotal(int total) { this.total = total; } + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + additions; + result = prime * result + deletions; + result = prime * result + total; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + GitChangeStatus other = (GitChangeStatus) obj; + if (additions != other.additions) + return false; + if (deletions != other.deletions) + return false; + if (total != other.total) + return false; + return true; + } + + @Override + public String toString() { + return "GitChangeStatus [deletions=" + deletions + ", additions=" + additions + ", total=" + total + "]"; + } + } diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GistAccessDeniedException.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GistAccessDeniedException.java new file mode 100644 index 0000000..e746273 --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GistAccessDeniedException.java @@ -0,0 +1,27 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.repository; + +public class GistAccessDeniedException extends RuntimeException { + + private static final long serialVersionUID = -217812774457301300L; + + private GistError error; + + public GistAccessDeniedException(GistError error, Throwable cause) { + super(error.getFormattedMessage(), cause); + this.error = error; + } + + public GistAccessDeniedException(GistError error) { + this(error, null); + } + + public GistError getGistError() { + return error; + } +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GistError.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GistError.java index b759338..5afeec8 100644 --- a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GistError.java +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GistError.java @@ -13,21 +13,6 @@ public class GistError { - public enum GistErrorCode { - - ERR_METADATA_NOT_READABLE, - ERR_METADATA_NOT_WRITEABLE, - ERR_COMMENTS_NOT_READABLE, - ERR_COMMENTS_NOT_WRITEABLE, - ERR_GIST_UPDATE_FAILURE, - ERR_GIST_FORK_FAILURE, - ERR_GIST_CONTENT_NOT_READABLE, - ERR_GIST_CONTENT_NOT_AVAILABLE, - ERR_GIST_NOT_EXIST, - ERR_COMMENT_NOT_EXIST, - FATAL_GIST_INITIALISATION - } - private static final String PREFIX_FORMAT = "{}: "; private GistErrorCode code; @@ -55,12 +40,13 @@ public Serializable[] getParams() { return this.params; } + public String getFormattedMessage() { String prefix = ""; if(!StringUtils.isEmpty(code)) { prefix = this.format(PREFIX_FORMAT, code); } - return prefix + this.format(message, params); + return prefix + this.format(message, (Object[]) params); } diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GistErrorCode.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GistErrorCode.java new file mode 100644 index 0000000..1ad6edd --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GistErrorCode.java @@ -0,0 +1,24 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.repository; + +public enum GistErrorCode { + + ERR_METADATA_NOT_READABLE, + ERR_METADATA_NOT_WRITEABLE, + ERR_COMMENTS_NOT_READABLE, + ERR_COMMENTS_NOT_WRITEABLE, + ERR_GIST_UPDATE_FAILURE, + ERR_GIST_FORK_FAILURE, + ERR_GIST_CONTENT_NOT_READABLE, + ERR_GIST_CONTENT_NOT_AVAILABLE, + ERR_GIST_NOT_EXIST, + ERR_COMMENT_NOT_EXIST, + FATAL_GIST_INITIALISATION, + ERR_ACL_WRITE_DENIED, + ERR_ACL_READ_DENIED +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GistRepository.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GistRepository.java index 964b467..69a9605 100644 --- a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GistRepository.java +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GistRepository.java @@ -12,25 +12,31 @@ import com.mangosolutions.rcloud.rawgist.model.GistRequest; import com.mangosolutions.rcloud.rawgist.model.GistResponse; +import com.mangosolutions.rcloud.rawgist.repository.git.GistMetadata; public interface GistRepository { File getGistRepositoryFolder(UserDetails userDetails); + + File getGistGitRepositoryFolder(UserDetails userDetails); - GistResponse getGist(UserDetails userDetails); + GistResponse readGist(UserDetails userDetails); - GistResponse getGist(String commitId, UserDetails userDetails); + GistResponse readGist(String commitId, UserDetails userDetails); - GistResponse createGist(GistRequest request, UserDetails userDetails); + GistResponse createGist(GistRequest request, String gistId, UserDetails userDetails); - GistResponse editGist(GistRequest request, UserDetails userDetails); + GistResponse updateGist(GistRequest request, UserDetails userDetails); - GistResponse fork(GistRepository forkedRepository, UserDetails userDetails); + GistResponse forkGist(GistRepository forkedRepository, String gistId, UserDetails userDetails); String getId(); void registerFork(GistRepository forkedRepository); GistMetadata getMetadata(); - + + GistCommentRepository getCommentRepository(); + + } diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GistRepositoryFactory.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GistRepositoryFactory.java new file mode 100644 index 0000000..11583da --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GistRepositoryFactory.java @@ -0,0 +1,15 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.repository; + +import java.io.File; + +public interface GistRepositoryFactory { + + GistRepository getRepository(File folder); + +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GistRepositoryService.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GistRepositoryService.java index f313a4c..26b6fc4 100644 --- a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GistRepositoryService.java +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GistRepositoryService.java @@ -11,6 +11,7 @@ import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; +import com.mangosolutions.rcloud.rawgist.model.Fork; import com.mangosolutions.rcloud.rawgist.model.GistComment; import com.mangosolutions.rcloud.rawgist.model.GistCommentResponse; import com.mangosolutions.rcloud.rawgist.model.GistRequest; @@ -41,5 +42,7 @@ public interface GistRepositoryService { public void deleteComment(String gistId, long commentId, UserDetails activeUser); public GistResponse forkGist(String gistId, User activeUser); + + public List getForks(String gistId, User activeUser); } diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GistSecurityManager.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GistSecurityManager.java new file mode 100644 index 0000000..10995eb --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GistSecurityManager.java @@ -0,0 +1,31 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.repository; + +import org.springframework.security.core.userdetails.UserDetails; + +public interface GistSecurityManager { + + public enum GistAccessRight { + NONE, READ, WRITE + } + + public enum GistRole { + OWNER, COLLABORATOR + } + + boolean canRead(GistRepository repository, UserDetails userDetails); + + boolean canWrite(GistRepository repository, UserDetails userDetails); + + GistAccessRight getAccessRight(GistRepository repository, UserDetails userDetails); + + boolean isOwner(GistRepository repository, UserDetails userDetails); + + GistRole getRole(GistRepository repository, UserDetails userDetails); + +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GitGistRepository.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GitGistRepository.java deleted file mode 100644 index e740abc..0000000 --- a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GitGistRepository.java +++ /dev/null @@ -1,437 +0,0 @@ -/******************************************************************************* -* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] -* -* SPDX-License-Identifier: MIT -* -*******************************************************************************/ -package com.mangosolutions.rcloud.rawgist.repository; - -import java.io.File; -import java.io.IOException; -import java.io.Serializable; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import javax.activation.MimetypesFileTypeMap; - -import org.ajoberstar.grgit.Grgit; -import org.ajoberstar.grgit.Person; -import org.ajoberstar.grgit.Repository; -import org.ajoberstar.grgit.Status; -import org.ajoberstar.grgit.Status.Changes; -import org.ajoberstar.grgit.operation.AddOp; -import org.ajoberstar.grgit.operation.CheckoutOp; -import org.ajoberstar.grgit.operation.CleanOp; -import org.ajoberstar.grgit.operation.CommitOp; -import org.ajoberstar.grgit.operation.InitOp; -import org.ajoberstar.grgit.operation.OpenOp; -import org.ajoberstar.grgit.operation.ResetOp; -import org.ajoberstar.grgit.operation.ResetOp.Mode; -import org.ajoberstar.grgit.operation.RmOp; -import org.ajoberstar.grgit.operation.StatusOp; -import org.apache.commons.codec.CharEncoding; -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.FilenameUtils; -import org.apache.commons.io.filefilter.FileFileFilter; -import org.apache.commons.io.filefilter.FileFilterUtils; -import org.apache.commons.io.filefilter.TrueFileFilter; -import org.joda.time.DateTime; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.util.StringUtils; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.mangosolutions.rcloud.rawgist.model.FileContent; -import com.mangosolutions.rcloud.rawgist.model.FileDefinition; -import com.mangosolutions.rcloud.rawgist.model.Fork; -import com.mangosolutions.rcloud.rawgist.model.GistHistory; -import com.mangosolutions.rcloud.rawgist.model.GistIdentity; -import com.mangosolutions.rcloud.rawgist.model.GistRequest; -import com.mangosolutions.rcloud.rawgist.model.GistResponse; -import com.mangosolutions.rcloud.rawgist.repository.GistError.GistErrorCode; - -public class GitGistRepository implements GistRepository { - - private static final Logger logger = LoggerFactory.getLogger(GitGistRepository.class); - - private static final String B64_BINARY_EXTENSION = "b64"; - - public static final String GIST_META_JSON_FILE = "gist.json"; - - private static final String GIT_REPO_FOLDER_NAME = "repo"; - - private File repositoryFolder; - private File gitFolder; - private String gistId; - private GistCommentRepository commentRepository; - - private ObjectMapper objectMapper; - - public GitGistRepository(File repositoryFolder, String gistId, ObjectMapper objectMapper, UserDetails owner) { - this(repositoryFolder, objectMapper); - this.commentRepository = new GitGistCommentRepository(repositoryFolder, gistId, objectMapper); - this.gistId = gistId; - this.initializeRepository(owner); - } - - public GitGistRepository(File repositoryFolder, ObjectMapper objectMapper) { - this.repositoryFolder = repositoryFolder; - this.gitFolder = new File(repositoryFolder, GIT_REPO_FOLDER_NAME); - this.objectMapper = objectMapper; - this.gistId = this.getMetadata().getId(); - this.commentRepository = new GitGistCommentRepository(repositoryFolder, gistId, objectMapper); - } - - private void initializeRepository(UserDetails userDetails) { - if (!repositoryFolder.exists()) { - try { - FileUtils.forceMkdir(repositoryFolder); - } catch (IOException e) { - GistError error = new GistError(GistErrorCode.FATAL_GIST_INITIALISATION, "Could not create gist storage location for gist {}", - this.gistId); - logger.error(error.getFormattedMessage() + " with folder path {}", repositoryFolder); - throw new GistRepositoryError(error, e); - } - } - - if (!gitFolder.exists()) { - try { - FileUtils.forceMkdir(gitFolder); - InitOp initOp = new InitOp(); - initOp.setDir(gitFolder); - initOp.call(); - } catch (IOException e) { - GistError error = new GistError(GistErrorCode.FATAL_GIST_INITIALISATION, "Could not create gist storage for gist {}", this.gistId); - logger.error(error.getFormattedMessage() + " with folder path {}", gitFolder); - throw new GistRepositoryError(error, e); - } - } - - this.updateMetadata(userDetails); - } - - @Override - public String getId() { - return this.gistId; - } - - @Override - public File getGistRepositoryFolder(UserDetails owner) { - return repositoryFolder; - } - - @Override - public GistResponse getGist(UserDetails userDetails) { - GistResponse response = null; - // create git repository - OpenOp openOp = new OpenOp(); - openOp.setDir(gitFolder); - Grgit git = openOp.call(); - response = buildResponse(git, userDetails); - return response; - } - - @Override - public GistResponse getGist(String commitId, UserDetails userDetails) { - GistResponse response = null; - // create git repository - OpenOp openOp = new OpenOp(); - openOp.setDir(gitFolder); - Grgit git = openOp.call(); - response = buildResponse(git, commitId, userDetails); - return response; - } - - @Override - public GistResponse createGist(GistRequest request, UserDetails userDetails) { - OpenOp openOp = new OpenOp(); - openOp.setDir(gitFolder); - Grgit git = openOp.call(); - saveContent(git, request, userDetails); - return buildResponse(git, userDetails); - } - - @Override - public GistResponse fork(GistRepository originalRepository, UserDetails userDetails) { - File originalFolder = originalRepository.getGistRepositoryFolder(userDetails); - try { - FileUtils.copyDirectory(originalFolder, this.repositoryFolder); - OpenOp openOp = new OpenOp(); - openOp.setDir(gitFolder); - Grgit git = openOp.call(); - //TODO write the fork of information - this.updateMetadata(userDetails); - originalRepository.registerFork(this); - return this.buildResponse(git, userDetails); - } catch (IOException e) { - GistError error = new GistError(GistErrorCode.ERR_GIST_FORK_FAILURE, "Could not fork gist {} to a new gist with id {}", originalRepository.getId(), this.gistId); - logger.error(error.getFormattedMessage() + " with folder path {}", gitFolder); - throw new GistRepositoryException(error, e); - } - - - } - - @Override - public GistResponse editGist(GistRequest request, UserDetails userDetails) { - OpenOp openOp = new OpenOp(); - openOp.setDir(gitFolder); - Grgit git = openOp.call(); - saveContent(git, request, userDetails); - return buildResponse(git, userDetails); - } - - private void saveContent(Grgit git, GistRequest request, UserDetails userDetails) { - Map files = request.getFiles(); - try { - for (Map.Entry file : files.entrySet()) { - String filename = file.getKey(); - FileDefinition definition = file.getValue(); - if (isDelete(definition)) { - try { - FileUtils.forceDelete(new File(gitFolder, filename)); - } catch (IOException e) { - GistError error = new GistError(GistErrorCode.ERR_GIST_UPDATE_FAILURE, "Could not remove {} from gist {}", filename, this.gistId); - logger.error(error.getFormattedMessage() + " with folder path {}", gitFolder); - throw new GistRepositoryException(error, e); - } - } - if (isUpdate(definition)) { - try { - FileUtils.write(new File(gitFolder, filename), definition.getContent(), CharEncoding.UTF_8); - } catch (IOException e) { - GistError error = new GistError(GistErrorCode.ERR_GIST_UPDATE_FAILURE, "Could not update {} for gist {}", filename, this.gistId); - logger.error(error.getFormattedMessage() + " with folder path {}", gitFolder); - throw new GistRepositoryException(error, e); - } - } - - if (isMove(definition)) { - File oldFile = new File(gitFolder, filename); - File newFile = new File(gitFolder, definition.getFilename()); - if(!oldFile.equals(newFile)) { - try { - FileUtils.moveFile(oldFile, newFile); - } catch (IOException e) { - GistError error = new GistError(GistErrorCode.ERR_GIST_UPDATE_FAILURE, "Could not move {} to {} for gist {}", filename, definition.getFilename(), this.gistId); - logger.error(error.getFormattedMessage() + " with folder path {}", gitFolder); - throw new GistRepositoryException(error, e); - } - } - } - } - StatusOp statusOp = new StatusOp(git.getRepository()); - Status status = statusOp.call(); - if (!status.isClean()) { - stageAllChanges(status, git.getRepository()); - CommitOp commitOp = new CommitOp(git.getRepository()); - Person person = new Person(userDetails.getUsername(), ""); - commitOp.setCommitter(person); - commitOp.setAuthor(person); - commitOp.setMessage(""); - commitOp.call(); - } - this.updateMetadata(request); - } finally { - StatusOp statusOp = new StatusOp(git.getRepository()); - Status status = statusOp.call(); - if (!status.isClean()) { - // clean and then reset - CleanOp cleanOp = new CleanOp(git.getRepository()); - cleanOp.call(); - ResetOp resetOp = new ResetOp(git.getRepository()); - resetOp.setMode(Mode.HARD); - resetOp.call(); - } - - } - } - - private void updateMetadata(UserDetails owner) { - GistMetadata metadata = getMetadata(); - metadata.setId(this.gistId); - if (owner != null && StringUtils.isEmpty(metadata.getOwner())) { - metadata.setOwner(owner.getUsername()); - } - this.saveMetadata(metadata); - } - - private void saveMetadata(GistMetadata metadata) { - File metadataFile = new File(this.repositoryFolder, GIST_META_JSON_FILE); - try { - objectMapper.writeValue(metadataFile, metadata); - } catch (IOException e) { - GistError error = new GistError(GistErrorCode.ERR_METADATA_NOT_WRITEABLE, "Could not update metadata for gist {}", this.gistId); - logger.error(error.getFormattedMessage() + " with path {}", metadataFile); - throw new GistRepositoryError(error, e); - } - } - - private void updateMetadata(GistRequest request) { - GistMetadata metadata = getMetadata(); - metadata.setId(this.gistId); - if (request != null) { - String description = request.getDescription(); - - if (description != null) { - metadata.setDescription(description); - } - - if (metadata.getCreatedAt() == null) { - metadata.setCreatedAt(new DateTime()); - } - - metadata.setUpdatedAt(new DateTime()); - - } - this.saveMetadata(metadata); - } - - public GistMetadata getMetadata() { - File metadataFile = new File(this.repositoryFolder, GIST_META_JSON_FILE); - GistMetadata metadata = new GistMetadata(); - if (metadataFile.exists()) { - try { - metadata = objectMapper.readValue(metadataFile, GistMetadata.class); - } catch (IOException e) { - GistError error = new GistError(GistErrorCode.ERR_METADATA_NOT_READABLE, "Could not update metadata for gist {}", this.gistId); - logger.error(error.getFormattedMessage() + " with path {}", metadataFile); - throw new GistRepositoryError(error, e); - } - } - return metadata; - } - - private void stageAllChanges(Status status, Repository repository) { - Changes changes = status.getUnstaged(); - Set added = changes.getAdded(); - Set modified = changes.getModified(); - Set removed = changes.getRemoved(); - if (added != null && !added.isEmpty()) { - AddOp addOp = new AddOp(repository); - addOp.setPatterns(added); - addOp.call(); - } - if (modified != null && !modified.isEmpty()) { - AddOp addOp = new AddOp(repository); - addOp.setPatterns(modified); - addOp.call(); - } - if (removed != null && !removed.isEmpty()) { - RmOp rm = new RmOp(repository); - rm.setPatterns(removed); - rm.call(); - } - } - - private boolean isMove(FileDefinition definition) { - return definition != null && !StringUtils.isEmpty(definition.getFilename()); - } - - private boolean isUpdate(FileDefinition definition) { - return definition != null && definition.getContent() != null; - } - - private boolean isDelete(FileDefinition definition) { - return definition == null; - } - - private GistResponse buildResponse(Grgit git, String commitId, UserDetails activeUser) { - // switch the working copy to that commit - GistResponse response = null; - try { - CheckoutOp checkoutOp = new CheckoutOp(git.getRepository()); - checkoutOp.setBranch(commitId); - checkoutOp.call(); - response = buildResponse(git, activeUser); - } finally { - CheckoutOp checkoutOp = new CheckoutOp(git.getRepository()); - checkoutOp.setBranch("master"); - checkoutOp.call(); - } - return response; - } - - private GistResponse buildResponse(Grgit git, UserDetails activeUser) { - GistResponse response = new GistResponse(); - - response.setId(gistId); - Map files = new LinkedHashMap(); - Collection fileList = FileUtils.listFiles(gitFolder, FileFileFilter.FILE, FileFilterUtils - .and(TrueFileFilter.INSTANCE, FileFilterUtils.notFileFilter(FileFilterUtils.nameFileFilter(".git")))); - for (File file : fileList) { - FileContent content = new FileContent(); - try { - content.setFilename(file.getName()); - content.setContent(FileUtils.readFileToString(file, CharEncoding.UTF_8)); - content.setSize(file.length()); - content.setTruncated(false); - // TODO the language - String language = FilenameUtils.getExtension(file.getName()); - if(!B64_BINARY_EXTENSION.equals(language) && !StringUtils.isEmpty(language) ) { - content.setLanguage(language); - } - // TODO mimetype - content.setType(MimetypesFileTypeMap.getDefaultFileTypeMap().getContentType(file)); - files.put(file.getName(), content); - } catch (IOException e) { - GistError error = new GistError(GistErrorCode.ERR_GIST_CONTENT_NOT_READABLE, "Could not read content of {} for gist {}", file.getName(), this.gistId); - logger.error(error.getFormattedMessage() + " with path {}", file); - throw new GistRepositoryError(error, e); - } - } - response.setFiles(files); - response.setComments(commentRepository.getComments(activeUser).size()); - List history = getHistory(git); - response.setHistory(history); - applyMetadata(response); - return response; - } - - private List getHistory(Grgit git) { - GitHistoryCreator historyCreator = new GitHistoryCreator(); - return historyCreator.call(git.getRepository()); - } - - private void applyMetadata(GistResponse response) { - GistMetadata metadata = this.getMetadata(); - response.setDescription(metadata.getDescription()); - response.setDescription(metadata.getDescription()); - response.setCreatedAt(metadata.getCreatedAt()); - response.setUpdatedAt(metadata.getUpdatedAt()); - if (!StringUtils.isEmpty(metadata.getOwner())) { - GistIdentity owner = new GistIdentity(); - owner.setLogin(metadata.getOwner()); - response.setOwner(owner); - response.setUser(owner); - } - response.addAdditionalProperties(metadata.getAdditionalProperties()); - } - - @Override - public void registerFork(GistRepository forkedRepository) { - this.updateForkInformation(forkedRepository); - } - - private void updateForkInformation(GistRepository forkedRepository) { - GistMetadata metadata = this.getMetadata(); - GistMetadata forksMetadata = forkedRepository.getMetadata(); - Fork fork = new Fork(); - fork.setCreatedAt(forksMetadata.getCreatedAt()); - fork.setUpdatedAt(forksMetadata.getUpdatedAt()); - fork.setId(forksMetadata.getId()); - String forkOwner = forksMetadata.getOwner(); - GistIdentity forkOwnerIdentity = new GistIdentity(); - forkOwnerIdentity.setLogin(forkOwner); - fork.setUser(forkOwnerIdentity); - metadata.addFork(fork); - this.saveMetadata(metadata); - } - - - -} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GitGistRepositoryService.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GitGistRepositoryService.java deleted file mode 100644 index e39cf6f..0000000 --- a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GitGistRepositoryService.java +++ /dev/null @@ -1,387 +0,0 @@ -/******************************************************************************* -* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] -* -* SPDX-License-Identifier: MIT -* -*******************************************************************************/ -package com.mangosolutions.rcloud.rawgist.repository; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.Lock; - -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.filefilter.FileFileFilter; -import org.apache.commons.io.filefilter.FileFilterUtils; -import org.apache.commons.io.filefilter.NameFileFilter; -import org.apache.commons.io.filefilter.TrueFileFilter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.security.core.userdetails.User; -import org.springframework.security.core.userdetails.UserDetails; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.base.Splitter; -import com.hazelcast.core.HazelcastInstance; -import com.mangosolutions.rcloud.rawgist.model.GistComment; -import com.mangosolutions.rcloud.rawgist.model.GistCommentResponse; -import com.mangosolutions.rcloud.rawgist.model.GistRequest; -import com.mangosolutions.rcloud.rawgist.model.GistResponse; -import com.mangosolutions.rcloud.rawgist.repository.GistError.GistErrorCode; - -public class GitGistRepositoryService implements GistRepositoryService { - - private static final int DEFAULT_LOCK_TIMEOUT = 30; - - private int lockTimeout = DEFAULT_LOCK_TIMEOUT; - - private static final Splitter REPOSITORYID_FOLDER_SPLITTER = Splitter.fixedLength(4); - - private static final String RECYCLE_FOLDER_NAME = ".recycle"; - - private Logger logger = LoggerFactory.getLogger(GitGistRepositoryService.class); - - private File repositoryRoot; - private File recycleRoot; - private GistIdGenerator idGenerator; - private HazelcastInstance hazelcastInstance; - private ObjectMapper objectMapper; - - public GitGistRepositoryService(String repositoryRoot, GistIdGenerator idGenerator, - HazelcastInstance hazelcastInstance, ObjectMapper objectMapper) throws IOException { - this.repositoryRoot = new File(repositoryRoot); - if (!this.repositoryRoot.exists()) { - FileUtils.forceMkdir(this.repositoryRoot); - } - recycleRoot = new File(repositoryRoot, RECYCLE_FOLDER_NAME); - if (!this.recycleRoot.exists()) { - FileUtils.forceMkdir(this.recycleRoot); - } - this.idGenerator = idGenerator; - this.hazelcastInstance = hazelcastInstance; - this.objectMapper = objectMapper; - } - - public void setLockTimeout(int timeout) { - this.lockTimeout = timeout; - } - - @Override - public List listGists(UserDetails userDetails) { - List gists = new ArrayList(); - for (File file : FileUtils.listFiles(repositoryRoot, - FileFilterUtils.and(FileFileFilter.FILE, new NameFileFilter(GitGistRepository.GIST_META_JSON_FILE)), - TrueFileFilter.INSTANCE)) { - GistRepository repository = new GitGistRepository(file.getParentFile(), objectMapper); - gists.add(repository.getGist(userDetails)); - } - return gists; - } - - @Override - public GistResponse getGist(String gistId, UserDetails userDetails) { - Lock lock = hazelcastInstance.getLock(gistId); - try { - if (lock.tryLock(lockTimeout, TimeUnit.SECONDS)) { - try { - File repositoryFolder = getAndValidateRepositoryFolder(gistId); - GistRepository repository = new GitGistRepository(repositoryFolder, objectMapper); - return repository.getGist(userDetails); - } finally { - lock.unlock(); - } - } else { - GistError error = new GistError(GistErrorCode.ERR_GIST_CONTENT_NOT_AVAILABLE, - "Could not read gist {}, it is currently being updated", gistId); - logger.error(error.getFormattedMessage()); - throw new GistRepositoryException(error); - } - } catch (InterruptedException e) { - GistError error = new GistError(GistErrorCode.ERR_GIST_CONTENT_NOT_AVAILABLE, - "Could not read gist {}, it is currently being updated", gistId); - logger.error(error.getFormattedMessage()); - throw new GistRepositoryException(error); - } - } - - @Override - public GistResponse getGist(String gistId, String commitId, UserDetails userDetails) { - Lock lock = hazelcastInstance.getLock(gistId); - try { - if (lock.tryLock(lockTimeout, TimeUnit.SECONDS)) { - try { - File repositoryFolder = getAndValidateRepositoryFolder(gistId); - GistRepository repository = new GitGistRepository(repositoryFolder, objectMapper); - return repository.getGist(commitId, userDetails); - } finally { - lock.unlock(); - } - } else { - GistError error = new GistError(GistErrorCode.ERR_GIST_CONTENT_NOT_AVAILABLE, - "Could not read gist {}, it is currently being updated", gistId); - logger.error(error.getFormattedMessage()); - throw new GistRepositoryException(error); - } - } catch (InterruptedException e) { - GistError error = new GistError(GistErrorCode.ERR_GIST_CONTENT_NOT_AVAILABLE, - "Could not read gist {}, it is currently being updated", gistId); - logger.error(error.getFormattedMessage()); - throw new GistRepositoryException(error); - } - } - - @Override - public GistResponse createGist(GistRequest request, UserDetails activeUser) { - String gistId = idGenerator.generateId(); - File repositoryFolder = getRepositoryFolder(gistId); - GistRepository repository = new GitGistRepository(repositoryFolder, gistId, objectMapper, activeUser); - return repository.createGist(request, activeUser); - } - - @Override - public GistResponse forkGist(String gistToForkId, User activeUser) { - Lock lock = hazelcastInstance.getLock(gistToForkId); - try { - if (lock.tryLock(lockTimeout, TimeUnit.SECONDS)) { - try { - File forkedGistRepositoryFolder = getAndValidateRepositoryFolder(gistToForkId); - GistRepository forkedRepository = new GitGistRepository(forkedGistRepositoryFolder, objectMapper); - String gistId = idGenerator.generateId(); - File repositoryFolder = getRepositoryFolder(gistId); - GistRepository repository = new GitGistRepository(repositoryFolder, gistId, objectMapper, activeUser); - return repository.fork(forkedRepository, activeUser); - } finally { - lock.unlock(); - } - } else { - GistError error = new GistError(GistErrorCode.ERR_GIST_CONTENT_NOT_AVAILABLE, - "Could not read gist {}, it is currently being updated", gistToForkId); - logger.error(error.getFormattedMessage()); - throw new GistRepositoryException(error); - } - } catch (InterruptedException e) { - GistError error = new GistError(GistErrorCode.ERR_GIST_CONTENT_NOT_AVAILABLE, - "Could not read gist {}, it is currently being updated", gistToForkId); - logger.error(error.getFormattedMessage()); - throw new GistRepositoryException(error); - } - } - - @Override - public GistResponse editGist(String gistId, GistRequest request, UserDetails activeUser) { - Lock lock = hazelcastInstance.getLock(gistId); - try { - if (lock.tryLock(lockTimeout, TimeUnit.SECONDS)) { - try { - File repositoryFolder = getAndValidateRepositoryFolder(gistId); - GistRepository repository = new GitGistRepository(repositoryFolder, objectMapper); - return repository.editGist(request, activeUser); - } finally { - lock.unlock(); - } - } else { - GistError error = new GistError(GistErrorCode.ERR_GIST_CONTENT_NOT_AVAILABLE, - "Could not update gist {}, it is currently being updated", gistId); - logger.error(error.getFormattedMessage()); - throw new GistRepositoryException(error); - } - } catch (InterruptedException e) { - GistError error = new GistError(GistErrorCode.ERR_GIST_CONTENT_NOT_AVAILABLE, - "Could not update gist {}, it is currently being updated", gistId); - logger.error(error.getFormattedMessage()); - throw new GistRepositoryException(error); - } - } - - @Override - public void deleteGist(String gistId, UserDetails activeUser) { - Lock lock = hazelcastInstance.getLock(gistId); - try { - if (lock.tryLock(lockTimeout, TimeUnit.SECONDS)) { - try { - File repositoryFolder = getAndValidateRepositoryFolder(gistId); - FileUtils.moveDirectoryToDirectory(repositoryFolder, new File(recycleRoot, gistId), true); - FileUtils.forceDelete(repositoryFolder); - } catch (IOException e) { - GistError error = new GistError(GistErrorCode.ERR_GIST_UPDATE_FAILURE, - "Could not delete gist {}, an internal error has occurred", gistId); - logger.error(error.getFormattedMessage()); - throw new GistRepositoryError(error, e); - } finally { - lock.unlock(); - } - } else { - GistError error = new GistError(GistErrorCode.ERR_GIST_CONTENT_NOT_AVAILABLE, - "Could not delete gist {}, it is currently being updated", gistId); - logger.error(error.getFormattedMessage()); - throw new GistRepositoryException(error); - } - - } catch (InterruptedException e) { - GistError error = new GistError(GistErrorCode.ERR_GIST_CONTENT_NOT_AVAILABLE, - "Could not delete gist {}, it is currently being updated", gistId); - logger.error(error.getFormattedMessage()); - throw new GistRepositoryException(error); - } - } - - private File getAndValidateRepositoryFolder(String id) { - - File folder = getRepositoryFolder(id); - if (!folder.exists()) { - GistError error = new GistError(GistErrorCode.ERR_GIST_NOT_EXIST, "Gist with id {} does not exist", id); - logger.error(error.getFormattedMessage()); - throw new GistRepositoryException(error); - } - return folder; - } - - private File getRepositoryFolder(String id) { - File folder = repositoryRoot; - for (String path : REPOSITORYID_FOLDER_SPLITTER.split(id)) { - folder = new File(folder, path); - } - return folder; - } - - @Override - public List getComments(String gistId, UserDetails activeUser) { - Lock lock = hazelcastInstance.getLock(gistId); - try { - if (lock.tryLock(lockTimeout, TimeUnit.SECONDS)) { - try { - File repositoryFolder = getAndValidateRepositoryFolder(gistId); - GistCommentRepository repository = new GitGistCommentRepository(repositoryFolder, gistId, - objectMapper); - return repository.getComments(activeUser); - } finally { - lock.unlock(); - } - } else { - GistError error = new GistError(GistErrorCode.ERR_GIST_CONTENT_NOT_AVAILABLE, - "Could not read comments for gist {}, it is currently being updated", gistId); - logger.error(error.getFormattedMessage()); - throw new GistRepositoryException(error); - } - } catch (InterruptedException e) { - GistError error = new GistError(GistErrorCode.ERR_GIST_CONTENT_NOT_AVAILABLE, - "Could not read comments for gist {}, it is currently being updated", gistId); - logger.error(error.getFormattedMessage()); - throw new GistRepositoryException(error); - } - } - - @Override - public GistCommentResponse getComment(String gistId, long commentId, UserDetails activeUser) { - Lock lock = hazelcastInstance.getLock(gistId); - try { - if (lock.tryLock(lockTimeout, TimeUnit.SECONDS)) { - try { - File repositoryFolder = getAndValidateRepositoryFolder(gistId); - GistCommentRepository repository = new GitGistCommentRepository(repositoryFolder, gistId, - objectMapper); - return repository.getComment(commentId, activeUser); - } finally { - lock.unlock(); - } - } else { - GistError error = new GistError(GistErrorCode.ERR_GIST_CONTENT_NOT_AVAILABLE, - "Could not read comment {} on gist {}, it is currently being updated", commentId, gistId); - logger.error(error.getFormattedMessage()); - throw new GistRepositoryException(error); - } - } catch (InterruptedException e) { - GistError error = new GistError(GistErrorCode.ERR_GIST_CONTENT_NOT_AVAILABLE, - "Could not read comment {} on gist {}, it is currently being updated", commentId, gistId); - logger.error(error.getFormattedMessage()); - throw new GistRepositoryException(error); - } - } - - @Override - public GistCommentResponse createComment(String gistId, GistComment comment, UserDetails activeUser) { - Lock lock = hazelcastInstance.getLock(gistId); - try { - if (lock.tryLock(lockTimeout, TimeUnit.SECONDS)) { - try { - File repositoryFolder = getAndValidateRepositoryFolder(gistId); - GistCommentRepository repository = new GitGistCommentRepository(repositoryFolder, gistId, - objectMapper); - return repository.createComment(comment, activeUser); - } finally { - lock.unlock(); - } - } else { - GistError error = new GistError(GistErrorCode.ERR_GIST_CONTENT_NOT_AVAILABLE, - "Could not create gist {} with new comment, it is currently being updated", gistId); - logger.error(error.getFormattedMessage()); - throw new GistRepositoryException(error); - } - } catch (InterruptedException e) { - GistError error = new GistError(GistErrorCode.ERR_GIST_CONTENT_NOT_AVAILABLE, - "Could not create gist {} with new comment, it is currently being updated", gistId); - logger.error(error.getFormattedMessage()); - throw new GistRepositoryException(error); - } - } - - @Override - public GistCommentResponse editComment(String gistId, long commentId, GistComment comment, UserDetails activeUser) { - Lock lock = hazelcastInstance.getLock(gistId); - try { - if (lock.tryLock(lockTimeout, TimeUnit.SECONDS)) { - try { - File repositoryFolder = getAndValidateRepositoryFolder(gistId); - GistCommentRepository repository = new GitGistCommentRepository(repositoryFolder, gistId, - objectMapper); - return repository.editComment(commentId, comment, activeUser); - } finally { - lock.unlock(); - } - } else { - GistError error = new GistError(GistErrorCode.ERR_GIST_CONTENT_NOT_AVAILABLE, - "Could not edit comment {} for gist {}, it is currently being updated", commentId, gistId); - logger.error(error.getFormattedMessage()); - throw new GistRepositoryException(error); - } - } catch (InterruptedException e) { - GistError error = new GistError(GistErrorCode.ERR_GIST_CONTENT_NOT_AVAILABLE, - "Could not edit comment {} for gist {}, it is currently being updated", commentId, gistId); - logger.error(error.getFormattedMessage()); - throw new GistRepositoryException(error); - } - } - - @Override - public void deleteComment(String gistId, long commentId, UserDetails activeUser) { - Lock lock = hazelcastInstance.getLock(gistId); - try { - if (lock.tryLock(lockTimeout, TimeUnit.SECONDS)) { - try { - File repositoryFolder = getAndValidateRepositoryFolder(gistId); - GistCommentRepository repository = new GitGistCommentRepository(repositoryFolder, gistId, - objectMapper); - repository.deleteComment(commentId, activeUser); - } finally { - lock.unlock(); - } - } else { - GistError error = new GistError(GistErrorCode.ERR_GIST_CONTENT_NOT_AVAILABLE, - "Could not delete comment {} on gist {}, it is currently being updated", commentId, gistId); - logger.error(error.getFormattedMessage()); - throw new GistRepositoryException(error); - } - } catch (InterruptedException e) { - GistError error = new GistError(GistErrorCode.ERR_GIST_CONTENT_NOT_AVAILABLE, - "Could not delete comment {} on gist {}, it is currently being updated", commentId, gistId); - logger.error(error.getFormattedMessage()); - throw new GistRepositoryException(error); - } - } - - - -} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/AsymetricFourFolderRepositoryStorageLocator.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/AsymetricFourFolderRepositoryStorageLocator.java new file mode 100644 index 0000000..db27204 --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/AsymetricFourFolderRepositoryStorageLocator.java @@ -0,0 +1,37 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.repository.git; + +import java.io.File; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class AsymetricFourFolderRepositoryStorageLocator implements RepositoryStorageLocator { + + private static final Pattern REPOSITORYID_FOLDER_PATTERN = Pattern.compile("(.)(.)(.)(.*)"); + + private File root; + + public AsymetricFourFolderRepositoryStorageLocator(File root) { + this.root = root; + } + + @Override + public File getStoragePath(String gistId) { + File path = null; + Matcher matcher = REPOSITORYID_FOLDER_PATTERN.matcher(gistId); + if(matcher.matches()) { + int groups = matcher.groupCount(); + path = root; + for(int i = 1; i <= groups; i++) { + path = new File(path, matcher.group(i)); + } + } + return path; + } + +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/BareAddCommand.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/BareAddCommand.java new file mode 100644 index 0000000..80d92af --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/BareAddCommand.java @@ -0,0 +1,206 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.repository.git; + +import static org.eclipse.jgit.lib.Constants.OBJ_BLOB; +import static org.eclipse.jgit.lib.FileMode.GITLINK; +import static org.eclipse.jgit.lib.FileMode.TYPE_GITLINK; +import static org.eclipse.jgit.lib.FileMode.TYPE_TREE; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Collection; +import java.util.LinkedHashSet; + +import org.eclipse.jgit.api.GitCommand; +import org.eclipse.jgit.api.errors.FilterFailedException; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.JGitInternalException; +import org.eclipse.jgit.api.errors.NoFilepatternException; +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.dircache.DirCacheBuildIterator; +import org.eclipse.jgit.dircache.DirCacheBuilder; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.dircache.DirCacheIterator; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.treewalk.FileTreeIterator; +import org.eclipse.jgit.treewalk.NameConflictTreeWalk; +import org.eclipse.jgit.treewalk.TreeWalk.OperationType; +import org.eclipse.jgit.treewalk.WorkingTreeIterator; +import org.eclipse.jgit.treewalk.filter.PathFilterGroup; + +/** + * Command that adds contents to a bare repository. + * This is based upon the JGit AddCommand + * + */ +public class BareAddCommand extends GitCommand { + + private Collection filepatterns; + + private WorkingTreeIterator workingTreeIterator; + + private boolean update = false; + + private DirCache dirCache; + + + protected BareAddCommand(Repository repo) { + super(repo); + filepatterns = new LinkedHashSet(); + } + + protected BareAddCommand(Repository repo, DirCache dirCache) { + this(repo); + this.dirCache = dirCache; + } + + public BareAddCommand addFilepattern(String filepattern) { + checkCallable(); + filepatterns.add(filepattern); + return this; + } + + public BareAddCommand addDirCache(DirCache dirCache) { + this.dirCache = dirCache; + return this; + } + + /** + * Allow clients to provide their own implementation of a FileTreeIterator + * @param f the iterator for the working tree + * @return {@code this} + */ + public BareAddCommand setWorkingTreeIterator(WorkingTreeIterator f) { + workingTreeIterator = f; + return this; + } + + @Override + public DirCache call() throws GitAPIException { + if (filepatterns.isEmpty()) { + throw new NoFilepatternException(JGitText.get().atLeastOnePatternIsRequired); + } + checkCallable(); + boolean addAll = filepatterns.contains("."); //$NON-NLS-1$ + try (ObjectInserter inserter = repo.newObjectInserter(); + NameConflictTreeWalk tw = new NameConflictTreeWalk(repo)) { + tw.setOperationType(OperationType.CHECKIN_OP); + dirCache.lock(); + DirCacheBuilder builder = dirCache.builder(); + tw.addTree(new DirCacheBuildIterator(builder)); + if (workingTreeIterator == null) + workingTreeIterator = new FileTreeIterator(repo); + workingTreeIterator.setDirCacheIterator(tw, 0); + tw.addTree(workingTreeIterator); + if (!addAll) { + tw.setFilter(PathFilterGroup.createFromStrings(filepatterns)); + } + + byte[] lastAdded = null; + + while (tw.next()) { + DirCacheIterator c = tw.getTree(0, DirCacheIterator.class); + WorkingTreeIterator f = tw.getTree(1, WorkingTreeIterator.class); + if (c == null && f != null && f.isEntryIgnored()) { + // file is not in index but is ignored, do nothing + continue; + } else if (c == null && update) { + // Only update of existing entries was requested. + continue; + } + + DirCacheEntry entry = c != null ? c.getDirCacheEntry() : null; + if (entry != null && entry.getStage() > 0 + && lastAdded != null + && lastAdded.length == tw.getPathLength() + && tw.isPathPrefix(lastAdded, lastAdded.length) == 0) { + // In case of an existing merge conflict the + // DirCacheBuildIterator iterates over all stages of + // this path, we however want to add only one + // new DirCacheEntry per path. + continue; + } + + if (tw.isSubtree() && !tw.isDirectoryFileConflict()) { + tw.enterSubtree(); + continue; + } + + if (f == null) { // working tree file does not exist + if (entry != null + && (!update || GITLINK == entry.getFileMode())) { + builder.add(entry); + } + continue; + } + + if (entry != null && entry.isAssumeValid()) { + // Index entry is marked assume valid. Even though + // the user specified the file to be added JGit does + // not consider the file for addition. + builder.add(entry); + continue; + } + + if ((f.getEntryRawMode() == TYPE_TREE + && f.getIndexFileMode(c) != FileMode.GITLINK) || + (f.getEntryRawMode() == TYPE_GITLINK + && f.getIndexFileMode(c) == FileMode.TREE)) { + // Index entry exists and is symlink, gitlink or file, + // otherwise the tree would have been entered above. + // Replace the index entry by diving into tree of files. + tw.enterSubtree(); + continue; + } + + byte[] path = tw.getRawPath(); + if (entry == null || entry.getStage() > 0) { + entry = new DirCacheEntry(path); + } + FileMode mode = f.getIndexFileMode(c); + entry.setFileMode(mode); + + if (GITLINK != mode) { + entry.setLength(f.getEntryLength()); + entry.setLastModified(f.getEntryLastModified()); + long len = f.getEntryContentLength(); + try (InputStream in = f.openEntryStream()) { + ObjectId id = inserter.insert(OBJ_BLOB, len, in); + entry.setObjectId(id); + } + } else { + entry.setLength(0); + entry.setLastModified(0); + entry.setObjectId(f.getEntryObjectId()); + } + builder.add(entry); + lastAdded = path; + } + inserter.flush(); + builder.commit(); + setCallable(false); + } catch (IOException e) { + Throwable cause = e.getCause(); + if (cause != null && cause instanceof FilterFailedException) + throw (FilterFailedException) cause; + throw new JGitInternalException( + JGitText.get().exceptionCaughtDuringExecutionOfAddCommand, e); + } finally { + if (dirCache != null) { + dirCache.unlock(); + } + } + return dirCache; + + } + +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/BareCommitCommand.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/BareCommitCommand.java new file mode 100644 index 0000000..4cb7735 --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/BareCommitCommand.java @@ -0,0 +1,840 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.repository.git; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.GitCommand; +import org.eclipse.jgit.api.errors.AbortedByHookException; +import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.JGitInternalException; +import org.eclipse.jgit.api.errors.NoFilepatternException; +import org.eclipse.jgit.api.errors.NoHeadException; +import org.eclipse.jgit.api.errors.NoMessageException; +import org.eclipse.jgit.api.errors.UnmergedPathsException; +import org.eclipse.jgit.api.errors.WrongRepositoryStateException; +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.dircache.DirCacheBuildIterator; +import org.eclipse.jgit.dircache.DirCacheBuilder; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.dircache.DirCacheIterator; +import org.eclipse.jgit.errors.UnmergedPathException; +import org.eclipse.jgit.hooks.Hooks; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.CommitBuilder; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectInserter; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.RefUpdate.Result; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.RepositoryState; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevObject; +import org.eclipse.jgit.revwalk.RevTag; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; +import org.eclipse.jgit.treewalk.FileTreeIterator; +import org.eclipse.jgit.treewalk.FileTreeIterator.DefaultFileModeStrategy; +import org.eclipse.jgit.treewalk.FileTreeIterator.FileModeStrategy; +import org.eclipse.jgit.treewalk.FileTreeIterator.NoGitlinksStrategy; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.TreeWalk.OperationType; +import org.eclipse.jgit.treewalk.WorkingTreeOptions; +import org.eclipse.jgit.util.ChangeIdUtil; + +/** + * Commits changes to a a bare git repository. This is + * based upon the JGit CommitCommand + * + */ +public class BareCommitCommand extends GitCommand { + + private PersonIdent author; + + private PersonIdent committer; + + private String message; + + private boolean all; + + private List only = new ArrayList(); + + private boolean[] onlyProcessed; + + private boolean amend; + + private boolean insertChangeId; + + /** + * parents this commit should have. The current HEAD will be in this list + * and also all commits mentioned in .git/MERGE_HEAD + */ + private List parents = new LinkedList(); + + private String reflogComment; + + /** + * Setting this option bypasses the pre-commit and commit-msg hooks. + */ + private boolean noVerify; + + private PrintStream hookOutRedirect; + + private Boolean allowEmpty; + + private DirCache index; + + public File workingFolder; + + /** + * @param repo the git repository + * @param index the index file to use for the commit + */ + protected BareCommitCommand(Repository repo, DirCache index) { + super(repo); + this.index = index; + } + + /** + * Executes the {@code commit} command with all the options and parameters + * collected by the setter methods of this class. Each instance of this + * class should only be used for one invocation of the command (means: one + * call to {@link #call()}) + * + * @return a {@link RevCommit} object representing the successful commit. + * @throws NoHeadException + * when called on a git repo without a HEAD reference + * @throws NoMessageException + * when called without specifying a commit message + * @throws UnmergedPathsException + * when the current index contained unmerged paths (conflicts) + * @throws ConcurrentRefUpdateException + * when HEAD or branch ref is updated concurrently by someone + * else + * @throws WrongRepositoryStateException + * when repository is not in the right state for committing + * @throws AbortedByHookException + * if there are either pre-commit or commit-msg hooks present in + * the repository and one of them rejects the commit. + */ + public RevCommit call() throws GitAPIException, NoHeadException, + NoMessageException, UnmergedPathsException, + ConcurrentRefUpdateException, WrongRepositoryStateException, + AbortedByHookException { + checkCallable(); + Collections.sort(only); + + try (RevWalk rw = new RevWalk(repo)) { + RepositoryState state = repo.getRepositoryState(); + + if (!noVerify) { + Hooks.preCommit(repo, hookOutRedirect).call(); + } + + processOptions(state, rw); + + if (all + && !repo.isBare() + ) { + try (Git git = new Git(repo)) { + git.add() + .addFilepattern(".") //$NON-NLS-1$ + .setUpdate(true).call(); + } catch (NoFilepatternException e) { + // should really not happen + throw new JGitInternalException(e.getMessage(), e); + } + } + + Ref head = repo.findRef(Constants.HEAD); + if (head == null) { + throw new NoHeadException( + JGitText.get().commitOnRepoWithoutHEADCurrentlyNotSupported); + } + + // determine the current HEAD and the commit it is referring to + ObjectId headId = repo.resolve(Constants.HEAD + "^{commit}"); //$NON-NLS-1$ + if (headId == null && amend) + throw new WrongRepositoryStateException( + JGitText.get().commitAmendOnInitialNotPossible); + + if (headId != null) { + if (amend) { + RevCommit previousCommit = rw.parseCommit(headId); + for (RevCommit p : previousCommit.getParents()) + parents.add(p.getId()); + if (author == null) + author = previousCommit.getAuthorIdent(); + } else { + parents.add(0, headId); + } + } + if (!noVerify) { + message = Hooks.commitMsg(repo, hookOutRedirect) + .setCommitMessage(message).call(); + } + + // lock the index +// DirCache index = repo.lockDirCache(); + index.lock(); + try (ObjectInserter odi = repo.newObjectInserter()) { + if (!only.isEmpty()) + index = createTemporaryIndex(headId, index, rw); + + // Write the index as tree to the object database. This may + // fail for example when the index contains unmerged paths + // (unresolved conflicts) + ObjectId indexTreeId = index.writeTree(odi); + + if (insertChangeId) + insertChangeId(indexTreeId); + + // Check for empty commits + if (headId != null && !allowEmpty.booleanValue()) { + RevCommit headCommit = rw.parseCommit(headId); + headCommit.getTree(); + if (indexTreeId.equals(headCommit.getTree())) { + return null; + } + } + + // Create a Commit object, populate it and write it + CommitBuilder commit = new CommitBuilder(); + commit.setCommitter(committer); + commit.setAuthor(author); + commit.setMessage(message); + + commit.setParentIds(parents); + commit.setTreeId(indexTreeId); + ObjectId commitId = odi.insert(commit); + odi.flush(); + + RevCommit revCommit = rw.parseCommit(commitId); + RefUpdate ru = repo.updateRef(Constants.HEAD); + ru.setNewObjectId(commitId); + if (reflogComment != null) { + ru.setRefLogMessage(reflogComment, false); + } else { + String prefix = amend ? "commit (amend): " //$NON-NLS-1$ + : parents.size() == 0 ? "commit (initial): " //$NON-NLS-1$ + : "commit: "; //$NON-NLS-1$ + ru.setRefLogMessage(prefix + revCommit.getShortMessage(), + false); + } + if (headId != null) { + ru.setExpectedOldObjectId(headId); + } else { + ru.setExpectedOldObjectId(ObjectId.zeroId()); + } + Result rc = ru.forceUpdate(); + switch (rc) { + case NEW: + case FORCED: + case FAST_FORWARD: { + setCallable(false); + if (state == RepositoryState.MERGING_RESOLVED + || isMergeDuringRebase(state)) { + // Commit was successful. Now delete the files + // used for merge commits + repo.writeMergeCommitMsg(null); + repo.writeMergeHeads(null); + } else if (state == RepositoryState.CHERRY_PICKING_RESOLVED) { + repo.writeMergeCommitMsg(null); + repo.writeCherryPickHead(null); + } else if (state == RepositoryState.REVERTING_RESOLVED) { + repo.writeMergeCommitMsg(null); + repo.writeRevertHead(null); + } + return revCommit; + } + case REJECTED: + case LOCK_FAILURE: + throw new ConcurrentRefUpdateException( + JGitText.get().couldNotLockHEAD, ru.getRef(), rc); + default: + throw new JGitInternalException(MessageFormat.format( + JGitText.get().updatingRefFailed, Constants.HEAD, + commitId.toString(), rc)); + } + } finally { + index.unlock(); + } + } catch (UnmergedPathException e) { + throw new UnmergedPathsException(e); + } catch (IOException e) { + throw new JGitInternalException( + JGitText.get().exceptionCaughtDuringExecutionOfCommitCommand, e); + } + } + + private void insertChangeId(ObjectId treeId) { + ObjectId firstParentId = null; + if (!parents.isEmpty()) { + firstParentId = parents.get(0); + } + ObjectId changeId = ChangeIdUtil.computeChangeId(treeId, firstParentId, + author, committer, message); + message = ChangeIdUtil.insertId(message, changeId); + if (changeId != null) { + message = message.replaceAll("\nChange-Id: I" //$NON-NLS-1$ + + ObjectId.zeroId().getName() + "\n", "\nChange-Id: I" //$NON-NLS-1$ //$NON-NLS-2$ + + changeId.getName() + "\n"); //$NON-NLS-1$ + } + } + + private DirCache createTemporaryIndex(ObjectId headId, DirCache index, + RevWalk rw) + throws IOException { + ObjectInserter inserter = null; + + // get DirCacheBuilder for existing index + DirCacheBuilder existingBuilder = index.builder(); + + // get DirCacheBuilder for newly created in-core index to build a + // temporary index for this commit + DirCache inCoreIndex = DirCache.newInCore(); + DirCacheBuilder tempBuilder = inCoreIndex.builder(); + + onlyProcessed = new boolean[only.size()]; + boolean emptyCommit = true; + + try (TreeWalk treeWalk = new TreeWalk(repo)) { + treeWalk.setOperationType(OperationType.CHECKIN_OP); + int dcIdx = treeWalk + .addTree(new DirCacheBuildIterator(existingBuilder)); + + FileModeStrategy fileModeStrategy = this.getRepository().getConfig().get(WorkingTreeOptions.KEY).isDirNoGitLinks() ? + NoGitlinksStrategy.INSTANCE : + DefaultFileModeStrategy.INSTANCE; + + + FileTreeIterator fti = new FileTreeIterator( + this.workingFolder, this.getRepository().getFS(), + this.getRepository().getConfig().get(WorkingTreeOptions.KEY), fileModeStrategy); + + + + fti.setDirCacheIterator(treeWalk, 0); + int fIdx = treeWalk.addTree(fti); + int hIdx = -1; + if (headId != null) { + hIdx = treeWalk.addTree(rw.parseTree(headId)); + } + treeWalk.setRecursive(true); + + String lastAddedFile = null; + while (treeWalk.next()) { + String path = treeWalk.getPathString(); + // check if current entry's path matches a specified path + int pos = lookupOnly(path); + + CanonicalTreeParser hTree = null; + if (hIdx != -1) { + hTree = treeWalk.getTree(hIdx, CanonicalTreeParser.class); + } + + DirCacheIterator dcTree = treeWalk.getTree(dcIdx, + DirCacheIterator.class); + + if (pos >= 0) { + // include entry in commit + + FileTreeIterator fTree = treeWalk.getTree(fIdx, + FileTreeIterator.class); + + // check if entry refers to a tracked file + boolean tracked = dcTree != null || hTree != null; + if (!tracked) { + continue; + } + + // for an unmerged path, DirCacheBuildIterator will yield 3 + // entries, we only want to add one + if (path.equals(lastAddedFile)) { + continue; + } + + lastAddedFile = path; + + if (fTree != null) { + // create a new DirCacheEntry with data retrieved from + // disk + final DirCacheEntry dcEntry = new DirCacheEntry(path); + long entryLength = fTree.getEntryLength(); + dcEntry.setLength(entryLength); + dcEntry.setLastModified(fTree.getEntryLastModified()); + dcEntry.setFileMode(fTree.getIndexFileMode(dcTree)); + + boolean objectExists = (dcTree != null + && fTree.idEqual(dcTree)) + || (hTree != null && fTree.idEqual(hTree)); + if (objectExists) { + dcEntry.setObjectId(fTree.getEntryObjectId()); + } else { + if (FileMode.GITLINK.equals(dcEntry.getFileMode())) { + dcEntry.setObjectId(fTree.getEntryObjectId()); + } else { + // insert object + if (inserter == null) { + inserter = repo.newObjectInserter(); + } + long contentLength = fTree + .getEntryContentLength(); + InputStream inputStream = fTree + .openEntryStream(); + try { + dcEntry.setObjectId(inserter.insert( + Constants.OBJ_BLOB, contentLength, + inputStream)); + } finally { + inputStream.close(); + } + } + } + + // add to existing index + existingBuilder.add(dcEntry); + // add to temporary in-core index + tempBuilder.add(dcEntry); + + if (emptyCommit + && (hTree == null || !hTree.idEqual(fTree) + || hTree.getEntryRawMode() != fTree + .getEntryRawMode())) { + // this is a change + emptyCommit = false; + } + } else { + // if no file exists on disk, neither add it to + // index nor to temporary in-core index + + if (emptyCommit && hTree != null) { + // this is a change + emptyCommit = false; + } + } + + // keep track of processed path + onlyProcessed[pos] = true; + } else { + // add entries from HEAD for all other paths + if (hTree != null) { + // create a new DirCacheEntry with data retrieved from + // HEAD + final DirCacheEntry dcEntry = new DirCacheEntry(path); + dcEntry.setObjectId(hTree.getEntryObjectId()); + dcEntry.setFileMode(hTree.getEntryFileMode()); + + // add to temporary in-core index + tempBuilder.add(dcEntry); + } + + // preserve existing entry in index + if (dcTree != null) { + existingBuilder.add(dcTree.getDirCacheEntry()); + } + } + } + } + + // there must be no unprocessed paths left at this point; otherwise an + // untracked or unknown path has been specified + for (int i = 0; i < onlyProcessed.length; i++) { + if (!onlyProcessed[i]) { + throw new JGitInternalException(MessageFormat.format( + JGitText.get().entryNotFoundByPath, only.get(i))); + } + } + + // there must be at least one change + if (emptyCommit) { + // Would like to throw a EmptyCommitException. But this would break the API + // TODO(ch): Change this in the next release +// throw new JGitInternalException(JGitText.get().emptyCommit); + } + + // update index + existingBuilder.commit(); + // finish temporary in-core index used for this commit + tempBuilder.finish(); + return inCoreIndex; + } + + /** + * Look an entry's path up in the list of paths specified by the --only/ -o + * option + * + * In case the complete (file) path (e.g. "d1/d2/f1") cannot be found in + * only, lookup is also tried with (parent) directory paths + * (e.g. "d1/d2" and "d1"). + * + * @param pathString the entry's path + * @return the item's index in only; -1 if no item matches + */ + private int lookupOnly(String pathString) { + String p = pathString; + while (true) { + int position = Collections.binarySearch(only, p); + if (position >= 0) { + return position; + } + int l = p.lastIndexOf("/"); //$NON-NLS-1$ + if (l < 1) { + break; + } + p = p.substring(0, l); + } + return -1; + } + + /** + * Sets default values for not explicitly specified options. Then validates + * that all required data has been provided. + * + * @param state + * the state of the repository we are working on + * @param rw + * the RevWalk to use + * + * @throws NoMessageException + * if the commit message has not been specified + */ + private void processOptions(RepositoryState state, RevWalk rw) + throws NoMessageException { + if (committer == null) { + committer = new PersonIdent(repo); + } + if (author == null && !amend) { + author = committer; + } + if (allowEmpty == null) { + // JGit allows empty commits by default. Only when pathes are + // specified the commit should not be empty. This behaviour differs + // from native git but can only be adapted in the next release. + // TODO(ch) align the defaults with native git + allowEmpty = (only.isEmpty()) ? Boolean.TRUE : Boolean.FALSE; + } + // when doing a merge commit parse MERGE_HEAD and MERGE_MSG files + if (state == RepositoryState.MERGING_RESOLVED + || isMergeDuringRebase(state)) { + try { + parents = repo.readMergeHeads(); + if (parents != null) { + for (int i = 0; i < parents.size(); i++) { + RevObject ro = rw.parseAny(parents.get(i)); + if (ro instanceof RevTag) + parents.set(i, rw.peel(ro)); + } + } + } catch (IOException e) { + throw new JGitInternalException(MessageFormat.format( + JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR, + Constants.MERGE_HEAD, e), e); + } + if (message == null) { + try { + message = repo.readMergeCommitMsg(); + } catch (IOException e) { + throw new JGitInternalException(MessageFormat.format( + JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR, + Constants.MERGE_MSG, e), e); + } + } + } else if (state == RepositoryState.SAFE && message == null) { + try { + message = repo.readSquashCommitMsg(); + if (message != null) { + repo.writeSquashCommitMsg(null /* delete */); + } + } catch (IOException e) { + throw new JGitInternalException(MessageFormat.format( + JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR, + Constants.MERGE_MSG, e), e); + } + + } + if (message == null) { + // as long as we don't support -C option we have to have + // an explicit message + throw new NoMessageException(JGitText.get().commitMessageNotSpecified); + } + } + + private boolean isMergeDuringRebase(RepositoryState state) { + if (state != RepositoryState.REBASING_INTERACTIVE + && state != RepositoryState.REBASING_MERGE) { + return false; + } + try { + return repo.readMergeHeads() != null; + } catch (IOException e) { + throw new JGitInternalException(MessageFormat.format( + JGitText.get().exceptionOccurredDuringReadingOfGIT_DIR, + Constants.MERGE_HEAD, e), e); + } + } + + /** + * @param message + * the commit message used for the {@code commit} + * @return {@code this} + */ + public BareCommitCommand setMessage(String message) { + checkCallable(); + this.message = message; + return this; + } + + /** + * @param allowEmpty + * whether it should be allowed to create a commit which has the + * same tree as it's sole predecessor (a commit which doesn't + * change anything). By default when creating standard commits + * (without specifying paths) JGit allows to create such commits. + * When this flag is set to false an attempt to create an "empty" + * standard commit will lead to an EmptyCommitException. + *

+ * By default when creating a commit containing only specified + * paths an attempt to create an empty commit leads to a + * {@link JGitInternalException}. By setting this flag to + * true this exception will not be thrown. + * @return {@code this} + * @since 4.2 + */ + public BareCommitCommand setAllowEmpty(boolean allowEmpty) { + this.allowEmpty = Boolean.valueOf(allowEmpty); + return this; + } + + /** + * @return the commit message used for the commit + */ + public String getMessage() { + return message; + } + + /** + * Sets the committer for this {@code commit}. If no committer is explicitly + * specified because this method is never called or called with {@code null} + * value then the committer will be deduced from config info in repository, + * with current time. + * + * @param committer + * the committer used for the {@code commit} + * @return {@code this} + */ + public BareCommitCommand setCommitter(PersonIdent committer) { + checkCallable(); + this.committer = committer; + return this; + } + + /** + * Sets the committer for this {@code commit}. If no committer is explicitly + * specified because this method is never called then the committer will be + * deduced from config info in repository, with current time. + * + * @param name + * the name of the committer used for the {@code commit} + * @param email + * the email of the committer used for the {@code commit} + * @return {@code this} + */ + public BareCommitCommand setCommitter(String name, String email) { + checkCallable(); + return setCommitter(new PersonIdent(name, email)); + } + + /** + * @return the committer used for the {@code commit}. If no committer was + * specified {@code null} is returned and the default + * {@link PersonIdent} of this repo is used during execution of the + * command + */ + public PersonIdent getCommitter() { + return committer; + } + + /** + * Sets the author for this {@code commit}. If no author is explicitly + * specified because this method is never called or called with {@code null} + * value then the author will be set to the committer or to the original + * author when amending. + * + * @param author + * the author used for the {@code commit} + * @return {@code this} + */ + public BareCommitCommand setAuthor(PersonIdent author) { + checkCallable(); + this.author = author; + return this; + } + + /** + * Sets the author for this {@code commit}. If no author is explicitly + * specified because this method is never called then the author will be set + * to the committer or to the original author when amending. + * + * @param name + * the name of the author used for the {@code commit} + * @param email + * the email of the author used for the {@code commit} + * @return {@code this} + */ + public BareCommitCommand setAuthor(String name, String email) { + checkCallable(); + return setAuthor(new PersonIdent(name, email)); + } + + /** + * @return the author used for the {@code commit}. If no author was + * specified {@code null} is returned and the default + * {@link PersonIdent} of this repo is used during execution of the + * command + */ + public PersonIdent getAuthor() { + return author; + } + + /** + * If set to true the Commit command automatically stages files that have + * been modified and deleted, but new files not known by the repository are + * not affected. This corresponds to the parameter -a on the command line. + * + * @param all true to stage all files. + * @return {@code this} + * @throws JGitInternalException + * in case of an illegal combination of arguments/ options + */ + public BareCommitCommand setAll(boolean all) { + checkCallable(); + if (all && !only.isEmpty()) { + throw new JGitInternalException(MessageFormat.format( + JGitText.get().illegalCombinationOfArguments, "--all", //$NON-NLS-1$ + "--only")); //$NON-NLS-1$ + } + this.all = all; + return this; + } + + /** + * Used to amend the tip of the current branch. If set to true, the previous + * commit will be amended. This is equivalent to --amend on the command + * line. + * + * @param amend true to amend the tip of the current branch + * @return {@code this} + */ + public BareCommitCommand setAmend(boolean amend) { + checkCallable(); + this.amend = amend; + return this; + } + + /** + * Commit dedicated path only. + *

+ * This method can be called several times to add multiple paths. Full file + * paths are supported as well as directory paths; in the latter case this + * commits all files/directories below the specified path. + * + * @param only + * path to commit (with / as separator) + * @return {@code this} + */ + public BareCommitCommand setOnly(String only) { + checkCallable(); + if (all) { + throw new JGitInternalException(MessageFormat.format( + JGitText.get().illegalCombinationOfArguments, "--only", //$NON-NLS-1$ + "--all")); //$NON-NLS-1$ + } + String o = only.endsWith("/") ? only.substring(0, only.length() - 1) //$NON-NLS-1$ + : only; + // ignore duplicates + if (!this.only.contains(o)) { + this.only.add(o); + } + return this; + } + + /** + * If set to true a change id will be inserted into the commit message + * + * An existing change id is not replaced. An initial change id (I000...) + * will be replaced by the change id. + * + * @param insertChangeId true to add the change id + * + * @return {@code this} + */ + public BareCommitCommand setInsertChangeId(boolean insertChangeId) { + checkCallable(); + this.insertChangeId = insertChangeId; + return this; + } + + /** + * Override the message written to the reflog + * + * @param reflogComment the comment + * @return {@code this} + */ + public BareCommitCommand setReflogComment(String reflogComment) { + this.reflogComment = reflogComment; + return this; + } + + /** + * Sets the {@link #noVerify} option on this commit command. + *

+ * Both the pre-commit and commit-msg hooks can block a commit by their + * return value; setting this option to true will bypass these + * two hooks. + *

+ * + * @param noVerify + * Whether this commit should be verified by the pre-commit and + * commit-msg hooks. + * @return {@code this} + * @since 3.7 + */ + public BareCommitCommand setNoVerify(boolean noVerify) { + this.noVerify = noVerify; + return this; + } + + /** + * Set the output stream for hook scripts executed by this command. If not + * set it defaults to {@code System.out}. + * + * @param hookStdOut + * the output stream for hook scripts executed by this command + * @return {@code this} + * @since 3.7 + */ + public BareCommitCommand setHookOutputStream(PrintStream hookStdOut) { + this.hookOutRedirect = hookStdOut; + return this; + } + +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/BareRmCommand.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/BareRmCommand.java new file mode 100644 index 0000000..fa2c4b2 --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/BareRmCommand.java @@ -0,0 +1,141 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.repository.git; + +import java.io.File; +import java.io.IOException; +import java.util.Collection; +import java.util.LinkedHashSet; + +import org.eclipse.jgit.api.GitCommand; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.JGitInternalException; +import org.eclipse.jgit.api.errors.NoFilepatternException; +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.dircache.DirCacheBuildIterator; +import org.eclipse.jgit.dircache.DirCacheBuilder; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.FileMode; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.eclipse.jgit.treewalk.filter.PathFilterGroup; + +/** + * Removes files from the bare git repository. This is based + * upon the JGit RmCommand. + */ +public class BareRmCommand extends GitCommand { + + /** + * patterns of files that should be removed from the repository. + */ + private Collection filepatterns; + + /** + * The index file to update with the changes. + */ + private DirCache index; + + /** Only remove files from index, not from working directory */ + private boolean cached = true; + + /** + * Creates the command to perform the operation on the repository + * @param repo the repository to operate on. + * @param index the index file to change. + */ + public BareRmCommand(Repository repo, DirCache index) { + super(repo); + filepatterns = new LinkedHashSet(); + this.index = index; + } + + /** + * Adds file patterns to be processed. + * @param filepattern + * repository-relative path of file to remove (with + * / as separator) + * @return {@code this} + */ + public BareRmCommand addFilepattern(String filepattern) { + checkCallable(); + filepatterns.add(filepattern); + return this; + } + + /** + * Only remove the specified files from the index. + * + * @param cached + * true if files should only be removed from index, false if + * files should also be deleted from the working directory + * @return {@code this} + * @since 2.2 + */ + public BareRmCommand setCached(boolean cached) { + checkCallable(); + this.cached = cached; + return this; + } + + /** + * Executes the {@code Rm} command. Each instance of this class should only + * be used for one invocation of the command. Don't call this method twice + * on an instance. + * + * @return the DirCache after Rm + */ + public DirCache call() throws GitAPIException, + NoFilepatternException { + + if (filepatterns.isEmpty()) { + throw new NoFilepatternException(JGitText.get().atLeastOnePatternIsRequired); + } + checkCallable(); + + try (final TreeWalk tw = new TreeWalk(repo)) { + index.lock(); + DirCacheBuilder builder = index.builder(); + tw.reset(); // drop the first empty tree, which we do not need here + tw.setRecursive(true); + tw.setFilter(PathFilterGroup.createFromStrings(filepatterns)); + tw.addTree(new DirCacheBuildIterator(builder)); + + while (tw.next()) { + if (!cached) { + final FileMode mode = tw.getFileMode(0); + if (mode.getObjectType() == Constants.OBJ_BLOB) { + final File path = new File(repo.getWorkTree(), + tw.getPathString()); + // Deleting a blob is simply a matter of removing + // the file or symlink named by the tree entry. + delete(path); + } + } + } + builder.commit(); + setCallable(false); + } catch (IOException e) { + throw new JGitInternalException( + JGitText.get().exceptionCaughtDuringExecutionOfRmCommand, e); + } finally { + if (index != null) { + index.unlock(); + } + } + + return index; + } + + private void delete(File p) { + while (p != null && !p.equals(repo.getWorkTree()) && p.delete()) { + p = p.getParentFile(); + } + } + +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/CommentStore.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/CommentStore.java new file mode 100644 index 0000000..b3983ea --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/CommentStore.java @@ -0,0 +1,20 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.repository.git; + +import java.io.File; +import java.util.List; + +import com.mangosolutions.rcloud.rawgist.model.GistCommentResponse; + +public interface CommentStore { + + List load(File store); + + List save(File store, List comments); + +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/CreateOrUpdateGistOperation.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/CreateOrUpdateGistOperation.java new file mode 100644 index 0000000..7223b58 --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/CreateOrUpdateGistOperation.java @@ -0,0 +1,332 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.repository.git; + +import java.io.File; +import java.io.IOException; +import java.util.Map; + +import org.ajoberstar.grgit.Grgit; +import org.ajoberstar.grgit.operation.OpenOp; +import org.apache.commons.codec.CharEncoding; +import org.apache.commons.io.FileUtils; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.treewalk.FileTreeIterator; +import org.eclipse.jgit.treewalk.FileTreeIterator.DefaultFileModeStrategy; +import org.eclipse.jgit.treewalk.FileTreeIterator.FileModeStrategy; +import org.eclipse.jgit.treewalk.FileTreeIterator.NoGitlinksStrategy; +import org.eclipse.jgit.treewalk.WorkingTreeOptions; +import org.eclipse.jgit.util.FS; +import org.joda.time.DateTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.util.StringUtils; + +import com.mangosolutions.rcloud.rawgist.model.FileContent; +import com.mangosolutions.rcloud.rawgist.model.FileDefinition; +import com.mangosolutions.rcloud.rawgist.model.GistRequest; +import com.mangosolutions.rcloud.rawgist.model.GistResponse; +import com.mangosolutions.rcloud.rawgist.repository.GistError; +import com.mangosolutions.rcloud.rawgist.repository.GistErrorCode; +import com.mangosolutions.rcloud.rawgist.repository.GistRepositoryException; + +/** + * Operation that creates a gist or updates an existing gist + * from the details of the provided request. + */ +public class CreateOrUpdateGistOperation extends ReadGistOperation { + + private static final Logger logger = LoggerFactory.getLogger(CreateOrUpdateGistOperation.class); + + private GistRequest gistRequest; + + private GistResponse preChangeResponse; + + public CreateOrUpdateGistOperation(RepositoryLayout layout, String gistId, GistRequest gistRequest, UserDetails user) { + super(layout, gistId, user); + this.gistRequest = gistRequest; + } + + public CreateOrUpdateGistOperation(File repositoryFolder, String gistId, GistRequest gistRequest, UserDetails user) { + this(new RepositoryLayout(repositoryFolder), gistId, gistRequest, user); + } + + @Override + public GistResponse call() { + + try (Grgit git = openRepository()) { + preChangeResponse = this.readGist(git); + } + + try (Grgit git = createOrOpenWorkingCopy()) { + createMetadata(); + saveContent(git); + } + + try (Grgit git = openRepository()) { + return this.readGist(git); + } + + } + + private Grgit createOrOpenWorkingCopy() { + Grgit git = openRepository(); + git = initialiseRepo(git); + cleanRepository(git); + return git; + + } + + private Grgit initialiseRepo(Grgit git) { + Git jgit = git.getRepository().getJgit(); + if(jgit.getRepository().isBare()) { + OpenOp openOp = new OpenOp(); + openOp.setDir(this.getLayout().getBareFolder()); + git = openOp.call(); + } + File workingFolder = getWorkTree(git); + if(!workingFolder.exists()) { + try { + FileUtils.forceMkdir(workingFolder); + } catch (IOException e) { + GistError error = new GistError(GistErrorCode.FATAL_GIST_INITIALISATION, + "Could not initialise gist repository with id {}", this.getGistId()); + logger.error(error.getFormattedMessage() + " with folder path {}", workingFolder, e); + throw new GistRepositoryException(error, e); + } + } + return git; + } + + private File getWorkTree(Grgit git) { + if(git.getRepository().getJgit().getRepository().isBare()) { + return this.getLayout().getWorkingFolder(); + } + return git.getRepository().getJgit().getRepository().getWorkTree(); + } + + private Grgit openRepository() { + OpenOp openOp = new OpenOp(); + openOp.setDir(this.getLayout().getBareFolder()); + Grgit git = openOp.call(); + return git; + } + + private void saveContent(Grgit git) { + RepositoryLayout layout = this.getLayout(); + UserDetails userDetails = this.getUser(); + Map files = this.gistRequest.getFiles(); + String gistId = this.getGistId(); + try { + applyFileChanges(git, layout, files, gistId, userDetails); + this.updateMetadata(this.gistRequest); + } finally { + cleanRepository(git); + } + + } + + private void cleanRepository(Grgit git) { + try { + FileUtils.cleanDirectory(this.getLayout().getWorkingFolder()); + } catch (IOException e) { + logger.warn("Couldn't clean working directory for gist {}", this.getGistId(), e); + } + } + + private DirCache getIndex(Grgit git) { + File indexFile = new File(this.getLayout().getWorkingFolder(), ".index"); + DirCache index = new DirCache(indexFile, FS.detect()); + return index; + } + + private void applyFileChanges(Grgit git, RepositoryLayout layout, Map files, String gistId, UserDetails userDetails) { + DirCache index = getIndex(git); + BareAddCommand addCommand = null; + BareRmCommand rmCommand = null; + BareCommitCommand commitCommand = new BareCommitCommand(git.getRepository().getJgit().getRepository(), index); + for (Map.Entry file : files.entrySet()) { + String filename = file.getKey(); + File workingFolder = getWorkTree(git); + FileDefinition definition = file.getValue(); + if (isDelete(definition)) { + commitCommand.setOnly(filename); + rmCommand = applyRmPath(rmCommand, filename, git, index); + } + if (isUpdate(definition)) { + updateFile(workingFolder, gistId, filename, definition); + commitCommand.setOnly(filename); + addCommand = applyAddPath(addCommand, filename, workingFolder, git, index); + } + + if (isMove(filename, definition)) { + moveFile(workingFolder, gistId, filename, definition); + rmCommand = applyRmPath(rmCommand, filename, git, index); + commitCommand.setOnly(filename); + addCommand = applyAddPath(addCommand, definition.getFilename(), workingFolder, git, index); + commitCommand.setOnly(definition.getFilename()); + } + } + try { + if(addCommand != null) { + FileTreeIterator it = this.getFileTreeIterator(git.getRepository().getJgit(), this.getWorkTree(git)); + addCommand.setWorkingTreeIterator(it); + addCommand.call(); + } + if(rmCommand != null) { + rmCommand.call(); + } + try { + if(addCommand != null || rmCommand != null) { + commitCommand.workingFolder = this.getWorkTree(git); + commitCommand.setAuthor(userDetails.getUsername(), ""); + commitCommand.setCommitter(userDetails.getUsername(), ""); + commitCommand.setMessage(""); + commitCommand.setNoVerify(true); + commitCommand.call(); + } + } catch (GitAPIException e) { + throw new RuntimeException(e); + } + } catch (GitAPIException e) { + GistError error = new GistError(GistErrorCode.ERR_GIST_UPDATE_FAILURE, + "Could not update gist with id {}", this.getGistId()); + logger.error(error.getFormattedMessage() + " with folder path {}", this.getWorkTree(git), e); + throw new GistRepositoryException(error, e); + } + } + + private BareRmCommand applyRmPath(BareRmCommand rmCommand, String filename, Grgit git, DirCache index) { + if(rmCommand == null) { + Git jgit = git.getRepository().getJgit(); + rmCommand = new BareRmCommand(jgit.getRepository(), index); + } + return rmCommand.addFilepattern(filename); + } + + + + private BareAddCommand applyAddPath(BareAddCommand addCommand, String filename, File workingFolder, Grgit git, DirCache index) { + if(addCommand == null) { + Git jgit = git.getRepository().getJgit(); + + FileTreeIterator iterator = getFileTreeIterator(jgit, workingFolder); + addCommand = new BareAddCommand(jgit.getRepository()); + addCommand.setWorkingTreeIterator(iterator); + addCommand.addDirCache(index); + } + return addCommand.addFilepattern(filename); + } + + private FileTreeIterator getFileTreeIterator(Git jgit, File workingFolder) { + + FileModeStrategy fileModeStrategy = jgit.getRepository().getConfig().get(WorkingTreeOptions.KEY).isDirNoGitLinks() ? + NoGitlinksStrategy.INSTANCE : + DefaultFileModeStrategy.INSTANCE; + + + FileTreeIterator iterator = new FileTreeIterator( + workingFolder, jgit.getRepository().getFS(), + jgit.getRepository().getConfig().get(WorkingTreeOptions.KEY), fileModeStrategy); + + return iterator; + } + + private void moveFile(File workingFolder, String gistId, String filename, FileDefinition definition) { + File oldFile = new File(workingFolder, filename); + File newFile = new File(workingFolder, definition.getFilename()); + if (!oldFile.equals(newFile)) { + try { + FileContent oldFileContent = this.preChangeResponse.getFiles().get(filename); + if(oldFileContent != null) { + String content = definition.getContent(); + if(StringUtils.isEmpty(content)) { + content = oldFileContent.getContent(); + } + FileUtils.write(newFile, content, + CharEncoding.UTF_8); + } + } catch (IOException e) { + GistError error = new GistError(GistErrorCode.ERR_GIST_UPDATE_FAILURE, + "Could not move {} to {} for gist {}", filename, definition.getFilename(), + gistId); + logger.error(error.getFormattedMessage() + " with folder path {}", workingFolder); + throw new GistRepositoryException(error, e); + } + } + } + + private void updateFile(File workingFolder, String gistId, String filename, FileDefinition definition) { + try { + FileUtils.write(new File(workingFolder, filename), definition.getContent(), + CharEncoding.UTF_8); + } catch (IOException e) { + GistError error = new GistError(GistErrorCode.ERR_GIST_UPDATE_FAILURE, + "Could not update {} for gist {}", filename, gistId); + logger.error(error.getFormattedMessage() + " with folder path {}", workingFolder); + throw new GistRepositoryException(error, e); + } + } + + private boolean isMove(String filename, FileDefinition definition) { + return definition != null && !StringUtils.isEmpty(definition.getFilename()) && !filename.equals(definition.getFilename()); + } + + private boolean isUpdate(FileDefinition definition) { + return definition != null && definition.getContent() != null; + } + + private boolean isDelete(FileDefinition definition) { + return definition == null; + } + + private void createMetadata() { + GistMetadata metadata = getMetadata(); + metadata.setId(this.getGistId()); + if (this.getUser() != null && StringUtils.isEmpty(metadata.getOwner())) { + metadata.setOwner(this.getUser().getUsername()); + } + this.saveMetadata(metadata); + } + + private void updateMetadata(GistRequest request) { + GistMetadata metadata = getMetadata(); + if (request != null) { + String description = request.getDescription(); + + if (description != null) { + metadata.setDescription(description); + } + + if (metadata.getCreatedAt() == null) { + metadata.setCreatedAt(new DateTime()); + } + + if (request.getPublic() != null) { + metadata.setPublic(request.getPublic()); + } + + metadata.setUpdatedAt(new DateTime()); + } + this.saveMetadata(metadata); + } + + private void saveMetadata(GistMetadata metadata) { + this.getMetadataStore().save(this.getLayout().getMetadataFile(), metadata); + } + + public GistRequest getGistRequest() { + return gistRequest; + } + + public void setGistRequest(GistRequest gistRequest) { + this.gistRequest = gistRequest; + } +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/DefaultFileContentCache.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/DefaultFileContentCache.java new file mode 100644 index 0000000..eb5a74f --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/DefaultFileContentCache.java @@ -0,0 +1,30 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.repository.git; + +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Component; + +import com.mangosolutions.rcloud.rawgist.model.FileContent; + +@Component +public class DefaultFileContentCache implements FileContentCache { + + @Override + @Cacheable(value = "filecontentcache", key = "{#contentId, #path}") + public FileContent load(String contentId, String path) { + return null; + } + + @Override + @CachePut(cacheNames = "filecontentcache", key = "{#contentId, #path}") + public FileContent save(String contentId, String path, FileContent content) { + return content; + } + +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/DefaultHistoryCache.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/DefaultHistoryCache.java new file mode 100644 index 0000000..b937223 --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/DefaultHistoryCache.java @@ -0,0 +1,33 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.repository.git; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Component; + +import com.mangosolutions.rcloud.rawgist.model.GistHistory; + +@Component +public class DefaultHistoryCache implements HistoryCache { + + @Override + @Cacheable(value = "historystore", key = "#commitId") + public List load(String commitId) { + return new ArrayList(); + } + + @Override + @CachePut(cacheNames = "historystore", key = "#commitId") + public List save(String commitId, List history) { + return history; + } + +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/FileContentCache.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/FileContentCache.java new file mode 100644 index 0000000..04b1584 --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/FileContentCache.java @@ -0,0 +1,17 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.repository.git; + +import com.mangosolutions.rcloud.rawgist.model.FileContent; + +public interface FileContentCache { + + FileContent load(String contentId, String path); + + FileContent save(String contentId, String path, FileContent content); + +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/ForkGistOperation.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/ForkGistOperation.java new file mode 100644 index 0000000..30974db --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/ForkGistOperation.java @@ -0,0 +1,130 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.repository.git; + +import java.io.File; +import java.io.IOException; +import java.util.Collection; + +import org.ajoberstar.grgit.Grgit; +import org.ajoberstar.grgit.operation.CloneOp; +import org.ajoberstar.grgit.operation.OpenOp; +import org.apache.commons.io.FileUtils; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.RemoteRemoveCommand; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.joda.time.DateTime; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.userdetails.UserDetails; + +import com.mangosolutions.rcloud.rawgist.model.GistResponse; +import com.mangosolutions.rcloud.rawgist.repository.GistError; +import com.mangosolutions.rcloud.rawgist.repository.GistErrorCode; +import com.mangosolutions.rcloud.rawgist.repository.GistRepository; +import com.mangosolutions.rcloud.rawgist.repository.GistRepositoryException; + +public class ForkGistOperation extends ReadGistOperation { + + private static final Logger logger = LoggerFactory.getLogger(ForkGistOperation.class); + + private GistRepository originalRepository; + + private GistRepository newRepository; + + public ForkGistOperation(RepositoryLayout layout, GistRepository originalRepository, GistRepository newRepository, String gistId, UserDetails user) { + super(layout, gistId, user); + this.originalRepository = originalRepository; + this.newRepository = newRepository; + } + + public ForkGistOperation(File repositoryFolder, GistRepository originalRepository, GistRepository newRepository, String gistId, UserDetails user) { + this(new RepositoryLayout(repositoryFolder), originalRepository, newRepository, gistId, user); + } + + @Override + public GistResponse call() { + this.forkGist(); + OpenOp openOp = new OpenOp(); + openOp.setDir(this.getLayout().getBareFolder()); + try (Grgit git = openOp.call()) { + return this.readGist(git); + } + } + + private void forkGist() { + RepositoryLayout layout = this.getLayout(); + try { + + Grgit git = cloneRepository(); + removeRemotes(git); + this.updateMetadata(); + originalRepository.registerFork(newRepository); + } catch (IOException | GitAPIException e) { + GistError error = new GistError(GistErrorCode.ERR_GIST_FORK_FAILURE, + "Could not fork gist {} to a new gist with id {}", originalRepository.getId(), this.getGistId()); + logger.error(error.getFormattedMessage() + " with folder path {}", layout.getBareFolder()); + throw new GistRepositoryException(error, e); + } + } + + private void removeRemotes(Grgit git) throws GitAPIException { + Git jgit = git.getRepository().getJgit(); + Collection remotes = jgit.getRepository().getRemoteNames(); + for(String remote: remotes) { + RemoteRemoveCommand remoteRemoveCommand = jgit.remoteRemove(); + remoteRemoveCommand.setName(remote); + remoteRemoveCommand.call(); + } + } + + private Grgit cloneRepository() throws IOException { + File bareFolder = this.getLayout().getBareFolder(); + FileUtils.cleanDirectory(bareFolder); + CloneOp cloneOp = new CloneOp(); + cloneOp.setCheckout(false); + cloneOp.setBare(true); + cloneOp.setDir(bareFolder); + cloneOp.setUri(originalRepository.getGistGitRepositoryFolder(this.getUser()).getAbsolutePath()); + + Grgit git = cloneOp.call(); + return git; + } + + private void updateMetadata() { + GistMetadata originalMetadata = originalRepository.getMetadata(); + GistMetadata metadata = getMetadata(); + metadata.setId(this.getGistId()); + metadata.setOwner(this.getUser().getUsername()); + metadata.setCreatedAt(new DateTime()); + metadata.setUpdatedAt(new DateTime()); + metadata.setPublic(false); + metadata.setDescription(originalMetadata.getDescription()); + this.saveMetadata(metadata); + } + + private void saveMetadata(GistMetadata metadata) { + this.getMetadataStore().save(this.getLayout().getMetadataFile(), metadata); + } + + public GistRepository getOriginalRepository() { + return originalRepository; + } + + public void setOriginalRepository(GistRepository originalRepository) { + this.originalRepository = originalRepository; + } + + public GistRepository getNewRepository() { + return newRepository; + } + + public void setNewRepository(GistRepository newRepository) { + this.newRepository = newRepository; + } + +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/GistCommentStore.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/GistCommentStore.java new file mode 100644 index 0000000..92bee49 --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/GistCommentStore.java @@ -0,0 +1,87 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.repository.git; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.mangosolutions.rcloud.rawgist.model.GistCommentResponse; +import com.mangosolutions.rcloud.rawgist.repository.GistError; +import com.mangosolutions.rcloud.rawgist.repository.GistErrorCode; +import com.mangosolutions.rcloud.rawgist.repository.GistRepositoryError; + +@Component +public class GistCommentStore implements CommentStore { + + private static final Logger logger = LoggerFactory.getLogger(GistCommentStore.class); + + @Autowired + private ObjectMapper objectMapper; + + public GistCommentStore() { + this.objectMapper = new ObjectMapper(); + } + + @Autowired + public GistCommentStore(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + public ObjectMapper getObjectMapper() { + return objectMapper; + } + + public void setObjectMapper(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + @Cacheable(value = "commentstore", key = "#store.getAbsolutePath()") + public List load(File store) { + List comments = new ArrayList<>(); + if (store.exists()) { + try { + comments = objectMapper.readValue(store, new TypeReference>() { + }); + } catch (IOException e) { + GistError error = new GistError(GistErrorCode.ERR_COMMENTS_NOT_READABLE, "Could not read comments"); + logger.error(error.getFormattedMessage() + " with path {}", store); + throw new GistRepositoryError(error, e); + } + } + return comments == null? new ArrayList(): comments; + } + + @Override + @CachePut(cacheNames = "commentstore", key = "#store.getAbsolutePath()") + public List save(File store, List comments) { + if(comments != null) { + try { + objectMapper.writeValue(store, comments); + } catch (IOException e) { + GistError error = new GistError(GistErrorCode.ERR_COMMENTS_NOT_WRITEABLE, "Could not save comments"); + logger.error(error.getFormattedMessage() + " with path {}", store); + throw new GistRepositoryError(error, e); + } + } + return comments; + } + + + +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GistMetadata.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/GistMetadata.java similarity index 87% rename from rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GistMetadata.java rename to rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/GistMetadata.java index 11d98f5..7f964d2 100644 --- a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GistMetadata.java +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/GistMetadata.java @@ -4,7 +4,7 @@ * SPDX-License-Identifier: MIT * *******************************************************************************/ -package com.mangosolutions.rcloud.rawgist.repository; +package com.mangosolutions.rcloud.rawgist.repository.git; import java.io.Serializable; import java.util.ArrayList; @@ -23,7 +23,7 @@ import com.mangosolutions.rcloud.rawgist.model.Fork; @JsonInclude(JsonInclude.Include.NON_NULL) -@JsonPropertyOrder({ "id", "owner", "description", "created_at", "updated_at", "forks"}) +@JsonPropertyOrder({ "id", "owner", "description", "public", "created_at", "updated_at", "forks"}) public class GistMetadata implements Serializable { private final static long serialVersionUID = -7352290872081419828L; @@ -36,6 +36,9 @@ public class GistMetadata implements Serializable { @JsonProperty("description") private String description; + + @JsonProperty("public") + private boolean _public = true; @JsonProperty("created_at") private DateTime createdAt; @@ -75,6 +78,16 @@ public String getDescription() { public void setDescription(String description) { this.description = description; } + + @JsonProperty("public") + public boolean isPublic() { + return _public; + } + + @JsonProperty("public") + public void setPublic(boolean _public) { + this._public = _public; + } @JsonProperty("created_at") public DateTime getCreatedAt() { diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/GistMetadataStore.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/GistMetadataStore.java new file mode 100644 index 0000000..314f7f6 --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/GistMetadataStore.java @@ -0,0 +1,81 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.repository.git; + +import java.io.File; +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.mangosolutions.rcloud.rawgist.repository.GistError; +import com.mangosolutions.rcloud.rawgist.repository.GistErrorCode; +import com.mangosolutions.rcloud.rawgist.repository.GistRepositoryError; + +@Component +public class GistMetadataStore implements MetadataStore { + + private static final Logger logger = LoggerFactory.getLogger(GistMetadataStore.class); + + @Autowired + private ObjectMapper objectMapper; + + public GistMetadataStore() { + this.objectMapper = new ObjectMapper(); + } + + @Autowired + public GistMetadataStore(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + public ObjectMapper getObjectMapper() { + return objectMapper; + } + + public void setObjectMapper(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + + @Override + @Cacheable(value = "metadatastore", key = "#store.getAbsolutePath()") + public GistMetadata load(File store) { + GistMetadata metadata = null; + if(store.exists()) { + try { + metadata = objectMapper.readValue(store, GistMetadata.class); + } catch (IOException e) { + GistError error = new GistError(GistErrorCode.ERR_METADATA_NOT_READABLE, "Could not read metadata for this gist"); + logger.error(error.getFormattedMessage() + " with path {}", store); + throw new GistRepositoryError(error, e); + } + } else { + metadata = new GistMetadata(); + } + return metadata; + } + + @Override + @CachePut(cacheNames = "metadatastore", key = "#store.getAbsolutePath()") + public GistMetadata save(File store, GistMetadata metadata) { + try { + objectMapper.writeValue(store, metadata); + } catch (IOException e) { + GistError error = new GistError(GistErrorCode.ERR_METADATA_NOT_WRITEABLE, "Could not update metadata for gist {}", metadata.getId()); + logger.error(error.getFormattedMessage() + " with path {}", store); + throw new GistRepositoryError(error, e); + } + return metadata; + } + +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/GistOperationFactory.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/GistOperationFactory.java new file mode 100644 index 0000000..9645ee7 --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/GistOperationFactory.java @@ -0,0 +1,145 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.repository.git; + +import java.io.File; +import java.util.LinkedList; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.mangosolutions.rcloud.rawgist.model.FileContent; +import com.mangosolutions.rcloud.rawgist.model.GistHistory; +import com.mangosolutions.rcloud.rawgist.model.GistRequest; +import com.mangosolutions.rcloud.rawgist.repository.GistCommentRepository; +import com.mangosolutions.rcloud.rawgist.repository.GistRepository; + +@Component +public class GistOperationFactory { + + + @Autowired + private HistoryCache historyCache = new HistoryCache() { + + @Override + public List load(String commitId) { + return new LinkedList<>(); + } + + @Override + public List save(String commitId, List history) { + return history; + } + + }; + + + @Autowired + private FileContentCache fileContentCache = new FileContentCache() { + + @Override + public FileContent load(String contentId, String path) { + return null; + } + + @Override + public FileContent save(String contentId, String path, FileContent content) { + return content; + } + + }; + + @Autowired + private MetadataStore metadataStore; + + @Autowired + private CommentStore commentStore; + + public GistOperationFactory() { + this(new ObjectMapper()); + } + + public GistOperationFactory(ObjectMapper objectMapper) { + this.metadataStore = new GistMetadataStore(objectMapper); + this.commentStore = new GistCommentStore(objectMapper); + } + + @Autowired + public GistOperationFactory(MetadataStore metadataStore, CommentStore commentStore, HistoryCache historyCache, FileContentCache fileContentCache) { + this.metadataStore = metadataStore; + this.commentStore = commentStore; + this.historyCache = historyCache; + this.fileContentCache = fileContentCache; + } + + public HistoryCache getHistoryCache() { + return historyCache; + } + + public void setHistoryCache(HistoryCache historyCache) { + this.historyCache = historyCache; + } + + public MetadataStore getMetadataStore() { + return metadataStore; + } + + public void setMetadataStore(MetadataStore metadataStore) { + this.metadataStore = metadataStore; + } + + public CommentStore getCommentStore() { + return commentStore; + } + + public void setCommentStore(CommentStore commentStore) { + this.commentStore = commentStore; + } + + public ReadGistOperation getReadOperation(RepositoryLayout layout, String gistId, UserDetails user, String commitId) { + GistCommentRepository repository = new GitGistCommentRepository(layout.getCommentsFile(), commentStore); + ReadGistOperation op = new ReadGistOperation(layout, gistId, user); + if(!StringUtils.isEmpty(commitId)) { + op.setCommitId(commitId); + } + op.setCommentRepository(repository); + op.setCommitId(commitId); + op.setHistorycache(historyCache); + op.setMetadataStore(this.metadataStore); + op.setFileContentCache(fileContentCache); + return op; + } + + public CreateOrUpdateGistOperation getCreateOrUpdateOperation(RepositoryLayout layout, String gistId, GistRequest gistRequest, UserDetails user) { + GistCommentRepository repository = new GitGistCommentRepository(layout.getCommentsFile(), commentStore); + CreateOrUpdateGistOperation op = new CreateOrUpdateGistOperation(layout, gistId, gistRequest, user); + op.setCommentRepository(repository); + op.setHistorycache(historyCache); + op.setMetadataStore(this.metadataStore); + op.setFileContentCache(fileContentCache); + return op; + } + + public ForkGistOperation getForkOperation(RepositoryLayout layout, String gistId, GistRepository originalRepository, GistRepository newRepository, UserDetails user) { + GistCommentRepository repository = new GitGistCommentRepository(layout.getCommentsFile(), commentStore); + ForkGistOperation op = new ForkGistOperation(layout, originalRepository, newRepository, gistId, user); + op.setCommentRepository(repository); + op.setHistorycache(historyCache); + op.setMetadataStore(this.metadataStore); + op.setFileContentCache(fileContentCache); + return op; + } + + public InitRepositoryLayoutOperation getInitRepositoryLayoutOperation(File repositoryRoot) { + return new InitRepositoryLayoutOperation(repositoryRoot); + } + +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GitGistCommentRepository.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/GitGistCommentRepository.java similarity index 66% rename from rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GitGistCommentRepository.java rename to rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/GitGistCommentRepository.java index 94fc300..76003cd 100644 --- a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GitGistCommentRepository.java +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/GitGistCommentRepository.java @@ -4,56 +4,40 @@ * SPDX-License-Identifier: MIT * *******************************************************************************/ -package com.mangosolutions.rcloud.rawgist.repository; +package com.mangosolutions.rcloud.rawgist.repository.git; import java.io.File; -import java.io.IOException; -import java.util.ArrayList; +import java.io.Serializable; import java.util.List; import org.apache.commons.collections4.IterableUtils; import org.apache.commons.collections4.Predicate; -import org.apache.commons.io.FileUtils; import org.joda.time.DateTime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.core.userdetails.UserDetails; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; import com.mangosolutions.rcloud.rawgist.model.GistComment; import com.mangosolutions.rcloud.rawgist.model.GistCommentResponse; import com.mangosolutions.rcloud.rawgist.model.GistIdentity; -import com.mangosolutions.rcloud.rawgist.repository.GistError.GistErrorCode; +import com.mangosolutions.rcloud.rawgist.repository.GistCommentRepository; +import com.mangosolutions.rcloud.rawgist.repository.GistError; +import com.mangosolutions.rcloud.rawgist.repository.GistErrorCode; +import com.mangosolutions.rcloud.rawgist.repository.GistRepositoryException; -public class GitGistCommentRepository implements GistCommentRepository { +public class GitGistCommentRepository implements GistCommentRepository, Serializable { - public static final String COMMENT_REPOSITORY_FOLDER = "comments"; + private static final long serialVersionUID = 414766810805325462L; - public static final String COMMENTS_FILE = "comments.json"; - - private Logger logger = LoggerFactory.getLogger(GitGistCommentRepository.class); - - private File commentRepositoryFolder; + private static final Logger logger = LoggerFactory.getLogger(GitGistCommentRepository.class); private File commentsFile; - private String gistId; - - private ObjectMapper objectMapper; + private CommentStore commentStore; - public GitGistCommentRepository(File gistRepository, String gistId, ObjectMapper objectMapper) { - this.gistId = gistId; - this.commentRepositoryFolder = new File(gistRepository, COMMENT_REPOSITORY_FOLDER); - if (!commentRepositoryFolder.exists()) { - try { - FileUtils.forceMkdir(commentRepositoryFolder); - } catch (IOException e) { - throw new GistRepositoryError(new GistError(GistErrorCode.FATAL_GIST_INITIALISATION, "Could not create comment repository for gist {}", this.gistId), e); - } - } - commentsFile = new File(commentRepositoryFolder, COMMENTS_FILE); - this.objectMapper = objectMapper; + public GitGistCommentRepository(File commentsFile, CommentStore commentStore) { + this.commentsFile = commentsFile; + this.commentStore = commentStore; } /* (non-Javadoc) @@ -135,32 +119,18 @@ public boolean evaluate(GistCommentResponse comment) { }); } - GistError error = new GistError(GistErrorCode.ERR_COMMENT_NOT_EXIST, "Comment with id {} on gist {} does not exist", - this.gistId, id); + GistError error = new GistError(GistErrorCode.ERR_COMMENT_NOT_EXIST, "Comment with id {} does not exist", id); logger.warn(error.getFormattedMessage()); throw new GistRepositoryException(error); } private List loadComments() { - List comments = new ArrayList<>(); - if (commentsFile.exists()) { - try { - comments = objectMapper.readValue(commentsFile, new TypeReference>() { - }); - } catch (IOException e) { - throw new GistRepositoryError(new GistError(GistErrorCode.ERR_COMMENTS_NOT_READABLE, "Could not read metadata for gist {}", this.gistId), e); - } - } - return comments == null? new ArrayList(): comments; + return this.commentStore.load(this.commentsFile); } private void saveComments(List comments) { - try { - objectMapper.writeValue(commentsFile, comments); - } catch (IOException e) { - throw new GistRepositoryError(new GistError(GistErrorCode.ERR_COMMENTS_NOT_WRITEABLE, "Could not save metadata for gist {}", this.gistId), e); - } + this.commentStore.save(this.commentsFile, comments); } } diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/GitGistRepository.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/GitGistRepository.java new file mode 100644 index 0000000..11a87e6 --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/GitGistRepository.java @@ -0,0 +1,144 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.repository.git; + +import java.io.File; +import java.io.Serializable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.userdetails.UserDetails; + +import com.mangosolutions.rcloud.rawgist.model.Fork; +import com.mangosolutions.rcloud.rawgist.model.GistIdentity; +import com.mangosolutions.rcloud.rawgist.model.GistRequest; +import com.mangosolutions.rcloud.rawgist.model.GistResponse; +import com.mangosolutions.rcloud.rawgist.repository.GistCommentRepository; +import com.mangosolutions.rcloud.rawgist.repository.GistRepository; + +public class GitGistRepository implements GistRepository, Serializable { + + private static final long serialVersionUID = -8235501365399798269L; + + private static final Logger logger = LoggerFactory.getLogger(GitGistRepository.class); + + static final String B64_BINARY_EXTENSION = "b64"; + + private GistOperationFactory gistOperationFactory; + + private RepositoryLayout layout; + + private MetadataStore metadataStore; + + private CommentStore commentStore; + + public GitGistRepository(File repositoryFolder) { + this(repositoryFolder, new GistOperationFactory()); + } + + + public GitGistRepository(File repositoryFolder, GistOperationFactory gistOperationFactory) { + this.gistOperationFactory = gistOperationFactory; + this.metadataStore = gistOperationFactory.getMetadataStore(); + this.commentStore = gistOperationFactory.getCommentStore(); + InitRepositoryLayoutOperation op = new InitRepositoryLayoutOperation(repositoryFolder); + this.layout = op.call(); + } + + + @Override + public String getId() { + return this.getMetadata().getId(); + } + + @Override + public File getGistRepositoryFolder(UserDetails owner) { + return layout.getRootFolder(); + } + + public File getGistGitRepositoryFolder(UserDetails owner) { + return layout.getBareFolder(); + } + + @Override + public GistResponse readGist(UserDetails userDetails) { + return readGistInternal(userDetails); + } + + @Override + public GistResponse readGist(String commitId, UserDetails userDetails) { + return readGistInternal(commitId, userDetails); + } + + @Override + public GistResponse createGist(GistRequest request, String gistId, UserDetails userDetails) { + CreateOrUpdateGistOperation op = gistOperationFactory.getCreateOrUpdateOperation(layout, gistId, request, userDetails); + return op.call(); + } + + @Override + public GistResponse forkGist(GistRepository originalRepository, String gistId, UserDetails userDetails) { + + ForkGistOperation op = gistOperationFactory.getForkOperation(layout, gistId, originalRepository, this, userDetails); + return op.call(); + + } + + @Override + public GistResponse updateGist(GistRequest request, UserDetails userDetails) { + CreateOrUpdateGistOperation op = gistOperationFactory.getCreateOrUpdateOperation(layout, this.getId(), request, userDetails); + return op.call(); + } + + + @Override + public GistMetadata getMetadata() { + return metadataStore.load(layout.getMetadataFile()); + } + + @Override + public void registerFork(GistRepository forkedRepository) { + this.updateForkInformation(forkedRepository); + } + + @Override + public GistCommentRepository getCommentRepository() { + return new GitGistCommentRepository(this.layout.getCommentsFile(), this.commentStore); + } + + private void saveMetadata(GistMetadata metadata) { + metadataStore.save(this.layout.getMetadataFile(), metadata); + } + + private GistResponse readGistInternal(String commitId, UserDetails activeUser) { + ReadGistOperation op = gistOperationFactory.getReadOperation(layout, this.getId(), activeUser, commitId); + return op.call(); + } + + private GistResponse readGistInternal(UserDetails activeUser) { + return this.readGistInternal(null, activeUser); + } + + private void updateForkInformation(GistRepository forkedRepository) { + GistMetadata metadata = this.getMetadata(); + GistMetadata forksMetadata = forkedRepository.getMetadata(); + Fork fork = new Fork(); + fork.setCreatedAt(forksMetadata.getCreatedAt()); + fork.setUpdatedAt(forksMetadata.getUpdatedAt()); + fork.setId(forksMetadata.getId()); + String forkOwner = forksMetadata.getOwner(); + GistIdentity forkOwnerIdentity = new GistIdentity(); + forkOwnerIdentity.setLogin(forkOwner); + fork.setUser(forkOwnerIdentity); + metadata.addFork(fork); + this.saveMetadata(metadata); + } + + + + +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/GitGistRepositoryFactory.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/GitGistRepositoryFactory.java new file mode 100644 index 0000000..51f2cda --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/GitGistRepositoryFactory.java @@ -0,0 +1,27 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.repository.git; + +import java.io.File; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.mangosolutions.rcloud.rawgist.repository.GistRepository; +import com.mangosolutions.rcloud.rawgist.repository.GistRepositoryFactory; + +@Component +public class GitGistRepositoryFactory implements GistRepositoryFactory { + + @Autowired + private GistOperationFactory gistOperationFactory; + + public GistRepository getRepository(File folder) { + return new GitGistRepository(folder, gistOperationFactory); + } + +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/GitGistRepositoryService.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/GitGistRepositoryService.java new file mode 100644 index 0000000..7ee8cd1 --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/GitGistRepositoryService.java @@ -0,0 +1,338 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.repository.git; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.filefilter.FileFileFilter; +import org.apache.commons.io.filefilter.FileFilterUtils; +import org.apache.commons.io.filefilter.NameFileFilter; +import org.apache.commons.io.filefilter.TrueFileFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; + +import com.hazelcast.core.HazelcastInstance; +import com.mangosolutions.rcloud.rawgist.model.Fork; +import com.mangosolutions.rcloud.rawgist.model.GistComment; +import com.mangosolutions.rcloud.rawgist.model.GistCommentResponse; +import com.mangosolutions.rcloud.rawgist.model.GistRequest; +import com.mangosolutions.rcloud.rawgist.model.GistResponse; +import com.mangosolutions.rcloud.rawgist.repository.GistAccessDeniedException; +import com.mangosolutions.rcloud.rawgist.repository.GistCommentRepository; +import com.mangosolutions.rcloud.rawgist.repository.GistError; +import com.mangosolutions.rcloud.rawgist.repository.GistErrorCode; +import com.mangosolutions.rcloud.rawgist.repository.GistIdGenerator; +import com.mangosolutions.rcloud.rawgist.repository.GistRepository; +import com.mangosolutions.rcloud.rawgist.repository.GistRepositoryError; +import com.mangosolutions.rcloud.rawgist.repository.GistRepositoryException; +import com.mangosolutions.rcloud.rawgist.repository.GistRepositoryFactory; +import com.mangosolutions.rcloud.rawgist.repository.GistRepositoryService; +import com.mangosolutions.rcloud.rawgist.repository.GistSecurityManager; + +public class GitGistRepositoryService implements GistRepositoryService { + + private static final int DEFAULT_LOCK_TIMEOUT = 30; + + private int lockTimeout = DEFAULT_LOCK_TIMEOUT; + + + + private static final String RECYCLE_FOLDER_NAME = ".recycle"; + + private Logger logger = LoggerFactory.getLogger(GitGistRepositoryService.class); + + private File repositoryRoot; + private File recycleRoot; + private GistIdGenerator idGenerator; + private HazelcastInstance hazelcastInstance; + private GistSecurityManager securityManager; + private GistRepositoryFactory repositoryFactory; + private List locators; + + + public GitGistRepositoryService(String repositoryRoot, GistIdGenerator idGenerator, + HazelcastInstance hazelcastInstance) throws IOException { + this.repositoryRoot = new File(repositoryRoot); + if (!this.repositoryRoot.exists()) { + FileUtils.forceMkdir(this.repositoryRoot); + } + recycleRoot = new File(repositoryRoot, RECYCLE_FOLDER_NAME); + if (!this.recycleRoot.exists()) { + FileUtils.forceMkdir(this.recycleRoot); + } + this.idGenerator = idGenerator; + this.hazelcastInstance = hazelcastInstance; + + locators = Arrays.asList(new AsymetricFourFolderRepositoryStorageLocator(this.repositoryRoot), new SymetricFourPartRepositoryStorageLocator(this.repositoryRoot)); + } + + public void setGistRepositoryFactory(GistRepositoryFactory repositoryFactory) { + this.repositoryFactory = repositoryFactory; + } + + public void setLockTimeout(int timeout) { + this.lockTimeout = timeout; + } + + public GistSecurityManager getSecurityManager() { + return securityManager; + } + + public void setSecurityManager(GistSecurityManager securityManager) { + this.securityManager = securityManager; + } + + @Override + public List listGists(UserDetails user) { + List gists = new ArrayList(); + for (File file : FileUtils.listFiles(repositoryRoot, + FileFilterUtils.and(FileFileFilter.FILE, new NameFileFilter(RepositoryLayout.GIST_META_FILE)), + TrueFileFilter.INSTANCE)) { + GistRepository repository = repositoryFactory.getRepository(file.getParentFile()); + if(this.securityManager.isOwner(repository, user)) { + gists.add(repository.readGist(user)); + } + } + return gists; + } + + @Override + public GistResponse getGist(String gistId, UserDetails user) { + Lock lock = acquireGistLock(gistId); + try { + File repositoryFolder = getAndValidateRepositoryFolder(gistId); + GistRepository repository = repositoryFactory.getRepository(repositoryFolder); + this.ensureReadable(repository, user); + return repository.readGist(user); + } finally { + lock.unlock(); + } + } + + @Override + public GistResponse getGist(String gistId, String commitId, UserDetails user) { + Lock lock = acquireGistLock(gistId); + try { + File repositoryFolder = getAndValidateRepositoryFolder(gistId); + GistRepository repository = repositoryFactory.getRepository(repositoryFolder); + this.ensureReadable(repository, user); + return repository.readGist(commitId, user); + } finally { + lock.unlock(); + } + } + + @Override + public GistResponse createGist(GistRequest request, UserDetails user) { + String gistId = idGenerator.generateId(); + File repositoryFolder = getRepositoryFolder(gistId); + GistRepository repository = repositoryFactory.getRepository(repositoryFolder); + return repository.createGist(request, gistId, user); + } + + @Override + public GistResponse forkGist(String gistToForkId, User user) { + Lock lock = acquireGistLock(gistToForkId); + try { + File gistToForkRepositoryFolder = getAndValidateRepositoryFolder(gistToForkId); + GistRepository gistToForkRepository = repositoryFactory.getRepository(gistToForkRepositoryFolder); + this.ensureReadable(gistToForkRepository, user); + String gistId = idGenerator.generateId(); + File repositoryFolder = getRepositoryFolder(gistId); + GistRepository repository = repositoryFactory.getRepository(repositoryFolder); + return repository.forkGist(gistToForkRepository, gistId, user); + } finally { + lock.unlock(); + } + } + + @Override + public GistResponse editGist(String gistId, GistRequest request, UserDetails user) { + Lock lock = acquireGistLock(gistId); + try { + File repositoryFolder = getAndValidateRepositoryFolder(gistId); + GistRepository repository = repositoryFactory.getRepository(repositoryFolder); + this.ensureWritable(repository, user); + return repository.updateGist(request, user); + } finally { + lock.unlock(); + } + } + + @Override + public void deleteGist(String gistId, UserDetails user) { + Lock lock = acquireGistLock(gistId); + try { + File repositoryFolder = getAndValidateRepositoryFolder(gistId); + GistRepository repository = repositoryFactory.getRepository(repositoryFolder); + this.ensureWritable(repository, user); + FileUtils.moveDirectoryToDirectory(repositoryFolder, new File(recycleRoot, gistId), true); + FileUtils.forceDelete(repositoryFolder); + } catch (IOException e) { + GistError error = new GistError(GistErrorCode.ERR_GIST_UPDATE_FAILURE, + "Could not delete gist {}, an internal error has occurred", gistId); + logger.error(error.getFormattedMessage()); + throw new GistRepositoryError(error, e); + } finally { + lock.unlock(); + } + } + + @Override + public List getForks(String gistId, User activeUser) { + Lock lock = acquireGistLock(gistId); + try { + File repositoryFolder = getAndValidateRepositoryFolder(gistId); + GistRepository repository = repositoryFactory.getRepository(repositoryFolder); + this.ensureReadable(repository, activeUser); + GistMetadata metadata = repository.getMetadata(); + List forks = metadata.getForks(); + if(forks == null) { + forks = Collections.emptyList(); + } + return forks; + } finally { + lock.unlock(); + } + } + + @Override + public List getComments(String gistId, UserDetails user) { + Lock lock = acquireGistLock(gistId); + try { + File repositoryFolder = getAndValidateRepositoryFolder(gistId); + GistRepository gistRepository = repositoryFactory.getRepository(repositoryFolder); + this.ensureReadable(gistRepository, user); + GistCommentRepository repository = gistRepository.getCommentRepository(); + return repository.getComments(user); + } finally { + lock.unlock(); + } + } + + @Override + public GistCommentResponse getComment(String gistId, long commentId, UserDetails user) { + Lock lock = acquireGistLock(gistId); + try { + File repositoryFolder = getAndValidateRepositoryFolder(gistId); + GistRepository gistRepository = repositoryFactory.getRepository(repositoryFolder); + this.ensureReadable(gistRepository, user); + GistCommentRepository repository = gistRepository.getCommentRepository(); + return repository.getComment(commentId, user); + } finally { + lock.unlock(); + } + } + + @Override + public GistCommentResponse createComment(String gistId, GistComment comment, UserDetails user) { + Lock lock = acquireGistLock(gistId); + try { + File repositoryFolder = getAndValidateRepositoryFolder(gistId); + GistRepository gistRepository = repositoryFactory.getRepository(repositoryFolder); + this.ensureWritable(gistRepository, user); + GistCommentRepository repository = gistRepository.getCommentRepository(); + return repository.createComment(comment, user); + } finally { + lock.unlock(); + } + } + + @Override + public GistCommentResponse editComment(String gistId, long commentId, GistComment comment, UserDetails user) { + Lock lock = acquireGistLock(gistId); + try { + File repositoryFolder = getAndValidateRepositoryFolder(gistId); + GistRepository gistRepository = repositoryFactory.getRepository(repositoryFolder); + this.ensureWritable(gistRepository, user); + GistCommentRepository repository = gistRepository.getCommentRepository(); + return repository.editComment(commentId, comment, user); + } finally { + lock.unlock(); + } + } + + @Override + public void deleteComment(String gistId, long commentId, UserDetails user) { + Lock lock = acquireGistLock(gistId); + try { + File repositoryFolder = getAndValidateRepositoryFolder(gistId); + GistRepository gistRepository = repositoryFactory.getRepository(repositoryFolder); + this.ensureWritable(gistRepository, user); + GistCommentRepository repository = gistRepository.getCommentRepository(); + repository.deleteComment(commentId, user); + } finally { + lock.unlock(); + } + } + + private Lock acquireGistLock(String gistId) { + Lock lock = hazelcastInstance.getLock(gistId); + try { + if (!lock.tryLock(lockTimeout, TimeUnit.SECONDS)) { + GistError error = new GistError(GistErrorCode.ERR_GIST_CONTENT_NOT_AVAILABLE, + "Could not access gist {}, it is currently being updated", gistId); + logger.error(error.getFormattedMessage()); + throw new GistRepositoryException(error); + } + } catch (InterruptedException e) { + GistError error = new GistError(GistErrorCode.ERR_GIST_CONTENT_NOT_AVAILABLE, + "Could not acess gist {}, it is currently being updated", gistId); + logger.error(error.getFormattedMessage()); + throw new GistRepositoryException(error); + } + return lock; + } + + private File getAndValidateRepositoryFolder(String id) { + for(RepositoryStorageLocator locator: this.locators) { + File repositoryFolder = locator.getStoragePath(id); + if(repositoryFolder.exists()) { + return repositoryFolder; + } + } + GistError error = new GistError(GistErrorCode.ERR_GIST_NOT_EXIST, "Gist with id {} does not exist", id); + logger.error(error.getFormattedMessage()); + throw new GistRepositoryException(error); + + } + + private File getRepositoryFolder(String id) { + return this.locators.get(0).getStoragePath(id); + } + + + private void ensureReadable(GistRepository repository, UserDetails user) { + if (!this.securityManager.canRead(repository, user)) { + GistError error = new GistError(GistErrorCode.ERR_ACL_READ_DENIED, + "You do not have permission to read the gist with id {}.", repository.getId()); + logger.error(error.getFormattedMessage()); + throw new GistAccessDeniedException(error); + } + } + + private void ensureWritable(GistRepository repository, UserDetails user) { + if (!this.securityManager.canWrite(repository, user)) { + GistError error = new GistError(GistErrorCode.ERR_ACL_WRITE_DENIED, + "You do not have permission to alter the gist with id {}.", repository.getId()); + logger.error(error.getFormattedMessage()); + throw new GistAccessDeniedException(error); + } + } + + +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GitHistoryCreator.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/GitHistoryOperation.java similarity index 60% rename from rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GitHistoryCreator.java rename to rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/GitHistoryOperation.java index 0b7b876..d0653da 100644 --- a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/GitHistoryCreator.java +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/GitHistoryOperation.java @@ -4,14 +4,17 @@ * SPDX-License-Identifier: MIT * *******************************************************************************/ -package com.mangosolutions.rcloud.rawgist.repository; +package com.mangosolutions.rcloud.rawgist.repository.git; import java.io.IOException; import java.util.ArrayList; +import java.util.LinkedList; import java.util.List; +import java.util.concurrent.Callable; import org.ajoberstar.grgit.Commit; import org.ajoberstar.grgit.CommitDiff; +import org.ajoberstar.grgit.Grgit; import org.ajoberstar.grgit.Person; import org.ajoberstar.grgit.Repository; import org.ajoberstar.grgit.operation.LogOp; @@ -49,30 +52,77 @@ * * */ -public class GitHistoryCreator { +public class GitHistoryOperation implements Callable> { - private static final Logger LOG = Logger.getLogger(GitHistoryCreator.class); + private static final Logger logger = Logger.getLogger(GitHistoryOperation.class); + private Repository repository; - public List call(Repository repository) { + private String commitId; + + private HistoryCache historyStore = new HistoryCache() { + + @Override + public List load(String commitId) { + return new LinkedList<>(); + } + + @Override + public List save(String commitId, List history) { + return history; + } + + }; + + public GitHistoryOperation(Grgit git, String commitId) { + this.repository = git.getRepository(); + this.commitId = commitId; + } + + public Repository getRepository() { + return repository; + } + + public void setRepository(Repository repository) { + this.repository = repository; + } + + public List call() { LogOp logOp = new LogOp(repository); List commits = logOp.call(); - return map(repository, commits); + return calculateHistory(repository, commits); } - private List map(Repository repository, List commits) { + private List calculateHistory(Repository repository, List commits) { List histories = new ArrayList<>(); + boolean recordHistory = commitId == null; for (Commit logCommit : commits) { + if(this.commitId == null) { + this.commitId = logCommit.getId(); + } try { - GistHistory history = create(repository, logCommit); - histories.add(history); + if(commitId != null && logCommit.getId().equals(commitId)) { + recordHistory = true; + } + if(recordHistory) { + List cachedHistory = historyStore.load(logCommit.getId()); + if(!cachedHistory.isEmpty()) { + histories.addAll(cachedHistory); + break; + } + GistHistory history = create(repository, logCommit); + histories.add(history); + + } } catch (GitAPIException | IOException e) { - LOG.error(String.format("Could not extract diff of commit %s.", logCommit.getId()), e); + logger.error(String.format("Could not extract diff of commit %s.", logCommit.getId()), e); } } + this.historyStore.save(this.commitId, histories); return histories; } + private GistHistory create(Repository repository, Commit logCommit) throws GitAPIException, IOException { ShowOp showOp = new ShowOp(repository); showOp.setCommit(logCommit); @@ -110,4 +160,13 @@ private void setVersion(GistHistory history, Commit logCommit) { history.setVersion(logCommit.getId()); } + public void setCommitId(String commitId) { + this.commitId = commitId; + } + + public void setHistoryCache(HistoryCache historyStore) { + this.historyStore = historyStore; + } + + } diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/HistoryCache.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/HistoryCache.java new file mode 100644 index 0000000..ddad1cf --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/HistoryCache.java @@ -0,0 +1,19 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.repository.git; + +import java.util.List; + +import com.mangosolutions.rcloud.rawgist.model.GistHistory; + +public interface HistoryCache { + + public List load(String commitId); + + public List save(String commitId, List history); + +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/InitRepositoryLayoutOperation.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/InitRepositoryLayoutOperation.java new file mode 100644 index 0000000..90fb16d --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/InitRepositoryLayoutOperation.java @@ -0,0 +1,97 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.repository.git; + +import java.io.File; +import java.io.IOException; +import java.util.concurrent.Callable; + +import org.ajoberstar.grgit.exception.GrgitException; +import org.ajoberstar.grgit.operation.InitOp; +import org.apache.commons.io.FileUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.mangosolutions.rcloud.rawgist.repository.GistError; +import com.mangosolutions.rcloud.rawgist.repository.GistErrorCode; +import com.mangosolutions.rcloud.rawgist.repository.GistRepositoryError; + +public class InitRepositoryLayoutOperation implements Callable { + + private static final Logger logger = LoggerFactory.getLogger(InitRepositoryLayoutOperation.class); + + private File repositoryRoot; + + public InitRepositoryLayoutOperation(File repositoryRoot) { + this.repositoryRoot = repositoryRoot; + } + + public File getRepositoryRoot() { + return repositoryRoot; + } + + public void setRepositoryRoot(File repositoryRoot) { + this.repositoryRoot = repositoryRoot; + } + + @Override + public RepositoryLayout call() { + RepositoryLayout layout = new RepositoryLayout(repositoryRoot); + //ensure the comments folder exists + createRootFolder(layout); + createCommentsFolder(layout); + createGistFolder(layout); + createWorkingFolder(layout); + initGistRepo(layout); + return layout; + } + + private void initGistRepo(RepositoryLayout layout) { + File bareFolder = layout.getBareFolder(); + if(bareFolder.list().length == 0) { + try { + InitOp initOp = new InitOp(); + initOp.setDir(bareFolder); + initOp.setBare(true); + initOp.call(); + } catch (GrgitException e) { + GistError error = new GistError(GistErrorCode.FATAL_GIST_INITIALISATION, "Could not create gist storage location for gist"); + logger.error(error.getFormattedMessage() + " with folder path {}", bareFolder); + throw new GistRepositoryError(error, e); + } + } + } + + private void createCommentsFolder(RepositoryLayout layout) { + mkdir(layout.getCommentsFolder()); + } + + private void createGistFolder(RepositoryLayout layout) { + mkdir(layout.getBareFolder()); + } + + private void createWorkingFolder(RepositoryLayout layout) { + mkdir(layout.getWorkingFolder()); + } + + private void createRootFolder(RepositoryLayout layout) { + mkdir(layout.getRootFolder()); + } + + private void mkdir(File dir) { + if (!dir.exists()) { + try { + FileUtils.forceMkdir(dir); + } catch (IOException e) { + GistError error = new GistError(GistErrorCode.FATAL_GIST_INITIALISATION, "Could not create gist storage location for gist"); + logger.error(error.getFormattedMessage() + " with folder path {}", dir); + throw new GistRepositoryError(error, e); + } + } + } + +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/MetadataStore.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/MetadataStore.java new file mode 100644 index 0000000..f1e6f45 --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/MetadataStore.java @@ -0,0 +1,17 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.repository.git; + +import java.io.File; + +public interface MetadataStore { + + GistMetadata load(File store); + + GistMetadata save(File store, GistMetadata metadata); + +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/PermissiveGistSecurityManager.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/PermissiveGistSecurityManager.java new file mode 100644 index 0000000..deb3691 --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/PermissiveGistSecurityManager.java @@ -0,0 +1,30 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.repository.git; + +import org.springframework.security.core.userdetails.UserDetails; + +import com.mangosolutions.rcloud.rawgist.repository.GistRepository; + +public class PermissiveGistSecurityManager extends SimpleGistSecurityManager { + + @Override + public boolean canRead(GistRepository repository, UserDetails userDetails) { + return true; + } + + @Override + public boolean canWrite(GistRepository repository, UserDetails userDetails) { + return true; + } + + @Override + public GistAccessRight getAccessRight(GistRepository repository, UserDetails userDetails) { + return GistAccessRight.WRITE; + } + +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/ReadGistOperation.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/ReadGistOperation.java new file mode 100644 index 0000000..a6acf70 --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/ReadGistOperation.java @@ -0,0 +1,296 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.repository.git; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; + +import javax.activation.MimetypesFileTypeMap; + +import org.ajoberstar.grgit.Grgit; +import org.ajoberstar.grgit.operation.OpenOp; +import org.apache.commons.io.Charsets; +import org.apache.commons.io.FilenameUtils; +import org.eclipse.jgit.errors.CorruptObjectException; +import org.eclipse.jgit.errors.IncorrectObjectTypeException; +import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectLoader; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevTree; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.TreeWalk; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.util.StringUtils; + +import com.mangosolutions.rcloud.rawgist.model.FileContent; +import com.mangosolutions.rcloud.rawgist.model.GistHistory; +import com.mangosolutions.rcloud.rawgist.model.GistIdentity; +import com.mangosolutions.rcloud.rawgist.model.GistResponse; +import com.mangosolutions.rcloud.rawgist.repository.GistCommentRepository; +import com.mangosolutions.rcloud.rawgist.repository.GistError; +import com.mangosolutions.rcloud.rawgist.repository.GistErrorCode; +import com.mangosolutions.rcloud.rawgist.repository.GistRepositoryError; + +public class ReadGistOperation implements Callable { + + private static final Logger logger = LoggerFactory.getLogger(ReadGistOperation.class); + + public static final String REF_HEAD_MASTER = "refs/heads/master"; + + private GistCommentRepository commentRepository; + + private MetadataStore metadataStore; + + private HistoryCache historyCache = new HistoryCache() { + + @Override + public List load(String commitId) { + return new LinkedList<>(); + } + + @Override + public List save(String commitId, List history) { + return history; + } + + }; + + private FileContentCache fileContentCache = new FileContentCache() { + + @Override + public FileContent load(String contentId, String path) { + return null; + } + + @Override + public FileContent save(String contentId, String path, FileContent content) { + return content; + } + + }; + private RepositoryLayout layout; + + private UserDetails user; + + private String gistId; + + private String commitId = null; + + + public ReadGistOperation(RepositoryLayout layout, String gistId, String commitId, UserDetails user) { + this.layout = layout; + this.gistId = gistId; + this.commitId = commitId; + this.user = user; + } + + public ReadGistOperation(File repositoryFolder, String gistId, String commitId, UserDetails user) { + this(new RepositoryLayout(repositoryFolder), gistId, commitId, user); + } + + public ReadGistOperation(RepositoryLayout layout, String gistId, UserDetails user) { + this(layout, gistId, null, user); + + } + + public ReadGistOperation(File repositoryFolder, String gistId, UserDetails user) { + this(new RepositoryLayout(repositoryFolder), gistId, user); + } + + @Override + public GistResponse call() { + OpenOp openOp = new OpenOp(); + openOp.setDir(layout.getBareFolder()); + try (Grgit git = openOp.call()) { + + return this.readGist(git); + } + } + + protected GistResponse readGist(Grgit git) { + try { + Repository repository = git.getRepository().getJgit().getRepository(); + RevCommit revCommit = resolveCommit(repository); + GistResponse response = new GistResponse(); + + Map fileContent = Collections.emptyMap(); + List history = Collections.emptyList(); + if(revCommit != null) { + fileContent = getFileContent(repository, revCommit); + history = getHistory(git, revCommit); + } + response.setFiles(fileContent); + response.setComments(this.commentRepository.getComments(user).size()); + response.setHistory(history); + applyMetadata(response); + return response; + } catch (IOException e) { + GistError error = new GistError(GistErrorCode.ERR_GIST_CONTENT_NOT_READABLE, + "Could not read content of gist {}", gistId); + logger.error(error.getFormattedMessage() + " with path {}", this.layout.getRootFolder(), e); + throw new GistRepositoryError(error, e); + } + } + + private Map getFileContent(Repository repository, RevCommit commit) throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException, IOException { + Map fileContent = new LinkedHashMap(); + RevTree tree = commit.getTree(); + try (TreeWalk treeWalk = new TreeWalk(repository)) { + treeWalk.addTree(tree); + treeWalk.setRecursive(true); + treeWalk.setPostOrderTraversal(false); + + while(treeWalk.next()) { + + FileContent content = readContent(repository, treeWalk); + fileContent.put(content.getFilename(), content); + } + } + return fileContent; + } + + private RevCommit resolveCommit(Repository repository) throws IOException { + try (RevWalk revWalk = new RevWalk(repository)) { + if(StringUtils.isEmpty(commitId)) { + Ref head = repository.exactRef(REF_HEAD_MASTER); + if(head != null) { + return revWalk.parseCommit(head.getObjectId()); + } + return null; + } else { + return revWalk.parseCommit(ObjectId.fromString(this.commitId)); + } + } + } + + private FileContent readContent(Repository repository, TreeWalk treeWalk) { + + ObjectId objectId = treeWalk.getObjectId(0); + String fileName = treeWalk.getPathString(); + FileContent content = fileContentCache.load(objectId.getName(), fileName); + if(content == null) { + content = new FileContent(); + try { + content.setFilename(fileName); + ObjectLoader loader = repository.open(objectId); + + content.setContent(new String(loader.getBytes(), Charsets.UTF_8)); + content.setSize(loader.getSize()); + content.setTruncated(false); + String language = FilenameUtils.getExtension(fileName); + if (!GitGistRepository.B64_BINARY_EXTENSION.equals(language) && !StringUtils.isEmpty(language)) { + content.setLanguage(language); + } + content.setType(MimetypesFileTypeMap.getDefaultFileTypeMap().getContentType(fileName)); + fileContentCache.save(objectId.getName(), fileName, content); + } catch (IOException e) { + GistError error = new GistError(GistErrorCode.ERR_GIST_CONTENT_NOT_READABLE, + "Could not read content of {} for gist {}", fileName, gistId); + logger.error(error.getFormattedMessage() + " with path {}", this.layout.getRootFolder(), e); + throw new GistRepositoryError(error, e); + } + } + return content; + } + + private void applyMetadata(GistResponse response) { + + GistMetadata metadata = this.getMetadata(); + response.setId(metadata.getId()); + response.setDescription(metadata.getDescription()); + response.setDescription(metadata.getDescription()); + response.setCreatedAt(metadata.getCreatedAt()); + response.setUpdatedAt(metadata.getUpdatedAt()); + response.setPublic(metadata.isPublic()); + if (!StringUtils.isEmpty(metadata.getOwner())) { + GistIdentity owner = new GistIdentity(); + owner.setLogin(metadata.getOwner()); + response.setOwner(owner); + response.setUser(owner); + } + response.addAdditionalProperties(metadata.getAdditionalProperties()); + } + + public GistMetadata getMetadata() { + return metadataStore.load(layout.getMetadataFile()); + } + + private List getHistory(Grgit git, RevCommit commit) { + GitHistoryOperation historyOperation = new GitHistoryOperation(git, commit.getName()); + historyOperation.setHistoryCache(historyCache); + return historyOperation.call(); + } + + public UserDetails getUser() { + return user; + } + + public void setUser(UserDetails user) { + this.user = user; + } + + public GistCommentRepository getCommentRepository() { + return commentRepository; + } + + public void setCommentRepository(GistCommentRepository commentRepository) { + this.commentRepository = commentRepository; + } + + public RepositoryLayout getLayout() { + return layout; + } + + public void setLayout(RepositoryLayout layout) { + this.layout = layout; + } + + public void setMetadataStore(MetadataStore metadataStore) { + this.metadataStore = metadataStore; + } + + public MetadataStore getMetadataStore() { + return this.metadataStore; + } + + public void setHistorycache(HistoryCache historyStore) { + this.historyCache = historyStore; + } + + public String getGistId() { + return gistId; + } + + public void setGistId(String gistId) { + this.gistId = gistId; + } + + public String getCommitId() { + return commitId; + } + + public void setCommitId(String commitId) { + this.commitId = commitId; + } + + public void setFileContentCache(FileContentCache fileContentCache) { + this.fileContentCache = fileContentCache; + } + +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/RepositoryLayout.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/RepositoryLayout.java new file mode 100644 index 0000000..fdfcd93 --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/RepositoryLayout.java @@ -0,0 +1,159 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.repository.git; + +import java.io.File; +import java.io.Serializable; + +public class RepositoryLayout implements Serializable { + + private static final long serialVersionUID = 6380460397113981325L; + + public static final String GIST_META_FILE = "gist.json"; + + public static final String GIST_BARE_REPOSITORY_FOLDER = "repo"; + + public static final String GIST_WORKING_REPOSITORY_FOLDER = ".work"; + + public static final String COMMENT_REPOSITORY_FOLDER = "comments"; + + public static final String COMMENTS_FILE = "comments.json"; + + private File rootFolder; + + private File commentsFolder; + + private File commentsFile; + + private File bareFolder; + + private File metadataFile; + + private File workingFolder; + + public RepositoryLayout(File root) { + rootFolder = root; + commentsFolder = new File(root, COMMENT_REPOSITORY_FOLDER); + commentsFile = new File(commentsFolder, COMMENTS_FILE); + bareFolder = new File(root, GIST_BARE_REPOSITORY_FOLDER); + metadataFile = new File(rootFolder, GIST_META_FILE); + workingFolder = new File(rootFolder, GIST_WORKING_REPOSITORY_FOLDER); + } + + public File getCommentsFolder() { + return commentsFolder; + } + + public void setCommentsFolder(File commentsFolder) { + this.commentsFolder = commentsFolder; + } + + public File getCommentsFile() { + return commentsFile; + } + + public void setCommentsFile(File commentsFile) { + this.commentsFile = commentsFile; + } + + public File getBareFolder() { + return bareFolder; + } + + public void setBareFolder(File bareFolder) { + this.bareFolder = bareFolder; + } + + public File getMetadataFile() { + return metadataFile; + } + + public void setMetadataFile(File metadataFile) { + this.metadataFile = metadataFile; + } + + public File getRootFolder() { + return rootFolder; + } + + public void setRootFolder(File rootFolder) { + this.rootFolder = rootFolder; + } + + public File getWorkingFolder() { + return workingFolder; + } + + public void setWorkingFolder(File workingFolder) { + this.workingFolder = workingFolder; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((commentsFile == null) ? 0 : commentsFile.hashCode()); + result = prime * result + ((commentsFolder == null) ? 0 : commentsFolder.hashCode()); + result = prime * result + ((bareFolder == null) ? 0 : bareFolder.hashCode()); + result = prime * result + ((metadataFile == null) ? 0 : metadataFile.hashCode()); + result = prime * result + ((rootFolder == null) ? 0 : rootFolder.hashCode()); + result = prime * result + ((workingFolder == null) ? 0 : workingFolder.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + RepositoryLayout other = (RepositoryLayout) obj; + if (commentsFile == null) { + if (other.commentsFile != null) + return false; + } else if (!commentsFile.equals(other.commentsFile)) + return false; + if (commentsFolder == null) { + if (other.commentsFolder != null) + return false; + } else if (!commentsFolder.equals(other.commentsFolder)) + return false; + if (bareFolder == null) { + if (other.bareFolder != null) + return false; + } else if (!bareFolder.equals(other.bareFolder)) + return false; + if (metadataFile == null) { + if (other.metadataFile != null) + return false; + } else if (!metadataFile.equals(other.metadataFile)) + return false; + if (rootFolder == null) { + if (other.rootFolder != null) + return false; + } else if (!rootFolder.equals(other.rootFolder)) + return false; + if (workingFolder == null) { + if (other.workingFolder != null) + return false; + } else if (!workingFolder.equals(other.workingFolder)) + return false; + return true; + } + + @Override + public String toString() { + return "RepositoryLayout [rootFolder=" + rootFolder + ", commentsFolder=" + commentsFolder + ", commentsFile=" + + commentsFile + ", gistFolder=" + bareFolder + ", metadataFile=" + metadataFile + ", workingFolder=" + + workingFolder + "]"; + } + + + +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/RepositoryStorageLocator.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/RepositoryStorageLocator.java new file mode 100644 index 0000000..1e3c959 --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/RepositoryStorageLocator.java @@ -0,0 +1,15 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.repository.git; + +import java.io.File; + +public interface RepositoryStorageLocator { + + File getStoragePath(String gistId); + +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/SimpleGistSecurityManager.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/SimpleGistSecurityManager.java new file mode 100644 index 0000000..b7f6fc1 --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/SimpleGistSecurityManager.java @@ -0,0 +1,72 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.repository.git; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.security.core.userdetails.UserDetails; + +import com.mangosolutions.rcloud.rawgist.repository.GistRepository; +import com.mangosolutions.rcloud.rawgist.repository.GistSecurityManager; + +public class SimpleGistSecurityManager implements GistSecurityManager { + + private static final Set READ_RIGHTS = new HashSet<>(Arrays.asList(GistAccessRight.READ, GistAccessRight.WRITE)); + private static final Set WRITE_RIGHTS = new HashSet<>(Arrays.asList(GistAccessRight.WRITE)); + + @Override + public boolean canRead(GistRepository repository, UserDetails userDetails) { + return READ_RIGHTS.contains(this.getAccessRight(repository, userDetails)); + } + + @Override + public boolean canWrite(GistRepository repository, UserDetails userDetails) { + return WRITE_RIGHTS.contains(this.getAccessRight(repository, userDetails)); + } + + @Override + public boolean isOwner(GistRepository repository, UserDetails userDetails) { + return GistRole.OWNER.equals(this.getRole(repository, userDetails)); + } + + @Override + public GistRole getRole(GistRepository repository, UserDetails userDetails) { + return this.isOwner(this.getMetaData(repository), userDetails)? GistRole.OWNER: GistRole.COLLABORATOR; + } + + @Override + public GistAccessRight getAccessRight(GistRepository repository, UserDetails userDetails) { + GistMetadata metadata = getMetaData(repository); + + if(this.canWrite(metadata, userDetails)) { + return GistAccessRight.WRITE; + } + if(this.canRead(metadata, userDetails)) { + return GistAccessRight.READ; + } + return GistAccessRight.NONE; + } + + private boolean canRead(GistMetadata metadata, UserDetails userDetails) { + return true; + } + + private boolean canWrite(GistMetadata metadata, UserDetails userDetails) { + return this.isOwner(metadata, userDetails); + } + + private GistMetadata getMetaData(GistRepository repository) { + return repository.getMetadata(); + } + + private boolean isOwner(GistMetadata metadata, UserDetails userDetails) { + return userDetails.getUsername().equals(metadata.getOwner()); + } + +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/SymetricFourPartRepositoryStorageLocator.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/SymetricFourPartRepositoryStorageLocator.java new file mode 100644 index 0000000..25e8711 --- /dev/null +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/SymetricFourPartRepositoryStorageLocator.java @@ -0,0 +1,32 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.repository.git; + +import java.io.File; + +import com.google.common.base.Splitter; + +public class SymetricFourPartRepositoryStorageLocator implements RepositoryStorageLocator { + + private static final Splitter REPOSITORYID_FOLDER_SPLITTER = Splitter.fixedLength(4); + + private File root; + + public SymetricFourPartRepositoryStorageLocator(File root) { + this.root = root; + } + + @Override + public File getStoragePath(String gistId) { + File folder = root; + for (String path : REPOSITORYID_FOLDER_SPLITTER.split(gistId)) { + folder = new File(folder, path); + } + return folder; + } + +} diff --git a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/UUIDGistIdGenerator.java b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/UUIDGistIdGenerator.java similarity index 81% rename from rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/UUIDGistIdGenerator.java rename to rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/UUIDGistIdGenerator.java index b70a8aa..ef15fbd 100644 --- a/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/UUIDGistIdGenerator.java +++ b/rcloud-gist-service/src/main/java/com/mangosolutions/rcloud/rawgist/repository/git/UUIDGistIdGenerator.java @@ -4,10 +4,12 @@ * SPDX-License-Identifier: MIT * *******************************************************************************/ -package com.mangosolutions.rcloud.rawgist.repository; +package com.mangosolutions.rcloud.rawgist.repository.git; import java.util.UUID; +import com.mangosolutions.rcloud.rawgist.repository.GistIdGenerator; + public class UUIDGistIdGenerator implements GistIdGenerator { /* (non-Javadoc) diff --git a/rcloud-gist-service/src/main/pkg/home/application.yml b/rcloud-gist-service/src/main/pkg/home/application.yml index 717fb7d..e73d9ae 100644 --- a/rcloud-gist-service/src/main/pkg/home/application.yml +++ b/rcloud-gist-service/src/main/pkg/home/application.yml @@ -33,6 +33,15 @@ # The port of the Session Key Server # keyserver.url: # The URL template for the session key server. +# route.redirect: +# A static route that when hit will return a redirect to the client +# route.redirect.from: +# The relative URL that this service uses to issue the redirect from. +# route.redirect.to: +# The URL to redirect to +# route.redirect.copyparams: +# Whether to copy the parameters from the request into the redirection URL. +# defaults to 'true' if not specified ################################################################################ #gists: # root: /var/rcloud-gist-service/gists/ @@ -42,6 +51,11 @@ # port: 4301 # realm: rcloud # url: http://${gists.sessionKeyServerHost}:${gists.sessionKeyServerPort}/valid?token={token}&realm={realm} +# route: +# redirect: +# from: '/login/*' +# to: 'http://example.com' +# copyparams: true ################################################################################ # Security information for administrative access. diff --git a/rcloud-gist-service/src/main/resources/application.yml b/rcloud-gist-service/src/main/resources/application.yml index b6db395..91e766c 100644 --- a/rcloud-gist-service/src/main/resources/application.yml +++ b/rcloud-gist-service/src/main/resources/application.yml @@ -36,31 +36,53 @@ security: gists: root: /var/rcloud-gist-service/gists/ lockTimeout: 30 + security: permissive keyserver: host: 127.0.0.1 port: 4301 realm: rcloud url: http://${gists.keyserver.host}:${gists.keyserver.port}/valid?token={token}&realm={realm} + token: access_token + mediatypes: + - application/vnd.github.beta+json + - application/vnd.github.beta + - application/vnd.github.v3+json + - application/vnd.github.v3 + route: + redirect: + from: '/login/*' + to: http://example.com + copyparams: true + -#TOO update the caches with better default values. +#TODO update the caches with better default values. caches: - name: 'sessionkeys' - evictionPercentage: 25 evictionPolicy: LRU ttl: 300 - evictionCheck: 500 - name: 'gists' - evictionPercentage: 25 evictionPolicy: LRU ttl: 300 - evictionCheck: 500 - name: 'comments' - evictionPercentage: 25 evictionPolicy: LRU ttl: 300 - evictionCheck: 500 - + - + name: 'commentstore' + evictionPolicy: LRU + ttl: 300 + - + name: 'metadatatore' + evictionPolicy: LRU + ttl: 300 + - + name: 'historystore' + evictionPolicy: LRU + ttl: 300 + - + name: 'filecontentcache' + evictionPolicy: LRU + ttl: 300 diff --git a/rcloud-gist-service/src/main/resources/logback-spring.xml b/rcloud-gist-service/src/main/resources/logback-spring.xml index bb83d6d..bc1041f 100644 --- a/rcloud-gist-service/src/main/resources/logback-spring.xml +++ b/rcloud-gist-service/src/main/resources/logback-spring.xml @@ -16,4 +16,6 @@ + + diff --git a/rcloud-gist-service/src/test/java/com/mangosolutions/rcloud/rawgist/ContextStartStopTest.java b/rcloud-gist-service/src/test/java/com/mangosolutions/rcloud/rawgist/ContextStartStopTest.java new file mode 100644 index 0000000..909925d --- /dev/null +++ b/rcloud-gist-service/src/test/java/com/mangosolutions/rcloud/rawgist/ContextStartStopTest.java @@ -0,0 +1,31 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootContextLoader; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.ClassMode; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest(classes = { Application.class }, webEnvironment=WebEnvironment.RANDOM_PORT) +@ContextConfiguration(loader = SpringBootContextLoader.class) +@DirtiesContext(classMode = ClassMode.AFTER_CLASS) +@ActiveProfiles({"test", "default"}) +public class ContextStartStopTest { + + @Test + public void testContextStartStop() { + + } +} diff --git a/rcloud-gist-service/src/test/java/com/mangosolutions/rcloud/rawgist/api/GistRestControllerPerformanceTest.java b/rcloud-gist-service/src/test/java/com/mangosolutions/rcloud/rawgist/api/GistRestControllerPerformanceTest.java new file mode 100644 index 0000000..0095a72 --- /dev/null +++ b/rcloud-gist-service/src/test/java/com/mangosolutions/rcloud/rawgist/api/GistRestControllerPerformanceTest.java @@ -0,0 +1,133 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.api; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.Arrays; +import java.util.Map; + +import org.apache.commons.math3.stat.StatUtils; +import org.apache.commons.math3.stat.descriptive.moment.StandardDeviation; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.helpers.MessageFormatter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.mangosolutions.rcloud.rawgist.Application; +import com.mangosolutions.rcloud.rawgist.model.FileContent; +import com.mangosolutions.rcloud.rawgist.model.GistResponse; + + + + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = Application.class) +@WebAppConfiguration +@ActiveProfiles({"test", "default"}) +public class GistRestControllerPerformanceTest { + + public static MediaType GITHUB_BETA_MEDIA_TYPE = MediaType.parseMediaType("application/vnd.github.beta+json"); + public static MediaType GITHUB_V3_MEDIA_TYPE = MediaType.parseMediaType("application/vnd.github.v3+json"); + + private MockMvc mvc; + + private String defaultGistId; + + @Autowired + private WebApplicationContext webApplicationContext; + + @Autowired + private GistTestHelper gistTestHelper; + + @Autowired + private ObjectMapper objectMapper; + + @Before + public void setup() throws Exception { + this.mvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext).build(); + + gistTestHelper.clearGistRepository(); + gistTestHelper.emptyHazelcast(); + defaultGistId = gistTestHelper.createGist("mock_user", "The default gist", "file1.txt", "This is some default content"); + gistTestHelper.warmupWebService(this.mvc, defaultGistId); + } + + @Test + @WithMockUser("mock_user") + public void testConsistentGistWriteSpeed() throws Exception { + int historySize = 300; +// int historySize = 10; + double[] durations = addFilesToGist(this.defaultGistId, historySize); + StandardDeviation stdDev = new StandardDeviation(); + + double min = StatUtils.min(durations); + double max = StatUtils.max(durations); + double dev = stdDev.evaluate(durations); + + System.out.println(min); + System.out.println(max); + System.out.println(dev); + System.out.println(Arrays.toString(durations)); + + } + + private double[] addFilesToGist(String gistId, int historySize) throws Exception { + double[] durations = new double[historySize]; + + for(int i = 0; i < historySize; i++) { + String fileName = i + "otherfile.txt"; + String fileName2 = i + "anotherfile.txt"; + String fileContent = "Some content for " + i; + String payloadTemplate = "{\"files\": {\"{}\": {\"content\": \"{}\"}, \"{}\": {\"content\": \"{}\"}}}"; + String payload = this.buildMessage(payloadTemplate, fileName, fileContent, fileName2, fileContent); + long start = System.currentTimeMillis(); + MvcResult result = mvc + .perform( + patch("/gists/" + gistId) + .accept(GITHUB_BETA_MEDIA_TYPE) + .contentType(GITHUB_BETA_MEDIA_TYPE) + .content(payload) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) + .andReturn(); + long end = System.currentTimeMillis(); + double diff = end - start; + if(i >= 0) { + durations[i] = diff; + } + String content = result.getResponse().getContentAsString(); + GistResponse response = objectMapper.readValue(content, GistResponse.class); + Map files = response.getFiles(); + Assert.assertEquals(((i + 1)*2) + 1, files.keySet().size()); + } + return durations; + + } + + + private String buildMessage(String format, Object... params) { + return MessageFormatter.arrayFormat(format, params).getMessage(); + } + +} diff --git a/rcloud-gist-service/src/test/java/com/mangosolutions/rcloud/rawgist/api/GistRestControllerTest.java b/rcloud-gist-service/src/test/java/com/mangosolutions/rcloud/rawgist/api/GistRestControllerTest.java new file mode 100644 index 0000000..12da941 --- /dev/null +++ b/rcloud-gist-service/src/test/java/com/mangosolutions/rcloud/rawgist/api/GistRestControllerTest.java @@ -0,0 +1,315 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.api; + +import static org.hamcrest.CoreMatchers.is; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.Map; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.slf4j.helpers.MessageFormatter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.ClassMode; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import com.jayway.jsonpath.JsonPath; +import com.mangosolutions.rcloud.rawgist.Application; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = Application.class) +@WebAppConfiguration +@DirtiesContext(classMode = ClassMode.AFTER_CLASS) +@ActiveProfiles({"test", "default"}) +public class GistRestControllerTest { + + public static MediaType GITHUB_BETA_MEDIA_TYPE = MediaType.parseMediaType("application/vnd.github.beta+json"); + public static MediaType GITHUB_V3_MEDIA_TYPE = MediaType.parseMediaType("application/vnd.github.v3+json"); + + private MockMvc mvc; + + private String defaultGistId; + + @Autowired + private WebApplicationContext webApplicationContext; + + @Autowired + private GistTestHelper gistTestHelper; + + @Before + public void setup() throws Exception { + this.mvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext).build(); + gistTestHelper.clearGistRepository(); + defaultGistId = gistTestHelper.createGist("mock_user", "The default gist", "file1.txt", "This is some default content"); + gistTestHelper.emptyHazelcast(); + } + + @Test + @WithMockUser("mock_user") + public void testCreateGist() throws Exception { + String description = "the description for this gist"; + String fileName = "file1.txt"; + String fileContent = "String file contents"; + String payloadTemplate = "{\"description\": \"{}\",\"public\": true,\"files\": {\"{}\": {\"content\": \"{}\"}}}"; + String payload = this.buildMessage(payloadTemplate, description, fileName, fileContent); + MvcResult result = mvc + .perform( + post("/gists") + .accept(GITHUB_BETA_MEDIA_TYPE) + .contentType(GITHUB_BETA_MEDIA_TYPE) + .content(payload) + ) + .andExpect(status().isCreated()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.owner.login", is("mock_user"))) + .andExpect(jsonPath("$.description", is(description))) + .andExpect(jsonPath("$.comments", is(0))) + .andReturn(); + } + + @Test + @WithMockUser("mock_user") + public void testListGistWithMockUser() throws Exception { + MvcResult result = mvc + .perform( + get("/gists") + .accept(GITHUB_BETA_MEDIA_TYPE) + .contentType(GITHUB_BETA_MEDIA_TYPE) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.length()", is(1))) + .andReturn(); + } + + @Test + @WithMockUser("mock_user_2") + public void testListGistWithMockUser2() throws Exception { + MvcResult result = mvc + .perform( + get("/gists") + .accept(GITHUB_BETA_MEDIA_TYPE) + .contentType(GITHUB_BETA_MEDIA_TYPE) + .with(user("mock_user_2")) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.length()", is(0))) + .andReturn(); + } + + @Test + @WithMockUser("mock_user") + public void testGetGistWithMockUser() throws Exception { + MvcResult result = mvc + .perform( + get("/gists/" + this.defaultGistId) + .accept(GITHUB_BETA_MEDIA_TYPE) + .contentType(GITHUB_BETA_MEDIA_TYPE) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.id", is(this.defaultGistId))) + .andReturn(); + } + + + @Test + @WithMockUser("mock_user") + public void testForkRepositoryWithMockUser() throws Exception { + + //Get the gist response. + String originalGist = mvc + .perform( + get("/gists/" + this.defaultGistId) + .accept(GITHUB_BETA_MEDIA_TYPE) + .contentType(GITHUB_BETA_MEDIA_TYPE) + ) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + //Check that there are no forks. + mvc.perform( + get("/gists/" + this.defaultGistId + "/forks") + .accept(GITHUB_BETA_MEDIA_TYPE) + .contentType(GITHUB_BETA_MEDIA_TYPE) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.length()", is(0))); + + //Fork the repository + String forkResponse = mvc + .perform( + post("/gists/" + this.defaultGistId + "/forks") + .accept(GITHUB_BETA_MEDIA_TYPE) + .contentType(GITHUB_BETA_MEDIA_TYPE) + ) + .andExpect(status().isCreated()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.files.length()", is(1))) + .andReturn().getResponse().getContentAsString(); + String forkedGistId = JsonPath.read(forkResponse, "$.id"); + Assert.assertNotEquals(this.defaultGistId, forkedGistId); + + //update the forked gist + String fileName = "file_in_new_gist.txt"; + String fileContent = "String file contents"; + String payloadTemplate = "{\"files\": {\"{}\": {\"content\": \"{}\"}}}"; + String payload = this.buildMessage(payloadTemplate, fileName, fileContent); + mvc.perform( + patch("/gists/" + forkedGistId) + .accept(GITHUB_BETA_MEDIA_TYPE) + .contentType(GITHUB_BETA_MEDIA_TYPE) + .content(payload) + ).andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.id", is(forkedGistId))) + .andExpect(jsonPath("$.files.length()", is(2))) + .andReturn(); + + //check that original gist hasn't changed + String originalGist2 = mvc + .perform( + get("/gists/" + this.defaultGistId) + .accept(GITHUB_BETA_MEDIA_TYPE) + .contentType(GITHUB_BETA_MEDIA_TYPE) + ) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(); + + Assert.assertEquals(originalGist, originalGist2); + + //get the list of forks + mvc.perform( + get("/gists/" + this.defaultGistId + "/forks") + .accept(GITHUB_BETA_MEDIA_TYPE) + .contentType(GITHUB_BETA_MEDIA_TYPE) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.length()", is(1))) + .andExpect(jsonPath("$.[0].id", is(forkedGistId))); + + } + + @Test + @WithMockUser("mock_user_2") + public void testGetGistWithMockUser2() throws Exception { + MvcResult result = mvc + .perform( + get("/gists/" + this.defaultGistId) + .accept(GITHUB_BETA_MEDIA_TYPE) + .contentType(GITHUB_BETA_MEDIA_TYPE) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.id", is(this.defaultGistId))) + .andReturn(); + } + + @Test + @WithMockUser("mock_user") + public void testGetGistHistory() throws Exception { + int historySize = 30; + int historyIndex = 10; + String historyVersion = null; + { + MvcResult result = mvc + .perform( + get("/gists/" + this.defaultGistId) + .accept(GITHUB_BETA_MEDIA_TYPE) + .contentType(GITHUB_BETA_MEDIA_TYPE) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.id", is(this.defaultGistId))) + .andExpect(jsonPath("$.history.length()", is(1))) + .andExpect(jsonPath("$.files.length()", is(1))) + .andReturn(); + addFilesToGist(this.defaultGistId, historySize); + } + { + MvcResult result = mvc + .perform( + get("/gists/" + this.defaultGistId) + .accept(GITHUB_BETA_MEDIA_TYPE) + .contentType(GITHUB_BETA_MEDIA_TYPE) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.id", is(this.defaultGistId))) + .andExpect(jsonPath("$.history.length()", is(historySize + 1))) + .andExpect(jsonPath("$.files.length()", is(historySize + 1))) + .andReturn(); + String response = result.getResponse().getContentAsString(); + historyVersion = JsonPath.read(response, "$.history[" + historyIndex + "].version"); + } + { + MvcResult result = mvc + .perform( + get("/gists/" + this.defaultGistId + "/" + historyVersion) + .accept(GITHUB_BETA_MEDIA_TYPE) + .contentType(GITHUB_BETA_MEDIA_TYPE) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.id", is(this.defaultGistId))) + .andExpect(jsonPath("$.files.length()", is(historySize + 1 - historyIndex))) + .andReturn(); + String response = result.getResponse().getContentAsString(); + System.out.println(response); + Map files = JsonPath.read(response, "$.files"); + Assert.assertEquals(historySize + 1 - historyIndex, files.size()); + } + } + + private void addFilesToGist(String gistId, int historySize) throws Exception { + for(int i = 0; i < historySize; i++) { + String fileName = i + "otherfile.txt"; + String fileContent = "Some content for " + i; + String payloadTemplate = "{\"files\": {\"{}\": {\"content\": \"{}\"}}}"; + String payload = this.buildMessage(payloadTemplate, fileName, fileContent); + MvcResult result = mvc + .perform( + patch("/gists/" + gistId) + .accept(GITHUB_BETA_MEDIA_TYPE) + .contentType(GITHUB_BETA_MEDIA_TYPE) + .content(payload) + ) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) + .andReturn(); + } + + } + + + + + private String buildMessage(String format, Object... params) { + return MessageFormatter.arrayFormat(format, params).getMessage(); + } + +} diff --git a/rcloud-gist-service/src/test/java/com/mangosolutions/rcloud/rawgist/api/GistTestHelper.java b/rcloud-gist-service/src/test/java/com/mangosolutions/rcloud/rawgist/api/GistTestHelper.java new file mode 100644 index 0000000..f21e120 --- /dev/null +++ b/rcloud-gist-service/src/test/java/com/mangosolutions/rcloud/rawgist/api/GistTestHelper.java @@ -0,0 +1,95 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.api; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +import java.io.File; +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.commons.io.FileUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +import com.hazelcast.config.Config; +import com.hazelcast.config.MapConfig; +import com.hazelcast.core.HazelcastInstance; +import com.hazelcast.core.IMap; +import com.mangosolutions.rcloud.rawgist.model.FileDefinition; +import com.mangosolutions.rcloud.rawgist.model.GistRequest; +import com.mangosolutions.rcloud.rawgist.model.GistResponse; +import com.mangosolutions.rcloud.rawgist.repository.GistRepositoryService; + +@Component +public class GistTestHelper { + + @Autowired + private HazelcastInstance hazelcastInstance; + + @Autowired + private GistRepositoryService service; + + public void emptyHazelcast() { + Config config = hazelcastInstance.getConfig(); + Map mapConfigs = config.getMapConfigs(); + for(Entry entry: mapConfigs.entrySet()) { + IMap map = hazelcastInstance.getMap(entry.getKey()); + map.evictAll(); + } + } + + public void clearGistRepository() throws IOException { + String tmpdir = System.getProperty("java.io.tmpdir"); + File gistFolder = new File(tmpdir + "/gists"); + FileUtils.forceDelete(gistFolder); + FileUtils.forceMkdir(gistFolder); + FileUtils.forceMkdir(new File(gistFolder, ".recycle")); + } + + public String createGist(String user, String description, String fileName, String fileContent) throws Exception { + GistRequest request = new GistRequest(); + request.setDescription(description); + request.setPublic(false); + + Map files = new HashMap<>(); + FileDefinition def = new FileDefinition(); + def.setContent(fileContent); + files.put(fileName, def); + request.setFiles(files); + Collection authorities = Collections.emptyList(); + UserDetails userDetails = new User(user, "gist_user_pwd", authorities); + GistResponse response = this.service.createGist(request, userDetails); + return response.getId(); + } + + public void warmupWebService(MockMvc mvc, String gistId) throws Exception { + for(int i = 0; i < 10; i++) { + MvcResult result = mvc + .perform( + get("/gists/" + gistId) + .accept(MediaType.APPLICATION_JSON_UTF8) + .contentType(MediaType.APPLICATION_JSON_UTF8) + ) + .andExpect(status().isOk()) + .andReturn(); + } + + } + +} diff --git a/rcloud-gist-service/src/test/java/com/mangosolutions/rcloud/rawgist/api/UserRestControllerTest.java b/rcloud-gist-service/src/test/java/com/mangosolutions/rcloud/rawgist/api/UserRestControllerTest.java new file mode 100644 index 0000000..71fdb46 --- /dev/null +++ b/rcloud-gist-service/src/test/java/com/mangosolutions/rcloud/rawgist/api/UserRestControllerTest.java @@ -0,0 +1,81 @@ +/******************************************************************************* +* Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +* +* SPDX-License-Identifier: MIT +* +*******************************************************************************/ +package com.mangosolutions.rcloud.rawgist.api; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.hamcrest.CoreMatchers.is; + + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.ClassMode; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import com.mangosolutions.rcloud.rawgist.Application; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = Application.class) +@DirtiesContext(classMode = ClassMode.AFTER_CLASS) +@WebAppConfiguration +@ActiveProfiles({"test", "default"}) +public class UserRestControllerTest { + + public static MediaType GITHUB_BETA_MEDIA_TYPE = MediaType.parseMediaType("application/vnd.github.beta+json"); + public static MediaType GITHUB_V3_MEDIA_TYPE = MediaType.parseMediaType("application/vnd.github.v3+json"); + + private MockMvc mvc; + + @Autowired + private WebApplicationContext webApplicationContext; + + @Before + public void setup() throws Exception { + this.mvc = MockMvcBuilders.webAppContextSetup(this.webApplicationContext).build(); + } + + @Test + @WithMockUser("mock_user") + public void testGetUserWithApplicationJsonMediaType() throws Exception { + ResultActions resultActions = mvc.perform(get("/user") + .accept(MediaType.APPLICATION_JSON_UTF8)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.login", is("mock_user"))); + } + + @Test + @WithMockUser("mock_user") + public void testGetUserWithGithubBetaMediaType() throws Exception { + ResultActions resultActions = mvc.perform(get("/user") + .accept(GITHUB_BETA_MEDIA_TYPE)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.login", is("mock_user"))); + } + + @Test + @WithMockUser("mock_user") + public void testGetUserWithGithubV3MediaType() throws Exception { + ResultActions resultActions = mvc.perform(get("/user") + .accept(GITHUB_V3_MEDIA_TYPE)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.login", is("mock_user"))); + } +} diff --git a/rcloud-gist-service/src/test/java/com/mangosolutions/rcloud/rawgist/repository/GistErrorTest.java b/rcloud-gist-service/src/test/java/com/mangosolutions/rcloud/rawgist/repository/GistErrorTest.java index 0a7d9bc..eb081e5 100644 --- a/rcloud-gist-service/src/test/java/com/mangosolutions/rcloud/rawgist/repository/GistErrorTest.java +++ b/rcloud-gist-service/src/test/java/com/mangosolutions/rcloud/rawgist/repository/GistErrorTest.java @@ -9,8 +9,6 @@ import org.junit.Assert; import org.junit.Test; -import com.mangosolutions.rcloud.rawgist.repository.GistError.GistErrorCode; - public class GistErrorTest { diff --git a/rcloud-gist-service/src/test/java/com/mangosolutions/rcloud/rawgist/repository/GitGistCommentRepositoryTest.java b/rcloud-gist-service/src/test/java/com/mangosolutions/rcloud/rawgist/repository/GitGistCommentRepositoryTest.java index 24ee404..431ae0f 100644 --- a/rcloud-gist-service/src/test/java/com/mangosolutions/rcloud/rawgist/repository/GitGistCommentRepositoryTest.java +++ b/rcloud-gist-service/src/test/java/com/mangosolutions/rcloud/rawgist/repository/GitGistCommentRepositoryTest.java @@ -12,7 +12,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.UUID; import org.junit.Assert; import org.junit.Before; @@ -26,6 +25,8 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.ClassMode; import org.springframework.test.context.junit4.SpringRunner; import com.fasterxml.jackson.databind.ObjectMapper; @@ -34,9 +35,13 @@ import com.mangosolutions.rcloud.rawgist.model.GistHistory; import com.mangosolutions.rcloud.rawgist.model.GistRequest; import com.mangosolutions.rcloud.rawgist.model.GistResponse; +import com.mangosolutions.rcloud.rawgist.repository.git.GistOperationFactory; +import com.mangosolutions.rcloud.rawgist.repository.git.GitGistRepository; +import com.mangosolutions.rcloud.rawgist.repository.git.UUIDGistIdGenerator; @RunWith(SpringRunner.class) +@DirtiesContext(classMode = ClassMode.AFTER_CLASS) @AutoConfigureJsonTesters @JsonTest public class GitGistCommentRepositoryTest { @@ -47,6 +52,8 @@ public class GitGistCommentRepositoryTest { private UserDetails userDetails; + private GistOperationFactory gistOperationFactory; + @Autowired private ObjectMapper objectMapper; @@ -56,10 +63,11 @@ public class GitGistCommentRepositoryTest { @Before public void setup() { File repositoryFolder = folder.getRoot(); - gistId = UUID.randomUUID().toString(); + gistId = new UUIDGistIdGenerator().generateId(); Collection authorities = Collections.emptyList(); userDetails = new User("gist_user", "gist_user_pwd", authorities); - repository = new GitGistRepository(repositoryFolder, gistId, objectMapper, userDetails); + gistOperationFactory = new GistOperationFactory(objectMapper); + repository = new GitGistRepository(repositoryFolder, gistOperationFactory); } @Test @@ -71,6 +79,28 @@ public void createNewGistTest() { validateResponse(1, expectedDescription, expectedFilename, expectedContent, response); validateHistory(response, 1); } + + @Test + public void updateGistWithTwoFiles() { + String expectedDescription = "This is a cool gist"; + String expectedFilename = "i_am_file_1.R"; + + String initialContent = "I am the content of the file"; + String newFilename = "i_am_file_2.R"; + String newContent = "I am the content of a different file"; + String newFilename2 = "i_am_file_3.R"; + String newContent2 = "I am the content of a different file2"; + this.createGist(expectedDescription, new String[]{expectedFilename, initialContent}); + GistRequest request = this.createGistRequest(null, new String[]{newFilename, newContent}); + FileDefinition def = new FileDefinition(); + def.setContent(newContent2); + request.getFiles().put(newFilename2, def); + GistResponse response = this.repository.updateGist(request, userDetails); + validateResponse(3, expectedDescription, expectedFilename, initialContent, response); + validateResponse(3, expectedDescription, newFilename, newContent, response); + validateResponse(3, expectedDescription, newFilename2, newContent2, response); + validateHistory(response, 2); + } @Test @@ -79,7 +109,7 @@ public void getGistTest() { String expectedFilename = "i_am_file.R"; String expectedContent = "I am the content of the file"; this.createGist(expectedDescription, new String[]{expectedFilename, expectedContent}); - GistResponse response = repository.getGist(userDetails); + GistResponse response = repository.readGist(userDetails); validateResponse(1, expectedDescription, expectedFilename, expectedContent, response); validateHistory(response, 1); } @@ -127,7 +157,7 @@ public void deleteGistFileTest() { @Test - public void getGistRevision() { + public void getGistRevisionTest() { String expectedDescription = "This is a cool gist"; String initialFilename = "i_am_file_1.R"; @@ -143,14 +173,46 @@ public void getGistRevision() { GistHistory history = response.getHistory().get(1); String commitId = history.getVersion(); - response = repository.getGist(commitId, userDetails); + response = repository.readGist(commitId, userDetails); validateResponse(2, expectedDescription, initialFilename, initialContent, response); validateResponse(2, expectedDescription, newFilename, newContent, response); validateHistory(response, 2); } @Test - public void updateGistWithDifferentUser() { + public void moveGistFileTest() { + String expectedDescription = "This is a cool gist"; + String initialFilename = "i_am_file_1.R"; + + String initialContent = "I am the content of the file"; + String newFilename = "i_am_file_2.R"; + String newContent = "I am the content of a different file"; + String movedFileName = "i_am_file_3.R"; + this.createGist(expectedDescription, new String[]{initialFilename, initialContent}); + this.updateGist(new String[]{newFilename, newContent}); + + + GistRequest request = this.createGistRequest(null, new String[]{initialFilename}); + FileDefinition def = new FileDefinition(); + + def.setFilename(movedFileName); + request.getFiles().put(initialFilename, def); + + GistResponse response = this.repository.updateGist(request, userDetails); + + validateResponse(2, expectedDescription, newFilename, newContent, response); + validateHistory(response, 3); + + GistHistory history = response.getHistory().get(1); + String commitId = history.getVersion(); + response = repository.readGist(commitId, userDetails); + validateResponse(2, expectedDescription, initialFilename, initialContent, response); + validateResponse(2, expectedDescription, newFilename, newContent, response); + validateHistory(response, 2); + } + + @Test + public void updateGistWithDifferentUserTest() { String expectedDescription = "This is a cool gist"; String expectedFilename = "i_am_file.R"; String initialContent = "I am the content of the file"; @@ -166,7 +228,7 @@ public void updateGistWithDifferentUser() { GistRequest request = createGistRequest(null, new String[]{newFilename, newContent}); Collection authorities = Collections.emptyList(); UserDetails userDetails = new User("another_gist_user", "gist_user_pwd", authorities); - response = repository.editGist(request, userDetails); + response = repository.updateGist(request, userDetails); validateResponse(2, expectedDescription, newFilename, newContent, response); GistHistory history = response.getHistory().get(0); Assert.assertEquals(userDetails.getUsername(), history.getUser().getLogin()); @@ -174,12 +236,12 @@ public void updateGistWithDifferentUser() { private GistResponse updateGist(String[] contents) { GistRequest request = createGistRequest(null, contents); - return repository.editGist(request, userDetails); + return repository.updateGist(request, userDetails); } private GistResponse createGist(String description, String[]... contents) { GistRequest request = createGistRequest(description, contents); - return repository.createGist(request, userDetails); + return repository.createGist(request, this.gistId, userDetails); } private GistRequest createGistRequest(String description, String[]... contents) { diff --git a/rcloud-gist-service/src/test/java/com/mangosolutions/rcloud/rawgist/repository/GitGistRepositoryTest.java b/rcloud-gist-service/src/test/java/com/mangosolutions/rcloud/rawgist/repository/GitGistRepositoryTest.java index 73c6c3c..5e15dc2 100644 --- a/rcloud-gist-service/src/test/java/com/mangosolutions/rcloud/rawgist/repository/GitGistRepositoryTest.java +++ b/rcloud-gist-service/src/test/java/com/mangosolutions/rcloud/rawgist/repository/GitGistRepositoryTest.java @@ -7,6 +7,7 @@ package com.mangosolutions.rcloud.rawgist.repository; import java.io.File; +import java.io.IOException; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -14,6 +15,7 @@ import java.util.Map; import java.util.UUID; +import org.apache.commons.io.FileUtils; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; @@ -26,25 +28,34 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.ClassMode; import org.springframework.test.context.junit4.SpringRunner; import com.fasterxml.jackson.databind.ObjectMapper; +import com.mangosolutions.rcloud.rawgist.model.FileContent; import com.mangosolutions.rcloud.rawgist.model.FileDefinition; import com.mangosolutions.rcloud.rawgist.model.GistComment; import com.mangosolutions.rcloud.rawgist.model.GistCommentResponse; import com.mangosolutions.rcloud.rawgist.model.GistRequest; import com.mangosolutions.rcloud.rawgist.model.GistResponse; +import com.mangosolutions.rcloud.rawgist.repository.git.GistOperationFactory; +import com.mangosolutions.rcloud.rawgist.repository.git.GitGistRepository; +import com.mangosolutions.rcloud.rawgist.repository.git.RepositoryLayout; @RunWith(SpringRunner.class) +@DirtiesContext(classMode = ClassMode.AFTER_CLASS) @AutoConfigureJsonTesters @JsonTest public class GitGistRepositoryTest { private GitGistRepository repository; - private GitGistCommentRepository commentRepository; + private GistCommentRepository commentRepository; + + private File repositoryFolder; private String gistId; @@ -52,19 +63,22 @@ public class GitGistRepositoryTest { @Autowired private ObjectMapper objectMapper; + + private GistOperationFactory gistOperationFactory; @Rule public TemporaryFolder folder = new TemporaryFolder(); @Before public void setup() { - File repositoryFolder = folder.getRoot(); + repositoryFolder = folder.getRoot(); gistId = UUID.randomUUID().toString(); Collection authorities = Collections.emptyList(); userDetails = new User("gist_user", "gist_user_pwd", authorities); - repository = new GitGistRepository(repositoryFolder, gistId, objectMapper, userDetails); + gistOperationFactory = new GistOperationFactory(objectMapper); + repository = new GitGistRepository(repositoryFolder, gistOperationFactory); this.populateTestRepository(); - commentRepository = new GitGistCommentRepository(repositoryFolder, gistId, objectMapper); + commentRepository = repository.getCommentRepository(); } public void populateTestRepository() { @@ -77,6 +91,47 @@ public void populateTestRepository() { this.createGist(expectedDescription, new String[]{expectedFilename, initialContent}); this.updateGist(new String[]{newFilename, newContent}); } + + @Test + public void deleteWorkingFolderTest() throws IOException { + RepositoryLayout layout = new RepositoryLayout(repositoryFolder); + File workingFolder = layout.getWorkingFolder(); + Assert.assertTrue(workingFolder.exists()); + FileUtils.forceDelete(workingFolder); + String newFilename = "file_after_delete.txt"; + String newContent = "Contents of the file"; + GistResponse response = this.updateGist(new String[]{newFilename, newContent}); + Map files = response.getFiles(); + Assert.assertTrue(files.containsKey(newFilename)); + Assert.assertEquals(3, files.size()); + } + + @Test + public void shouldBeUpdateNotMoveTest() throws IOException { + String newFilename = "file_after_delete.txt"; + String newContent = "Contents of the file"; + GistResponse response = this.updateGist(new String[]{newFilename, newContent}); + GistRequest request = this.createGistRequest(null, new String[]{newFilename, newContent + "new new new"}); + request.getFiles().get(newFilename).setFilename(newFilename); + response = this.repository.updateGist(request, userDetails); + Map files = response.getFiles(); + Assert.assertTrue(files.containsKey(newFilename)); + Assert.assertEquals(3, files.size()); + Assert.assertEquals(newContent + "new new new", files.get(newFilename).getContent()); + } + + @Test + public void emptyCommitTest() throws IOException { + String newFilename = "file_after_delete.txt"; + String newContent = "Contents of the file"; + GistResponse response = this.updateGist(new String[]{newFilename, newContent}); + GistRequest request = this.createGistRequest(null, new String[]{newFilename, newContent}); + request.getFiles().get(newFilename).setFilename(newFilename); + response = this.repository.updateGist(request, userDetails); + Map files = response.getFiles(); + Assert.assertTrue(files.containsKey(newFilename)); + Assert.assertEquals(3, files.size()); + } @Test public void getEmptyCommentsTest() { @@ -151,12 +206,12 @@ public void createLotsOfCommentsTest() { private GistResponse updateGist(String[] contents) { GistRequest request = createGistRequest(null, contents); - return repository.editGist(request, userDetails); + return repository.updateGist(request, userDetails); } private GistResponse createGist(String description, String[]... contents) { GistRequest request = createGistRequest(description, contents); - return repository.createGist(request, userDetails); + return repository.createGist(request, this.gistId, userDetails); } private GistRequest createGistRequest(String description, String[]... contents) { diff --git a/rcloud-gist-service/src/test/resources/application-test.yml b/rcloud-gist-service/src/test/resources/application-test.yml new file mode 100644 index 0000000..9202cd1 --- /dev/null +++ b/rcloud-gist-service/src/test/resources/application-test.yml @@ -0,0 +1,85 @@ +#******************************************************************************* +# Copyright (c) 2017 AT&T Intellectual Property, [http://www.att.com] +# +# SPDX-License-Identifier: MIT +# +#******************************************************************************* +server: + port: 17020 + +management: + port: 17021 + security: + enabled: true + +info: + version: 0.1.0 + stage: dev + +spring: + jackson: + serialization: + write_dates_as_timestamps: false + application: + name: rcloud-gist-service + cloud: + config: + enabled: false + + +security: + user: + name: admin + basic: + enabled: false + +gists: + root: ${java.io.tmpdir}/gists/ + lockTimeout: 30 + keyserver: + host: 127.0.0.1 + port: 4301 + realm: rcloud + url: http://${gists.keyserver.host}:${gists.keyserver.port}/valid?token={token}&realm={realm} + token: access_token + mediatypes: + - application/vnd.github.beta+json + - application/vnd.github.beta + - application/vnd.github.v3+json + - application/vnd.github.v3 + route: + redirect: + from: '/login/*' + to: http://example.com + copyparams: true + +#TOO update the caches with better default values. +caches: + - + name: 'sessionkeys' + evictionPolicy: LRU + ttl: 300 + - + name: 'gists' + evictionPolicy: LRU + ttl: 300 + - + name: 'comments' + evictionPolicy: LRU + ttl: 300 + - + name: 'commentstore' + evictionPolicy: LRU + ttl: 300 + - + name: 'metadatatore' + evictionPolicy: LRU + ttl: 300 + - + name: 'historystore' + evictionPolicy: LRU + ttl: 300 + - + name: 'filecontentcache' + evictionPolicy: LRU + ttl: 300