just getting it her
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
package org.cloudfoundry.samples.music;
|
||||
|
||||
import org.cloudfoundry.samples.music.config.SpringApplicationContextInitializer;
|
||||
import org.cloudfoundry.samples.music.repositories.AlbumRepositoryPopulator;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
|
||||
@SpringBootApplication
|
||||
public class Application {
|
||||
|
||||
public static void main(String[] args) {
|
||||
new SpringApplicationBuilder(Application.class)
|
||||
.initializers(new SpringApplicationContextInitializer())
|
||||
.listeners(new AlbumRepositoryPopulator())
|
||||
.application()
|
||||
.run(args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package org.cloudfoundry.samples.music.config;
|
||||
|
||||
import io.pivotal.cfenv.core.CfEnv;
|
||||
import io.pivotal.cfenv.core.CfService;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
|
||||
import org.springframework.context.ApplicationContextInitializer;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.core.env.MapPropertySource;
|
||||
import org.springframework.core.env.Profiles;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class SpringApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(SpringApplicationContextInitializer.class);
|
||||
|
||||
private static final Map<String, List<String>> profileNameToServiceTags = new HashMap<>();
|
||||
static {
|
||||
profileNameToServiceTags.put("mongodb", Collections.singletonList("mongodb"));
|
||||
profileNameToServiceTags.put("postgres", Collections.singletonList("postgres"));
|
||||
profileNameToServiceTags.put("mysql", Collections.singletonList("mysql"));
|
||||
profileNameToServiceTags.put("redis", Collections.singletonList("redis"));
|
||||
profileNameToServiceTags.put("oracle", Collections.singletonList("oracle"));
|
||||
profileNameToServiceTags.put("sqlserver", Collections.singletonList("sqlserver"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(ConfigurableApplicationContext applicationContext) {
|
||||
ConfigurableEnvironment appEnvironment = applicationContext.getEnvironment();
|
||||
|
||||
validateActiveProfiles(appEnvironment);
|
||||
|
||||
addCloudProfile(appEnvironment);
|
||||
|
||||
excludeAutoConfiguration(appEnvironment);
|
||||
}
|
||||
|
||||
private void addCloudProfile(ConfigurableEnvironment appEnvironment) {
|
||||
CfEnv cfEnv = new CfEnv();
|
||||
|
||||
List<String> profiles = new ArrayList<>();
|
||||
|
||||
List<CfService> services = cfEnv.findAllServices();
|
||||
List<String> serviceNames = services.stream()
|
||||
.map(CfService::getName)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
logger.info("Found services " + StringUtils.collectionToCommaDelimitedString(serviceNames));
|
||||
|
||||
for (CfService service : services) {
|
||||
for (String profileKey : profileNameToServiceTags.keySet()) {
|
||||
if (service.getTags().containsAll(profileNameToServiceTags.get(profileKey))) {
|
||||
profiles.add(profileKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (profiles.size() > 1) {
|
||||
throw new IllegalStateException(
|
||||
"Only one service of the following types may be bound to this application: " +
|
||||
profileNameToServiceTags.values().toString() + ". " +
|
||||
"These services are bound to the application: [" +
|
||||
StringUtils.collectionToCommaDelimitedString(profiles) + "]");
|
||||
}
|
||||
|
||||
if (profiles.size() > 0) {
|
||||
logger.info("Setting service profile " + profiles.get(0));
|
||||
appEnvironment.addActiveProfile(profiles.get(0));
|
||||
}
|
||||
}
|
||||
|
||||
private void validateActiveProfiles(ConfigurableEnvironment appEnvironment) {
|
||||
Set<String> validLocalProfiles = profileNameToServiceTags.keySet();
|
||||
|
||||
List<String> serviceProfiles = Stream.of(appEnvironment.getActiveProfiles())
|
||||
.filter(validLocalProfiles::contains)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (serviceProfiles.size() > 1) {
|
||||
throw new IllegalStateException("Only one active Spring profile may be set among the following: " +
|
||||
validLocalProfiles.toString() + ". " +
|
||||
"These profiles are active: [" +
|
||||
StringUtils.collectionToCommaDelimitedString(serviceProfiles) + "]");
|
||||
}
|
||||
}
|
||||
|
||||
private void excludeAutoConfiguration(ConfigurableEnvironment environment) {
|
||||
List<String> exclude = new ArrayList<>();
|
||||
if (environment.acceptsProfiles(Profiles.of("redis"))) {
|
||||
excludeDataSourceAutoConfiguration(exclude);
|
||||
excludeMongoAutoConfiguration(exclude);
|
||||
} else if (environment.acceptsProfiles(Profiles.of("mongodb"))) {
|
||||
excludeDataSourceAutoConfiguration(exclude);
|
||||
excludeRedisAutoConfiguration(exclude);
|
||||
} else {
|
||||
excludeMongoAutoConfiguration(exclude);
|
||||
excludeRedisAutoConfiguration(exclude);
|
||||
}
|
||||
|
||||
Map<String, Object> properties = Collections.singletonMap("spring.autoconfigure.exclude",
|
||||
StringUtils.collectionToCommaDelimitedString(exclude));
|
||||
|
||||
PropertySource<?> propertySource = new MapPropertySource("springMusicAutoConfig", properties);
|
||||
|
||||
environment.getPropertySources().addFirst(propertySource);
|
||||
}
|
||||
|
||||
private void excludeDataSourceAutoConfiguration(List<String> exclude) {
|
||||
exclude.add(DataSourceAutoConfiguration.class.getName());
|
||||
}
|
||||
|
||||
private void excludeMongoAutoConfiguration(List<String> exclude) {
|
||||
exclude.addAll(Arrays.asList(
|
||||
MongoAutoConfiguration.class.getName(),
|
||||
MongoDataAutoConfiguration.class.getName(),
|
||||
MongoRepositoriesAutoConfiguration.class.getName()
|
||||
));
|
||||
}
|
||||
|
||||
private void excludeRedisAutoConfiguration(List<String> exclude) {
|
||||
exclude.addAll(Arrays.asList(
|
||||
RedisAutoConfiguration.class.getName(),
|
||||
RedisRepositoriesAutoConfiguration.class.getName()
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.cloudfoundry.samples.music.config.data;
|
||||
|
||||
import org.cloudfoundry.samples.music.domain.Album;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
@Configuration
|
||||
@Profile("redis")
|
||||
public class RedisConfig {
|
||||
|
||||
@Bean
|
||||
public RedisTemplate<String, Album> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
|
||||
RedisTemplate<String, Album> template = new RedisTemplate<>();
|
||||
|
||||
template.setConnectionFactory(redisConnectionFactory);
|
||||
|
||||
RedisSerializer<String> stringSerializer = new StringRedisSerializer();
|
||||
RedisSerializer<Album> albumSerializer = new Jackson2JsonRedisSerializer<>(Album.class);
|
||||
|
||||
template.setKeySerializer(stringSerializer);
|
||||
template.setValueSerializer(albumSerializer);
|
||||
template.setHashKeySerializer(stringSerializer);
|
||||
template.setHashValueSerializer(albumSerializer);
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package org.cloudfoundry.samples.music.domain;
|
||||
|
||||
import org.hibernate.annotations.GenericGenerator;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.Id;
|
||||
|
||||
@Entity
|
||||
public class Album {
|
||||
@Id
|
||||
@Column(length=40)
|
||||
@GeneratedValue(generator="randomId")
|
||||
@GenericGenerator(name="randomId", strategy="org.cloudfoundry.samples.music.domain.RandomIdGenerator")
|
||||
private String id;
|
||||
|
||||
private String title;
|
||||
private String artist;
|
||||
private String releaseYear;
|
||||
private String genre;
|
||||
private int trackCount;
|
||||
private String albumId;
|
||||
|
||||
public Album() {
|
||||
}
|
||||
|
||||
public Album(String title, String artist, String releaseYear, String genre) {
|
||||
this.title = title;
|
||||
this.artist = artist;
|
||||
this.releaseYear = releaseYear;
|
||||
this.genre = genre;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getArtist() {
|
||||
return artist;
|
||||
}
|
||||
|
||||
public void setArtist(String artist) {
|
||||
this.artist = artist;
|
||||
}
|
||||
|
||||
public String getReleaseYear() {
|
||||
return releaseYear;
|
||||
}
|
||||
|
||||
public void setReleaseYear(String releaseYear) {
|
||||
this.releaseYear = releaseYear;
|
||||
}
|
||||
|
||||
public String getGenre() {
|
||||
return genre;
|
||||
}
|
||||
|
||||
public void setGenre(String genre) {
|
||||
this.genre = genre;
|
||||
}
|
||||
|
||||
public int getTrackCount() {
|
||||
return trackCount;
|
||||
}
|
||||
|
||||
public void setTrackCount(int trackCount) {
|
||||
this.trackCount = trackCount;
|
||||
}
|
||||
|
||||
public String getAlbumId() {
|
||||
return albumId;
|
||||
}
|
||||
|
||||
public void setAlbumId(String albumId) {
|
||||
this.albumId = albumId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.cloudfoundry.samples.music.domain;
|
||||
|
||||
public class ApplicationInfo {
|
||||
private String[] profiles;
|
||||
private String[] services;
|
||||
|
||||
public ApplicationInfo(String[] profiles, String[] services) {
|
||||
this.profiles = profiles;
|
||||
this.services = services;
|
||||
}
|
||||
|
||||
public String[] getProfiles() {
|
||||
return profiles;
|
||||
}
|
||||
|
||||
public void setProfiles(String[] profiles) {
|
||||
this.profiles = profiles;
|
||||
}
|
||||
|
||||
public String[] getServices() {
|
||||
return services;
|
||||
}
|
||||
|
||||
public void setServices(String[] services) {
|
||||
this.services = services;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.cloudfoundry.samples.music.domain;
|
||||
|
||||
import org.hibernate.HibernateException;
|
||||
import org.hibernate.engine.spi.SharedSessionContractImplementor;
|
||||
import org.hibernate.id.IdentifierGenerator;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.UUID;
|
||||
|
||||
public class RandomIdGenerator implements IdentifierGenerator {
|
||||
@Override
|
||||
public Serializable generate(SharedSessionContractImplementor session, Object object) throws HibernateException {
|
||||
return generateId();
|
||||
}
|
||||
|
||||
public String generateId() {
|
||||
return UUID.randomUUID().toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.cloudfoundry.samples.music.repositories;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.cloudfoundry.samples.music.domain.Album;
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.data.repository.init.Jackson2ResourceReader;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public class AlbumRepositoryPopulator implements ApplicationListener<ApplicationReadyEvent> {
|
||||
private final Jackson2ResourceReader resourceReader;
|
||||
private final Resource sourceData;
|
||||
|
||||
public AlbumRepositoryPopulator() {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
resourceReader = new Jackson2ResourceReader(mapper);
|
||||
sourceData = new ClassPathResource("albums.json");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(ApplicationReadyEvent event) {
|
||||
CrudRepository albumRepository =
|
||||
BeanFactoryUtils.beanOfTypeIncludingAncestors(event.getApplicationContext(), CrudRepository.class);
|
||||
|
||||
if (albumRepository != null && albumRepository.count() == 0) {
|
||||
populate(albumRepository);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private void populate(CrudRepository repository) {
|
||||
Object entity = getEntityFromResource(sourceData);
|
||||
|
||||
if (entity instanceof Collection) {
|
||||
for (Album album : (Collection<Album>) entity) {
|
||||
if (album != null) {
|
||||
repository.save(album);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
repository.save(entity);
|
||||
}
|
||||
}
|
||||
|
||||
private Object getEntityFromResource(Resource resource) {
|
||||
try {
|
||||
return resourceReader.readFrom(resource, this.getClass().getClassLoader());
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.cloudfoundry.samples.music.repositories.jpa;
|
||||
|
||||
import org.cloudfoundry.samples.music.domain.Album;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
@Profile({"!mongodb", "!redis"})
|
||||
public interface JpaAlbumRepository extends JpaRepository<Album, String> {
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.cloudfoundry.samples.music.repositories.mongodb;
|
||||
|
||||
import org.cloudfoundry.samples.music.domain.Album;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
@Profile("mongodb")
|
||||
public interface MongoAlbumRepository extends MongoRepository<Album, String> {
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package org.cloudfoundry.samples.music.repositories.redis;
|
||||
|
||||
import org.cloudfoundry.samples.music.domain.Album;
|
||||
import org.cloudfoundry.samples.music.domain.RandomIdGenerator;
|
||||
|
||||
import org.springframework.context.annotation.Profile;
|
||||
import org.springframework.data.redis.core.HashOperations;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
@Repository
|
||||
@Profile("redis")
|
||||
public class RedisAlbumRepository implements CrudRepository<Album, String> {
|
||||
public static final String ALBUMS_KEY = "albums";
|
||||
|
||||
private final RandomIdGenerator idGenerator;
|
||||
private final HashOperations<String, String, Album> hashOps;
|
||||
|
||||
public RedisAlbumRepository(RedisTemplate<String, Album> redisTemplate) {
|
||||
this.hashOps = redisTemplate.opsForHash();
|
||||
this.idGenerator = new RandomIdGenerator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S extends Album> S save(S album) {
|
||||
if (album.getId() == null) {
|
||||
album.setId(idGenerator.generateId());
|
||||
}
|
||||
|
||||
hashOps.put(ALBUMS_KEY, album.getId(), album);
|
||||
|
||||
return album;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <S extends Album> Iterable<S> saveAll(Iterable<S> albums) {
|
||||
List<S> result = new ArrayList<>();
|
||||
|
||||
for (S entity : albums) {
|
||||
save(entity);
|
||||
result.add(entity);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Album> findById(String id) {
|
||||
return Optional.ofNullable(hashOps.get(ALBUMS_KEY, id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsById(String id) {
|
||||
return hashOps.hasKey(ALBUMS_KEY, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<Album> findAll() {
|
||||
return hashOps.values(ALBUMS_KEY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<Album> findAllById(Iterable<String> ids) {
|
||||
return hashOps.multiGet(ALBUMS_KEY, convertIterableToList(ids));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long count() {
|
||||
return hashOps.keys(ALBUMS_KEY).size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteById(String id) {
|
||||
hashOps.delete(ALBUMS_KEY, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(Album album) {
|
||||
hashOps.delete(ALBUMS_KEY, album.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteAll(Iterable<? extends Album> albums) {
|
||||
for (Album album : albums) {
|
||||
delete(album);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteAll() {
|
||||
Set<String> ids = hashOps.keys(ALBUMS_KEY);
|
||||
for (String id : ids) {
|
||||
deleteById(id);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteAllById(Iterable<? extends String> ids) {
|
||||
for (String id : ids) {
|
||||
deleteById(id);
|
||||
}
|
||||
}
|
||||
|
||||
private <T> List<T> convertIterableToList(Iterable<T> iterable) {
|
||||
List<T> list = new ArrayList<>();
|
||||
for (T object : iterable) {
|
||||
list.add(object);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.cloudfoundry.samples.music.web;
|
||||
|
||||
import org.cloudfoundry.samples.music.domain.Album;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
|
||||
@RestController
|
||||
@RequestMapping(value = "/albums")
|
||||
public class AlbumController {
|
||||
private static final Logger logger = LoggerFactory.getLogger(AlbumController.class);
|
||||
private CrudRepository<Album, String> repository;
|
||||
|
||||
@Autowired
|
||||
public AlbumController(CrudRepository<Album, String> repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET)
|
||||
public Iterable<Album> albums() {
|
||||
return repository.findAll();
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.PUT)
|
||||
public Album add(@RequestBody @Valid Album album) {
|
||||
logger.info("Adding album " + album.getId());
|
||||
return repository.save(album);
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.POST)
|
||||
public Album update(@RequestBody @Valid Album album) {
|
||||
logger.info("Updating album " + album.getId());
|
||||
return repository.save(album);
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/{id}", method = RequestMethod.GET)
|
||||
public Album getById(@PathVariable String id) {
|
||||
logger.info("Getting album " + id);
|
||||
return repository.findById(id).orElse(null);
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
|
||||
public void deleteById(@PathVariable String id) {
|
||||
logger.info("Deleting album " + id);
|
||||
repository.deleteById(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.cloudfoundry.samples.music.web;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/errors")
|
||||
public class ErrorController {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ErrorController.class);
|
||||
private List<int[]> junk = new ArrayList<>();
|
||||
|
||||
@RequestMapping(value = "/kill")
|
||||
public void kill() {
|
||||
logger.info("Forcing application exit");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/fill-heap")
|
||||
public void fillHeap() {
|
||||
logger.info("Filling heap with junk, to initiate a crash");
|
||||
while (true) {
|
||||
junk.add(new int[9999999]);
|
||||
}
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/throw")
|
||||
public void throwException() {
|
||||
logger.info("Forcing an exception to be thrown");
|
||||
throw new NullPointerException("Forcing an exception to be thrown");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package org.cloudfoundry.samples.music.web;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.cloudfoundry.samples.music.domain.ApplicationInfo;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import io.pivotal.cfenv.core.CfEnv;
|
||||
import io.pivotal.cfenv.core.CfService;
|
||||
|
||||
@RestController
|
||||
public class InfoController {
|
||||
private final CfEnv cfEnv;
|
||||
// just a change to kick a build
|
||||
private Environment springEnvironment;
|
||||
|
||||
@Autowired
|
||||
public InfoController(Environment springEnvironment) {
|
||||
this.springEnvironment = springEnvironment;
|
||||
this.cfEnv = new CfEnv();
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/request")
|
||||
public Map<String, String> requestInfo(HttpServletRequest req) {
|
||||
HashMap<String, String> result = new HashMap<>();
|
||||
result.put("session-id", req.getSession().getId());
|
||||
result.put("protocol", req.getProtocol());
|
||||
result.put("method", req.getMethod());
|
||||
result.put("scheme", req.getScheme());
|
||||
result.put("remote-addr", req.getRemoteAddr());
|
||||
return result;
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/appinfo")
|
||||
public ApplicationInfo info() {
|
||||
return new ApplicationInfo(springEnvironment.getActiveProfiles(), getServiceNames());
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/service")
|
||||
public List<CfService> showServiceInfo() {
|
||||
return cfEnv.findAllServices();
|
||||
}
|
||||
|
||||
private String[] getServiceNames() {
|
||||
List<CfService> services = cfEnv.findAllServices();
|
||||
|
||||
List<String> names = new ArrayList<>();
|
||||
for (CfService service : services) {
|
||||
names.add(service.getName());
|
||||
}
|
||||
return names.toArray(new String[0]);
|
||||
}
|
||||
}
|
||||
205
src/main/resources/albums.json
Normal file
205
src/main/resources/albums.json
Normal file
@@ -0,0 +1,205 @@
|
||||
[
|
||||
{
|
||||
"_class": "org.cloudfoundry.samples.music.domain.Album",
|
||||
"artist": "Nirvana",
|
||||
"title": "Nevermind",
|
||||
"releaseYear": "1991",
|
||||
"genre": "Rock"
|
||||
},
|
||||
{
|
||||
"_class": "org.cloudfoundry.samples.music.domain.Album",
|
||||
"artist": "The Beach Boys",
|
||||
"title": "Pet Sounds",
|
||||
"releaseYear": "1966",
|
||||
"genre": "Rock"
|
||||
},
|
||||
{
|
||||
"_class": "org.cloudfoundry.samples.music.domain.Album",
|
||||
"artist": "Marvin Gaye",
|
||||
"title": "What's Going On",
|
||||
"releaseYear": "1971",
|
||||
"genre": "Rock"
|
||||
},
|
||||
{
|
||||
"_class": "org.cloudfoundry.samples.music.domain.Album",
|
||||
"artist": "Jimi Hendrix Experience",
|
||||
"title": "Are You Experienced?",
|
||||
"releaseYear": "1967",
|
||||
"genre": "Rock"
|
||||
},
|
||||
{
|
||||
"_class": "org.cloudfoundry.samples.music.domain.Album",
|
||||
"artist": "U2",
|
||||
"title": "The Joshua Tree",
|
||||
"releaseYear": "1987",
|
||||
"genre": "Rock"
|
||||
},
|
||||
{
|
||||
"_class": "org.cloudfoundry.samples.music.domain.Album",
|
||||
"artist": "The Beatles",
|
||||
"title": "Abbey Road",
|
||||
"releaseYear": "1969",
|
||||
"genre": "Rock"
|
||||
},
|
||||
{
|
||||
"_class": "org.cloudfoundry.samples.music.domain.Album",
|
||||
"artist": "Fleetwood Mac",
|
||||
"title": "Rumours",
|
||||
"releaseYear": "1977",
|
||||
"genre": "Rock"
|
||||
},
|
||||
{
|
||||
"_class": "org.cloudfoundry.samples.music.domain.Album",
|
||||
"artist": "Elvis Presley",
|
||||
"title": "Sun Sessions",
|
||||
"releaseYear": "1976",
|
||||
"genre": "Rock"
|
||||
},
|
||||
{
|
||||
"_class": "org.cloudfoundry.samples.music.domain.Album",
|
||||
"artist": "Michael Jackson",
|
||||
"title": "Thriller",
|
||||
"releaseYear": "1982",
|
||||
"genre": "Pop"
|
||||
},
|
||||
{
|
||||
"_class": "org.cloudfoundry.samples.music.domain.Album",
|
||||
"artist": "The Rolling Stones",
|
||||
"title": "Exile on Main Street",
|
||||
"releaseYear": "1972",
|
||||
"genre": "Rock"
|
||||
},
|
||||
{
|
||||
"_class": "org.cloudfoundry.samples.music.domain.Album",
|
||||
"artist": "Bruce Springsteen",
|
||||
"title": "Born to Run",
|
||||
"releaseYear": "1975",
|
||||
"genre": "Rock"
|
||||
},
|
||||
{
|
||||
"_class": "org.cloudfoundry.samples.music.domain.Album",
|
||||
"artist": "The Clash",
|
||||
"title": "London Calling",
|
||||
"releaseYear": "1980",
|
||||
"genre": "Rock"
|
||||
},
|
||||
{
|
||||
"_class": "org.cloudfoundry.samples.music.domain.Album",
|
||||
"artist": "The Eagles",
|
||||
"title": "Hotel California",
|
||||
"releaseYear": "1976",
|
||||
"genre": "Rock"
|
||||
},
|
||||
{
|
||||
"_class": "org.cloudfoundry.samples.music.domain.Album",
|
||||
"artist": "Led Zeppelin",
|
||||
"title": "Led Zeppelin",
|
||||
"releaseYear": "1969",
|
||||
"genre": "Rock"
|
||||
},
|
||||
{
|
||||
"_class": "org.cloudfoundry.samples.music.domain.Album",
|
||||
"artist": "Led Zeppelin",
|
||||
"title": "IV",
|
||||
"releaseYear": "1971",
|
||||
"genre": "Rock"
|
||||
},
|
||||
{
|
||||
"_class": "org.cloudfoundry.samples.music.domain.Album",
|
||||
"artist": "Police",
|
||||
"title": "Synchronicity",
|
||||
"releaseYear": "1983",
|
||||
"genre": "Rock"
|
||||
},
|
||||
{
|
||||
"_class": "org.cloudfoundry.samples.music.domain.Album",
|
||||
"artist": "U2",
|
||||
"title": "Achtung Baby",
|
||||
"releaseYear": "1991",
|
||||
"genre": "Rock"
|
||||
},
|
||||
{
|
||||
"_class": "org.cloudfoundry.samples.music.domain.Album",
|
||||
"artist": "The Rolling Stones",
|
||||
"title": "Let it Bleed",
|
||||
"releaseYear": "1969",
|
||||
"genre": "Rock"
|
||||
},
|
||||
{
|
||||
"_class": "org.cloudfoundry.samples.music.domain.Album",
|
||||
"artist": "The Beatles",
|
||||
"title": "Rubber Soul",
|
||||
"releaseYear": "1965",
|
||||
"genre": "Rock"
|
||||
},
|
||||
{
|
||||
"_class": "org.cloudfoundry.samples.music.domain.Album",
|
||||
"artist": "The Ramones",
|
||||
"title": "The Ramones",
|
||||
"releaseYear": "1976",
|
||||
"genre": "Rock"
|
||||
},
|
||||
{
|
||||
"_class": "org.cloudfoundry.samples.music.domain.Album",
|
||||
"artist": "Queen",
|
||||
"title": "A Night At The Opera",
|
||||
"releaseYear": "1975",
|
||||
"genre": "Rock"
|
||||
},
|
||||
{
|
||||
"_class": "org.cloudfoundry.samples.music.domain.Album",
|
||||
"artist": "Boston",
|
||||
"title": "Don't Look Back",
|
||||
"releaseYear": "1978",
|
||||
"genre": "Rock"
|
||||
},
|
||||
{
|
||||
"_class": "org.cloudfoundry.samples.music.domain.Album",
|
||||
"artist": "BB King",
|
||||
"title": "Singin' The Blues",
|
||||
"releaseYear": "1956",
|
||||
"genre": "Blues"
|
||||
},
|
||||
{
|
||||
"_class": "org.cloudfoundry.samples.music.domain.Album",
|
||||
"artist": "Albert King",
|
||||
"title": "Born Under A Bad Sign",
|
||||
"releaseYear": "1967",
|
||||
"genre": "Blues"
|
||||
},
|
||||
{
|
||||
"_class": "org.cloudfoundry.samples.music.domain.Album",
|
||||
"artist": "Muddy Waters",
|
||||
"title": "Folk Singer",
|
||||
"releaseYear": "1964",
|
||||
"genre": "Blues"
|
||||
},
|
||||
{
|
||||
"_class": "org.cloudfoundry.samples.music.domain.Album",
|
||||
"artist": "The Fabulous Thunderbirds",
|
||||
"title": "Rock With Me",
|
||||
"releaseYear": "1979",
|
||||
"genre": "Blues"
|
||||
},
|
||||
{
|
||||
"_class": "org.cloudfoundry.samples.music.domain.Album",
|
||||
"artist": "Robert Johnson",
|
||||
"title": "King of the Delta Blues",
|
||||
"releaseYear": "1961",
|
||||
"genre": "Blues"
|
||||
},
|
||||
{
|
||||
"_class": "org.cloudfoundry.samples.music.domain.Album",
|
||||
"artist": "Stevie Ray Vaughan",
|
||||
"title": "Texas Flood",
|
||||
"releaseYear": "1983",
|
||||
"genre": "Blues"
|
||||
},
|
||||
{
|
||||
"_class": "org.cloudfoundry.samples.music.domain.Album",
|
||||
"artist": "Stevie Ray Vaughan",
|
||||
"title": "Couldn't Stand The Weather",
|
||||
"releaseYear": "1984",
|
||||
"genre": "Blues"
|
||||
}
|
||||
]
|
||||
52
src/main/resources/application.yml
Normal file
52
src/main/resources/application.yml
Normal file
@@ -0,0 +1,52 @@
|
||||
spring:
|
||||
jpa:
|
||||
generate-ddl: true
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: "*"
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: http2
|
||||
|
||||
server:
|
||||
http2:
|
||||
enabled: true
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: mysql
|
||||
datasource:
|
||||
url: "jdbc:mysql://localhost/music"
|
||||
driver-class-name: com.mysql.jdbc.Driver
|
||||
username:
|
||||
password:
|
||||
jpa:
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.MySQL55Dialect
|
||||
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: postgres
|
||||
datasource:
|
||||
url: "jdbc:postgresql://localhost/music"
|
||||
driver-class-name: org.postgresql.Driver
|
||||
username: postgres
|
||||
password:
|
||||
jpa:
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.ProgressDialect
|
||||
35
src/main/resources/static/css/app.css
Normal file
35
src/main/resources/static/css/app.css
Normal file
@@ -0,0 +1,35 @@
|
||||
#body {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background-color: white;
|
||||
border: 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.navbar .container {
|
||||
background-color: #008a00;
|
||||
background-image: -moz-linear-gradient(top, #008a00, #006b00);
|
||||
background-image: -ms-linear-gradient(top, #008a00, #006b00);
|
||||
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#008a00), to(#006b00));
|
||||
background-image: -webkit-linear-gradient(top, #008a00, #006b00);
|
||||
background-image: -o-linear-gradient(top, #008a00, #006b00);
|
||||
background-image: linear-gradient(top, #008a00, #006b00);
|
||||
}
|
||||
|
||||
.navbar .navbar-brand {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.navbar .navbar-brand:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.icon-white {
|
||||
color: white;
|
||||
}
|
||||
59
src/main/resources/static/css/multi-columns-row.css
Normal file
59
src/main/resources/static/css/multi-columns-row.css
Normal file
@@ -0,0 +1,59 @@
|
||||
/* From https://github.com/sixfootsixdesigns/Bootstrap-3-Grid-Columns-Clearing */
|
||||
|
||||
/* clear first in row in ie 8 or lower */
|
||||
.multi-columns-row .first-in-row {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
/* clear the first in row for any block that has the class "multi-columns-row" */
|
||||
.multi-columns-row .col-xs-6:nth-child(2n + 3) { clear: left; }
|
||||
.multi-columns-row .col-xs-4:nth-child(3n + 4) { clear: left; }
|
||||
.multi-columns-row .col-xs-3:nth-child(4n + 5) { clear: left; }
|
||||
.multi-columns-row .col-xs-2:nth-child(6n + 7) { clear: left; }
|
||||
.multi-columns-row .col-xs-1:nth-child(12n + 13) { clear: left; }
|
||||
|
||||
@media (min-width: 768px) {
|
||||
/* reset previous grid */
|
||||
.multi-columns-row .col-xs-6:nth-child(2n + 3) { clear: none; }
|
||||
.multi-columns-row .col-xs-4:nth-child(3n + 4) { clear: none; }
|
||||
.multi-columns-row .col-xs-3:nth-child(4n + 5) { clear: none; }
|
||||
.multi-columns-row .col-xs-2:nth-child(6n + 7) { clear: none; }
|
||||
.multi-columns-row .col-xs-1:nth-child(12n + 13) { clear: none; }
|
||||
|
||||
/* clear first in row for small columns */
|
||||
.multi-columns-row .col-sm-6:nth-child(2n + 3) { clear: left; }
|
||||
.multi-columns-row .col-sm-4:nth-child(3n + 4) { clear: left; }
|
||||
.multi-columns-row .col-sm-3:nth-child(4n + 5) { clear: left; }
|
||||
.multi-columns-row .col-sm-2:nth-child(6n + 7) { clear: left; }
|
||||
.multi-columns-row .col-sm-1:nth-child(12n + 13) { clear: left; }
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
/* reset previous grid */
|
||||
.multi-columns-row .col-sm-6:nth-child(2n + 3) { clear: none; }
|
||||
.multi-columns-row .col-sm-4:nth-child(3n + 4) { clear: none; }
|
||||
.multi-columns-row .col-sm-3:nth-child(4n + 5) { clear: none; }
|
||||
.multi-columns-row .col-sm-2:nth-child(6n + 7) { clear: none; }
|
||||
.multi-columns-row .col-sm-1:nth-child(12n + 13) { clear: none; }
|
||||
|
||||
/* clear first in row for medium columns */
|
||||
.multi-columns-row .col-md-6:nth-child(2n + 3) { clear: left; }
|
||||
.multi-columns-row .col-md-4:nth-child(3n + 4) { clear: left; }
|
||||
.multi-columns-row .col-md-3:nth-child(4n + 5) { clear: left; }
|
||||
.multi-columns-row .col-md-2:nth-child(6n + 7) { clear: left; }
|
||||
.multi-columns-row .col-md-1:nth-child(12n + 13) { clear: left; }
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
/* reset previous grid */
|
||||
.multi-columns-row .col-md-6:nth-child(2n + 3) { clear: none; }
|
||||
.multi-columns-row .col-md-4:nth-child(3n + 4) { clear: none; }
|
||||
.multi-columns-row .col-md-3:nth-child(4n + 5) { clear: none; }
|
||||
.multi-columns-row .col-md-2:nth-child(6n + 7) { clear: none; }
|
||||
.multi-columns-row .col-md-1:nth-child(12n + 13) { clear: none; }
|
||||
|
||||
/* clear first in row for large columns */
|
||||
.multi-columns-row .col-lg-6:nth-child(2n + 3) { clear: left; }
|
||||
.multi-columns-row .col-lg-4:nth-child(3n + 4) { clear: left; }
|
||||
.multi-columns-row .col-lg-3:nth-child(4n + 5) { clear: left; }
|
||||
.multi-columns-row .col-lg-2:nth-child(6n + 7) { clear: left; }
|
||||
.multi-columns-row .col-lg-1:nth-child(12n + 13) { clear: left; }
|
||||
}
|
||||
BIN
src/main/resources/static/img/glyphicons-halflings-white.png
Normal file
BIN
src/main/resources/static/img/glyphicons-halflings-white.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.6 KiB |
BIN
src/main/resources/static/img/glyphicons-halflings.png
Normal file
BIN
src/main/resources/static/img/glyphicons-halflings.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
47
src/main/resources/static/index.html
Normal file
47
src/main/resources/static/index.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" class="en" ng-app="SpringMusic">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<meta name="description" content="Spring Music">
|
||||
<meta name="title" content="Spring Music">
|
||||
<link rel="shortcut icon" href="favicon.ico">
|
||||
<title>Spring Music</title>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="webjars/bootstrap/3.1.1/css/bootstrap.css">
|
||||
<link rel="stylesheet" type="text/css" href="css/app.css">
|
||||
<link rel="stylesheet" type="text/css" href="css/multi-columns-row.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div ng-include="'templates/header.html'"></div>
|
||||
</div>
|
||||
|
||||
<div id="body" class="row">
|
||||
<ng-view></ng-view>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div ng-include="'templates/footer.html'"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" src="webjars/jquery/2.1.0/jquery.min.js"></script>
|
||||
|
||||
<script type="text/javascript" src="webjars/angularjs/1.2.16/angular.js"></script>
|
||||
<script type="text/javascript" src="webjars/angularjs/1.2.16/angular-resource.js"></script>
|
||||
<script type="text/javascript" src="webjars/angularjs/1.2.16/angular-route.js"></script>
|
||||
<script type="text/javascript" src="webjars/angular-ui/0.4.0/angular-ui.js"></script>
|
||||
<script type="text/javascript" src="webjars/angular-ui-bootstrap/0.10.0/ui-bootstrap.js"></script>
|
||||
<script type="text/javascript" src="webjars/angular-ui-bootstrap/0.10.0/ui-bootstrap-tpls.js"></script>
|
||||
|
||||
<script type="text/javascript" src="webjars/bootstrap/3.1.1/js/bootstrap.js"></script>
|
||||
|
||||
<script type="text/javascript" src="js/app.js"></script>
|
||||
<script type="text/javascript" src="js/albums.js"></script>
|
||||
<script type="text/javascript" src="js/errors.js"></script>
|
||||
<script type="text/javascript" src="js/info.js"></script>
|
||||
<script type="text/javascript" src="js/status.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
199
src/main/resources/static/js/albums.js
Normal file
199
src/main/resources/static/js/albums.js
Normal file
@@ -0,0 +1,199 @@
|
||||
angular.module('albums', ['ngResource', 'ui.bootstrap']).
|
||||
factory('Albums', function ($resource) {
|
||||
return $resource('albums');
|
||||
}).
|
||||
factory('Album', function ($resource) {
|
||||
return $resource('albums/:id', {id: '@id'});
|
||||
}).
|
||||
factory("EditorStatus", function () {
|
||||
var editorEnabled = {};
|
||||
|
||||
var enable = function (id, fieldName) {
|
||||
editorEnabled = { 'id': id, 'fieldName': fieldName };
|
||||
};
|
||||
|
||||
var disable = function () {
|
||||
editorEnabled = {};
|
||||
};
|
||||
|
||||
var isEnabled = function(id, fieldName) {
|
||||
return (editorEnabled['id'] == id && editorEnabled['fieldName'] == fieldName);
|
||||
};
|
||||
|
||||
return {
|
||||
isEnabled: isEnabled,
|
||||
enable: enable,
|
||||
disable: disable
|
||||
}
|
||||
});
|
||||
|
||||
function AlbumsController($scope, $modal, Albums, Album, Status) {
|
||||
function list() {
|
||||
$scope.albums = Albums.query();
|
||||
}
|
||||
|
||||
function clone (obj) {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
function saveAlbum(album) {
|
||||
Albums.save(album,
|
||||
function () {
|
||||
Status.success("Album saved");
|
||||
list();
|
||||
},
|
||||
function (result) {
|
||||
Status.error("Error saving album: " + result.status);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
$scope.addAlbum = function () {
|
||||
var addModal = $modal.open({
|
||||
templateUrl: 'templates/albumForm.html',
|
||||
controller: AlbumModalController,
|
||||
resolve: {
|
||||
album: function () {
|
||||
return {};
|
||||
},
|
||||
action: function() {
|
||||
return 'add';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
addModal.result.then(function (album) {
|
||||
saveAlbum(album);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.updateAlbum = function (album) {
|
||||
var updateModal = $modal.open({
|
||||
templateUrl: 'templates/albumForm.html',
|
||||
controller: AlbumModalController,
|
||||
resolve: {
|
||||
album: function() {
|
||||
return clone(album);
|
||||
},
|
||||
action: function() {
|
||||
return 'update';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
updateModal.result.then(function (album) {
|
||||
saveAlbum(album);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deleteAlbum = function (album) {
|
||||
Album.delete({id: album.id},
|
||||
function () {
|
||||
Status.success("Album deleted");
|
||||
list();
|
||||
},
|
||||
function (result) {
|
||||
Status.error("Error deleting album: " + result.status);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.setAlbumsView = function (viewName) {
|
||||
$scope.albumsView = "templates/" + viewName + ".html";
|
||||
};
|
||||
|
||||
$scope.init = function() {
|
||||
list();
|
||||
$scope.setAlbumsView("grid");
|
||||
$scope.sortField = "name";
|
||||
$scope.sortDescending = false;
|
||||
};
|
||||
}
|
||||
|
||||
function AlbumModalController($scope, $modalInstance, album, action) {
|
||||
$scope.albumAction = action;
|
||||
$scope.yearPattern = /^[1-2]\d{3}$/;
|
||||
$scope.album = album;
|
||||
|
||||
$scope.ok = function () {
|
||||
$modalInstance.close($scope.album);
|
||||
};
|
||||
|
||||
$scope.cancel = function () {
|
||||
$modalInstance.dismiss('cancel');
|
||||
};
|
||||
};
|
||||
|
||||
function AlbumEditorController($scope, Albums, Status, EditorStatus) {
|
||||
$scope.enableEditor = function (album, fieldName) {
|
||||
$scope.newFieldValue = album[fieldName];
|
||||
EditorStatus.enable(album.id, fieldName);
|
||||
};
|
||||
|
||||
$scope.disableEditor = function () {
|
||||
EditorStatus.disable();
|
||||
};
|
||||
|
||||
$scope.isEditorEnabled = function (album, fieldName) {
|
||||
return EditorStatus.isEnabled(album.id, fieldName);
|
||||
};
|
||||
|
||||
$scope.save = function (album, fieldName) {
|
||||
if ($scope.newFieldValue === "") {
|
||||
return false;
|
||||
}
|
||||
|
||||
album[fieldName] = $scope.newFieldValue;
|
||||
|
||||
Albums.save({}, album,
|
||||
function () {
|
||||
Status.success("Album saved");
|
||||
list();
|
||||
},
|
||||
function (result) {
|
||||
Status.error("Error saving album: " + result.status);
|
||||
}
|
||||
);
|
||||
|
||||
$scope.disableEditor();
|
||||
};
|
||||
|
||||
$scope.disableEditor();
|
||||
}
|
||||
|
||||
angular.module('albums').
|
||||
directive('inPlaceEdit', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
transclude: true,
|
||||
replace: true,
|
||||
|
||||
scope: {
|
||||
ipeFieldName: '@fieldName',
|
||||
ipeInputType: '@inputType',
|
||||
ipeInputClass: '@inputClass',
|
||||
ipePattern: '@pattern',
|
||||
ipeModel: '=model'
|
||||
},
|
||||
|
||||
template:
|
||||
'<div>' +
|
||||
'<span ng-hide="isEditorEnabled(ipeModel, ipeFieldName)" ng-click="enableEditor(ipeModel, ipeFieldName)">' +
|
||||
'<span ng-transclude></span>' +
|
||||
'</span>' +
|
||||
'<span ng-show="isEditorEnabled(ipeModel, ipeFieldName)">' +
|
||||
'<div class="input-append">' +
|
||||
'<input type="{{ipeInputType}}" name="{{ipeFieldName}}" class="{{ipeInputClass}}" ' +
|
||||
'ng-required ng-pattern="{{ipePattern}}" ng-model="newFieldValue" ' +
|
||||
'ui-keyup="{enter: \'save(ipeModel, ipeFieldName)\', esc: \'disableEditor()\'}"/>' +
|
||||
'<div class="btn-group btn-group-xs" role="toolbar">' +
|
||||
'<button ng-click="save(ipeModel, ipeFieldName)" type="button" class="btn"><span class="glyphicon glyphicon-ok"></span></button>' +
|
||||
'<button ng-click="disableEditor()" type="button" class="btn"><span class="glyphicon glyphicon-remove"></span></button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</span>' +
|
||||
'</div>',
|
||||
|
||||
controller: 'AlbumEditorController'
|
||||
};
|
||||
});
|
||||
14
src/main/resources/static/js/app.js
Normal file
14
src/main/resources/static/js/app.js
Normal file
@@ -0,0 +1,14 @@
|
||||
angular.module('SpringMusic', ['albums', 'errors', 'status', 'info', 'ngRoute', 'ui.directives']).
|
||||
config(function ($locationProvider, $routeProvider) {
|
||||
// $locationProvider.html5Mode(true);
|
||||
|
||||
$routeProvider.when('/errors', {
|
||||
controller: 'ErrorsController',
|
||||
templateUrl: 'templates/errors.html'
|
||||
});
|
||||
$routeProvider.otherwise({
|
||||
controller: 'AlbumsController',
|
||||
templateUrl: 'templates/albums.html'
|
||||
});
|
||||
}
|
||||
);
|
||||
37
src/main/resources/static/js/errors.js
Normal file
37
src/main/resources/static/js/errors.js
Normal file
@@ -0,0 +1,37 @@
|
||||
angular.module('errors', ['ngResource']).
|
||||
factory('Errors', function ($resource) {
|
||||
return $resource('errors', {}, {
|
||||
kill: { url: 'errors/kill' },
|
||||
throw: { url: 'errors/throw' }
|
||||
});
|
||||
});
|
||||
|
||||
function ErrorsController($scope, Errors, Status) {
|
||||
$scope.kill = function() {
|
||||
Errors.kill({},
|
||||
function () {
|
||||
Status.error("The application should have been killed, but returned successfully instead.");
|
||||
},
|
||||
function (result) {
|
||||
if (result.status === 502)
|
||||
Status.error("An error occurred as expected, the application backend was killed: " + result.status);
|
||||
else
|
||||
Status.error("An unexpected error occurred: " + result.status);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.throwException = function() {
|
||||
Errors.throw({},
|
||||
function () {
|
||||
Status.error("An exception should have been thrown, but was not.");
|
||||
},
|
||||
function (result) {
|
||||
if (result.status === 500)
|
||||
Status.error("An error occurred as expected: " + result.status);
|
||||
else
|
||||
Status.error("An unexpected error occurred: " + result.status);
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
8
src/main/resources/static/js/info.js
Normal file
8
src/main/resources/static/js/info.js
Normal file
@@ -0,0 +1,8 @@
|
||||
angular.module('info', ['ngResource']).
|
||||
factory('Info', function ($resource) {
|
||||
return $resource('appinfo');
|
||||
});
|
||||
|
||||
function InfoController($scope, Info) {
|
||||
$scope.info = Info.get();
|
||||
}
|
||||
38
src/main/resources/static/js/status.js
Normal file
38
src/main/resources/static/js/status.js
Normal file
@@ -0,0 +1,38 @@
|
||||
angular.module('status', []).
|
||||
factory("Status", function () {
|
||||
var status = null;
|
||||
|
||||
var success = function (message) {
|
||||
this.status = { isError: false, message: message };
|
||||
};
|
||||
|
||||
var error = function (message) {
|
||||
this.status = { isError: true, message: message };
|
||||
};
|
||||
|
||||
var clear = function () {
|
||||
this.status = null;
|
||||
};
|
||||
|
||||
return {
|
||||
status: status,
|
||||
success: success,
|
||||
error: error,
|
||||
clear: clear
|
||||
}
|
||||
});
|
||||
|
||||
function StatusController($scope, Status) {
|
||||
$scope.$watch(
|
||||
function () {
|
||||
return Status.status;
|
||||
},
|
||||
function (status) {
|
||||
$scope.status = status;
|
||||
},
|
||||
true);
|
||||
|
||||
$scope.clearStatus = function () {
|
||||
Status.clear();
|
||||
};
|
||||
}
|
||||
32
src/main/resources/static/templates/albumForm.html
Normal file
32
src/main/resources/static/templates/albumForm.html
Normal file
@@ -0,0 +1,32 @@
|
||||
<div class="modal-header">
|
||||
<h3 ng-show="albumAction === 'add'">Add an album</h3>
|
||||
<h3 ng-show="albumAction === 'update'">Edit an album</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form name="albumForm" role="form">
|
||||
<div class="form-group has-feedback" ng-class="{'has-warning': albumForm.title.$invalid, 'has-success': albumForm.title.$valid}">
|
||||
<label for="title">Album Title</label>
|
||||
<input type="text" class="form-control input-large" id="title" name="title" required ng-model="album.title">
|
||||
<span class="glyphicon form-control-feedback" ng-class="{'glyphicon-warning-sign': albumForm.title.$invalid, 'glyphicon-ok': albumForm.title.$valid}"></span>
|
||||
</div>
|
||||
<div class="form-group has-feedback" ng-class="{'has-warning': albumForm.artist.$invalid, 'has-success': albumForm.artist.$valid}">
|
||||
<label for="artist">Artist</label>
|
||||
<input type="text" class="form-control input-large" id="artist" name="artist" required ng-model="album.artist">
|
||||
<span class="glyphicon form-control-feedback" ng-class="{'glyphicon-warning-sign': albumForm.artist.$invalid, 'glyphicon-ok': albumForm.artist.$valid}"></span>
|
||||
</div>
|
||||
<div class="form-group has-feedback" ng-class="{'has-warning': albumForm.releaseYear.$invalid, 'has-success': albumForm.releaseYear.$valid}">
|
||||
<label for="releaseYear">Release Year</label>
|
||||
<input type="text" class="form-control input-small" id="releaseYear" name="releaseYear" required ng-model="album.releaseYear" ng-pattern=yearPattern>
|
||||
<span class="glyphicon form-control-feedback" ng-class="{'glyphicon-warning-sign': albumForm.releaseYear.$invalid, 'glyphicon-ok': albumForm.releaseYear.$valid}"></span>
|
||||
</div>
|
||||
<div class="form-group has-feedback" ng-class="{'has-warning': albumForm.genre.$invalid, 'has-success': albumForm.genre.$valid}">
|
||||
<label for="genre">Genre</label>
|
||||
<input type="text" class="form-control input-medium" id=genre name="genre" required ng-model="album.genre">
|
||||
<span class="glyphicon form-control-feedback" ng-class="{'glyphicon-warning-sign': albumForm.genre.$invalid, 'glyphicon-ok': albumForm.genre.$valid}"></span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary btn-md active" ng-disabled="albumForm.$invalid" ng-click="ok()" >OK</button>
|
||||
<button class="btn btn-warning btn-md active" ng-click="cancel()">Cancel</button>
|
||||
</div>
|
||||
29
src/main/resources/static/templates/albums.html
Normal file
29
src/main/resources/static/templates/albums.html
Normal file
@@ -0,0 +1,29 @@
|
||||
<div id="albums" ng-init="init()">
|
||||
<div class="page-header">
|
||||
<h1>Albums</h1>
|
||||
<div>
|
||||
<span> [ view as: </span>
|
||||
<a href="" ng-click="setAlbumsView('grid')"><span class="glyphicon glyphicon-th"></span></a>
|
||||
<a href="" ng-click="setAlbumsView('list')"><span class="glyphicon glyphicon-th-list"></span></a>
|
||||
<span> | sort by: </span>
|
||||
<a href="" ng-click="sortField='title'">title</a>
|
||||
<a href="" ng-click="sortField='artist'">artist</a>
|
||||
<a href="" ng-click="sortField='releaseYear'">year</a>
|
||||
<a href="" ng-click="sortField='genre'">genre</a>
|
||||
<a href="" ng-click="sortDescending=!sortDescending">
|
||||
<span class="glyphicon" ng-class="{'glyphicon-chevron-up' : !sortDescending, 'glyphicon-chevron-down' : sortDescending}"></span>
|
||||
</a>
|
||||
<span> | </span>
|
||||
<a href="" ng-click="addAlbum()"><span class="glyphicon glyphicon-plus"></span>add an album</a>
|
||||
<span> ] </span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div ng-include src="'templates/status.html'"></div>
|
||||
</div>
|
||||
|
||||
<div class="row multi-columns-row">
|
||||
<div ng-include src="albumsView"></div>
|
||||
</div>
|
||||
</div>
|
||||
22
src/main/resources/static/templates/errors.html
Normal file
22
src/main/resources/static/templates/errors.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<div id="errors" class="col-xs-12" ng-controller="ErrorsController">
|
||||
<div class="page-header">
|
||||
<h1>Force Errors</h1>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div ng-include src="'templates/status.html'"></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<form role="form">
|
||||
<div class="form-group">
|
||||
<h3>Kill this instance of the application</h3>
|
||||
<a ng-click="kill()" class="btn btn-primary btn-lg active" role="button">Kill</a>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<h3>Force an exception to be thrown from the application</h3>
|
||||
<a ng-click="throwException()" class="btn btn-primary btn-lg active" role="button">Throw Exception</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
3
src/main/resources/static/templates/footer.html
Normal file
3
src/main/resources/static/templates/footer.html
Normal file
@@ -0,0 +1,3 @@
|
||||
<div id="footer" class="row">
|
||||
|
||||
</div>
|
||||
31
src/main/resources/static/templates/grid.html
Normal file
31
src/main/resources/static/templates/grid.html
Normal file
@@ -0,0 +1,31 @@
|
||||
<div class="col-xs-6 col-sm-3 col-md-3 col-lg-3" ng-repeat="album in albums | orderBy:sortField:sortDescending">
|
||||
<div class="thumbnail">
|
||||
<div class="caption">
|
||||
<h4>
|
||||
<in-place-edit field-name='title' input-type='text' input-class='input-medium' model=album>{{album.title}}</in-place-edit>
|
||||
</h4>
|
||||
|
||||
<h4>
|
||||
<in-place-edit field-name='artist' input-type='text' input-class='input-medium' model=album>{{album.artist}}</in-place-edit>
|
||||
</h4>
|
||||
|
||||
<h5>
|
||||
<in-place-edit field-name='releaseYear' input-type='text' input-class='input-small' model=album>{{album.releaseYear}}</in-place-edit>
|
||||
</h5>
|
||||
|
||||
<h5>
|
||||
<in-place-edit field-name='genre' input-type='text' input-class='input-small' model=album>{{album.genre}}</in-place-edit>
|
||||
</h5>
|
||||
|
||||
<div class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
||||
<span class="glyphicon glyphicon-cog"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu" role="menu" aria-labelledby="dLabel">
|
||||
<li role="presentation"><a ng-click="updateAlbum(album)">edit</a></li>
|
||||
<li role="presentation"><a ng-click="deleteAlbum(album)">delete</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
24
src/main/resources/static/templates/header.html
Normal file
24
src/main/resources/static/templates/header.html
Normal file
@@ -0,0 +1,24 @@
|
||||
<div id="header" ng-controller="InfoController" class="col-xs-12">
|
||||
<nav class="navbar navbar-default navbar-fixed-top" role="navigation">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<a class="navbar-brand" href="#">Spring Music <span class="glyphicon glyphicon-music"></span></a>
|
||||
<ul class="navbar-nav">
|
||||
<!--<li></li>-->
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
||||
<span class="glyphicon glyphicon-info-sign icon-white"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a><strong>Profiles:</strong> {{info.profiles.join()}}</a></li>
|
||||
<li><a><strong>Services:</strong> {{info.services.join()}}</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
38
src/main/resources/static/templates/list.html
Normal file
38
src/main/resources/static/templates/list.html
Normal file
@@ -0,0 +1,38 @@
|
||||
<div class="col-xs-12">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Album Title</th>
|
||||
<th>Artist</th>
|
||||
<th>Year</th>
|
||||
<th>Genre</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="album in albums | orderBy:sortField:sortDescending">
|
||||
<td>
|
||||
<in-place-edit field-name='title' input-type='text' input-class='input-large' model=album>{{album.title}}</in-place-edit>
|
||||
</td>
|
||||
<td>
|
||||
<in-place-edit field-name='artist' input-type='artist' input-class='input-large' model=album>{{album.artist}}</in-place-edit>
|
||||
</td>
|
||||
<td>
|
||||
<in-place-edit field-name='releaseYear' input-type='text' input-class='input-small' model=album>{{album.releaseYear}}</in-place-edit>
|
||||
</td>
|
||||
<td>
|
||||
<in-place-edit field-name='genre' input-type='text' input-class='input-medium' model=album>{{album.genre}}</in-place-edit>
|
||||
</td>
|
||||
<td class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
||||
<span class="glyphicon glyphicon-cog"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu" role="menu" aria-labelledby="dLabel">
|
||||
<li role="presentation"><a ng-click="updateAlbum(album)">edit</a></li>
|
||||
<li role="presentation"><a ng-click="deleteAlbum(album)">delete</a></li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
8
src/main/resources/static/templates/status.html
Normal file
8
src/main/resources/static/templates/status.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<div class="col-xs-12" id="status" ng-controller="StatusController">
|
||||
<div id="alert" ng-show="status">
|
||||
<div class="alert" ng-class="{'alert-success': !status.isError, 'alert-danger': status.isError}">
|
||||
<button type="button" class="close" ng-click="clearStatus()"><span class="glyphicon glyphicon-remove-circle"></span></button>
|
||||
<p>{{status.message}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.cloudfoundry.samples.music;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
|
||||
@RunWith(SpringRunner.class)
|
||||
@SpringBootTest()
|
||||
public class ApplicationTests {
|
||||
|
||||
@Test
|
||||
public void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user