I'm new to Weld and have been trying to get my head around it's concepts. I have a little experience with Spring and nothing with Guice, so I'm pretty much a novice with the DI frameworks.
Here's a tutorial that introduce CDI, but in the context of web apps. I'm interested to see how this works in Java SE alone. I have created the following classes, but have no idea how to test the ItemProcessor's execute method with the DefaultItemDao class (or any other alternative) in a Java SE app.
Here're the classes:
public class Item {
private int value;
private int limit;
public Item(int v, int l) {
value = v;
limit = l;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
public int getLimit() {
return limit;
}
public void setLimit(int limit) {
this.limit = limit;
}
#Override
public String toString() {
return "Item [value=" + value + ", limit=" + limit + "]";
}
}
import java.util.List;
public interface ItemDao {
List<Item> fetchItems();
}
import java.util.ArrayList;
import java.util.List;
public class DefaultItemDao implements ItemDao {
#Override
public List<Item> fetchItems() {
List<Item> results = new ArrayList<Item>(){{
add(new Item(1,2));
add(new Item(2,3));
}};
return results;
}
}
import java.util.List;
import javax.inject.Inject;
public class ItemProcessor {
#Inject
private ItemDao itemDao;
public void execute() {
List<Item> items = itemDao.fetchItems();
for (Item item : items) {
System.out.println("Found item: "+item);
}
}
}
And I have no idea how to write a test client for the ItemProcessor class. Can someone help me understand how to write one with CDI?
Thanks, Kumar
I had same question with injection Validator using JavaSE. Finally I managed to solve it. Hope it helps someone!
Dependencies i used:
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.0.Alpha2</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator-cdi</artifactId>
<version>6.0.0.Alpha2</version>
</dependency>
<dependency>
<groupId>org.jboss.weld.se</groupId>
<artifactId>weld-se</artifactId>
<version>2.4.3.Final</version>
</dependency>
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>javax.el</artifactId>
<version>2.2.6</version>
</dependency>
Main method:
Weld weld = new Weld().interceptors(Validator.class);
WeldContainer container = weld.initialize();
PurchaseOrderService service =
container.select(ru.code.service.PurchaseOrderService.class).get();
Customer customer = new Customer(.....);
service.createCustomer(customer);
weld.shutdown();
PurchaseOrderService.java
#Inject
private Validator validator;
private Set<ConstraintViolation<Customer>> violations;
public PurchaseOrderService() {
}
public void createCustomer(Customer customer) {
violations = validator.validate(customer);
if (violations.size() > 0) {
throw new ConstraintViolationException(violations);
}
}
And also i created beans.xml in resources/META-INF directory:
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
bean-discovery-mode="all">
</beans>
Related
I am new to GraphQL and working on a demo springboot app using GraphQL. I am looking to access the http://localhost:8080/graphql/schema.json endpoint but getting 404 whitelabel error. I am able to access one of the endpoints in the controller. Initially, I used the springboot graphql maven dependency but included another graphql maven dependency in my pom file but still got 404 error. I will greatly appreciate any help in debugging the error. Below is snippet of my code.
The app
#SpringBootApplication
public class DogGraphqlApiApplication {
public static void main(String[] args) {
SpringApplication.run(DogGraphqlApiApplication.class, args);
}
}
application.properties
spring.jpa.defer-datasource-initialization=true
spring.h2.console.enabled=true
spring.h2.console.path=/h2
spring.datasource.url=jdbc:h2:mem:dogdata
graphql.servlet.mapping=/graphql
graphql.servlet.enabled=true
graphql.servlet.corsEnabled=true
graphiql.enabled=true
graphiql.endpoint=/graphql
graphiql.mapping=graphiql
pom.xml
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.graphql</groupId>
<artifactId>spring-graphql-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-spring-boot-starter</artifactId>
<version>5.0.2</version>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java-tools</artifactId>
<version>5.2.4</version>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphiql-spring-boot-starter</artifactId>
<version>5.0.2</version>
</dependency>
dog.graphqls
type Dog {
id: ID!
name: String!
breed: String!
origin: String!
}
type Query {
findAllDogs: [Dog]!
findDogById(id:ID!): Dog!
}
type Mutation {
deleteDogBreed(breed: String!) : Boolean
updateDogName(newName: String!, id:ID!) : Dog!
}
Controller class
#RestController
public class DogController {
private DogService dogService;
#Autowired
public void setDogService(DogService dogService){
this.dogService = dogService;
}
#GetMapping("/dogs")
public ResponseEntity<List<Dog>> getAllDog(){
List<Dog> list = dogService.retrieveDogs();
return new ResponseEntity<List<Dog>>(list, HttpStatus.OK);
}
}
Query.java
#Component
public class Query implements GraphQLQueryResolver {
private DogRepository dogRepository;
public Query(DogRepository dogRepository){
this.dogRepository = dogRepository;
}
public Iterable<Dog> findAllDogs(){
return dogRepository.findAll();
}
public Dog findDogById(Long id){
Optional<Dog> optionalDog = dogRepository.findById(id);
if(optionalDog.isPresent()){
Dog dog = optionalDog.get();
return dog;
}else{
throw new DogNotFoundException("Dog Not Found", id);
}
}
}
Mutation.java
#Component
public class Mutation implements GraphQLMutationResolver {
private DogRepository dogRepository;
public Mutation(DogRepository dogRepository){
this.dogRepository = dogRepository;
}
public Boolean deleteDogBreed(String breed){
boolean deleted = false;
Iterable <Dog> dogs = dogRepository.findAll();
for(Dog dog: dogs){
if(dog.getBreed().equals( breed) ){
dogRepository.delete(dog);
deleted = true;
}
}
if(!deleted){
throw new BreedNotFoundException("Breed Not Found", breed);
}
return deleted;
}
public Dog updateDogName(String newName, Long id){
Optional<Dog> optionalDog = dogRepository.findById(id);
if(optionalDog.isPresent()){
Dog dog = optionalDog.get();
dog.setName(newName);
dogRepository.save(dog);
return dog;
}else {
throw new DogNotFoundException("Dog Not Found", id);
}
}
}
Error page
We would like to send actuator metrics to Cloudwatch. Using the provided micrometer cloudwatch MeterRegistry solutions makes to many assumptions about how our project is setup, for example you need to depend on cloud AWS which then makes even more assumptions. We would like to write a more lightweight implementation which just get a CloudWatchAsyncClient injected and makes no other assumptions about our project.
However im not sure how. Is there any example on how to make a custom implementation insted of having to depend on the available metrics registry?
So far I have done some experimenting with the following:
public interface CloudWatchConfig extends StepRegistryConfig {
int MAX_BATCH_SIZE = 20;
#Override
default String prefix() {
return "cloudwatch";
}
default String namespace() {
String v = get(prefix() + ".namespace");
if (v == null)
throw new MissingRequiredConfigurationException("namespace must be set to report metrics to CloudWatch");
return v;
}
#Override
default int batchSize() {
String v = get(prefix() + ".batchSize");
if (v == null) {
return MAX_BATCH_SIZE;
}
int vInt = Integer.parseInt(v);
if (vInt > MAX_BATCH_SIZE)
throw new InvalidConfigurationException("batchSize must be <= " + MAX_BATCH_SIZE);
return vInt;
}
}
#Service
#Log
public class CloudWatchMeterRegistry extends StepMeterRegistry {
public CloudWatchMeterRegistry(CloudWatchConfig config, Clock clock) {
super(config, clock);
}
#Override
protected void publish() {
getMeters().stream().forEach(a -> {
log.warning(a.getId().toString());
});
}
#Override
protected TimeUnit getBaseTimeUnit() {
return TimeUnit.MILLISECONDS;
}
}
#Configuration
public class MetricsPublisherConfig {
#Bean
public CloudWatchConfig cloudWatchConfig() {
return new CloudWatchConfig() {
#Override
public String get(String key) {
switch (key) {
case "cloudwatch.step":
return props.getStep();
default:
return "testtest";
}
}
};
}
}
However when I run the publish method is never called and no metrics are ever logged. What am I missing to get this working?
Here's an example project. I don't use cloudwatch myself so not had a chance to test it integrating with AWS. Leave a comment if there are any issues and we can try to resolve them
https://github.com/michaelmcfadyen/spring-boot-cloudwatch
I am trying to do something similar, and avoid using Spring Cloud. The simplest solution I have found so far is:
import io.micrometer.cloudwatch2.CloudWatchConfig;
import io.micrometer.cloudwatch2.CloudWatchMeterRegistry;
import io.micrometer.core.instrument.Clock;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.StepRegistryProperties;
import org.springframework.boot.actuate.autoconfigure.metrics.export.properties.StepRegistryPropertiesConfigAdapter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import software.amazon.awssdk.services.cloudwatch.CloudWatchAsyncClient;
#Configuration
public class MetricsConfiguration {
#Bean
public CloudWatchMeterRegistry cloudWatchMeterRegistry(CloudWatchConfig config, Clock clock) {
return new CloudWatchMeterRegistry(config, clock, CloudWatchAsyncClient.create());
}
#Component
public static class MicrometerCloudWatchConfig
extends StepRegistryPropertiesConfigAdapter<StepRegistryProperties>
implements CloudWatchConfig {
private final String namespace;
private final boolean enabled;
public MicrometerCloudWatchConfig(
#Value("${CLOUDWATCH_NAMESPACE}") String namespace,
#Value("${METRICS_ENABLED}") boolean enabled) {
super(new StepRegistryProperties() {
});
this.namespace = namespace;
this.enabled = enabled;
}
#Override
public String namespace() {
return namespace;
}
#Override
public boolean enabled() {
return enabled;
}
#Override
public int batchSize() {
return CloudWatchConfig.MAX_BATCH_SIZE;
}
}
}
Dependencies:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-cloudwatch2</artifactId>
</dependency>
Having problems to solve LiveObject raised exception, I try to reproduce problematic behavior based on Redisson test cases.
The minimal code I get to reproduce issue is this test case (mostly inspired from RedissonLiveObjectServiceTest.java):
public class LiveObjectTest {
public static final String TEST_VALUE = "my test value";
public static final Integer TEST_INTEGER = 30;
private RedissonClient redisson;
#BeforeEach
public void beforeEach () {
Config config = new Config();
config.useSingleServer()
.setAddress("http://127.0.0.1:6379");
redisson = Redisson.create(config);
}
#AfterEach
public void afterEach () {
redisson.shutdown();
}
#Test
#DisplayName("Test LiveObject with collection")
public void testLiveObjectMap () {
// Use Live Objects service
RLiveObjectService service = redisson.getLiveObjectService();
service.registerClass(TestREntityWithMap.class);
TestREntityWithMap createdObject = new TestREntityWithMap("testID2");
createdObject = service.persist(createdObject);
RMap<Integer, String> map = redisson.getMap("testMap");
createdObject.setValue(map);
map.put(TEST_INTEGER, TEST_VALUE);
TestREntityWithMap updatedObject = service.get(TestREntityWithMap.class, "testID");
// Fails here to access updatedObject.getValue()
assertEquals(TEST_VALUE, updatedObject.getValue().get(TEST_INTEGER));
}
// Tested class
#REntity
public static class TestREntityWithMap implements Comparable<TestREntityWithMap> {
#RId(generator = UUIDGenerator.class)
private String name;
private Map<Integer, String> value;
public TestREntityWithMap (String name) {
super();
this.name = name;
}
protected TestREntityWithMap () {
super();
}
public String getName () {
return name;
}
public void setName (String name) {
this.name = name;
}
public Map<Integer, String> getValue () {
return value;
}
public void setValue (Map<Integer, String> value) {
this.value = value;
}
#Override
public int compareTo (TestREntityWithMap o) {
return name.compareTo(o.name);
}
}
}
This fails converting back the RedissonMap object to an RObject...
Should not it rather try to convert value property to standard java.util.Map ?
This looks like rather simple usage of the API, am I missing some point here ?
Here is my ObjectMapper setup for JsonJackson:
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.configure(MapperFeature.DEFAULT_VIEW_INCLUSION, true);
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
mapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);
mapper.addMixIn(Throwable.class, JsonJacksonCodec.ThrowableMixIn.class);
mapper.findAndRegisterModules();
mapper.registerModule(new JavaTimeModule());
OK....
In case that helps anyone, switching from Redisson 3.8.2 to 3.9.1 solved the bug.
A few minor changes in API (RTopics, connection scheme...) but it is well worth it !
As of now latest redisson
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>
Latest Jackson jackson.version property is 2.11.2 for me
<!-- faster JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
Updating Eclipse references
eclipse:eclipse -U
I am developing a simple application using Jersey 1.x, Guice and trying to run on Tomcat.
I used Guice filter and Guice Listener along with Resources and Application.
Below is my web.xml :
<filter>
<filter-name>guiceFilter</filter-name>
<filter-class>com.google.inject.servlet.GuiceFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>guiceFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
<display-name>Guice Listener</display-name>
<listener-class>com.simple.application.GuiceListener</listener-class>
</listener>
And using GuiceListener I injected all my dependencies,
public class GuiceListener extends GuiceServletContextListener {
private Injector injector;
#Override
public void contextDestroyed(ServletContextEvent servletContextEvent) {
try {
super.contextDestroyed(servletContextEvent);
} finally {
injector = null;
}
}
#Override
public void contextInitialized(ServletContextEvent servletContextEvent) {
injector = Guice.createInjector(new SimpleServlet());
super.contextInitialized(servletContextEvent);
injector.injectMembers(this);
}
#Override
protected Injector getInjector() {
return injector;
}
}
And This is how my Servlet looks
public class SimpleServlet extends JerseyServletModule {
#Override
protected void configureServlets() {
configureGuiceServlet();
}
private void configureGuiceServlet() {
bind(SimpleResource.class).in(Scopes.SINGLETON);
serve("/service/*").with(GuiceContainer.class);
bind(Manager.class).to(ManagerImpl.class);
}
}
And I created a resource with a GET method,
#Path("/stuff")
public class SimpleResource {
private final ManagerImpl manager;
#Inject
public SimpleResource(final ManagerImpl manager) {
this.manager = manager;
}
#GET
#Path("{id}")
#Produces(MediaType.TEXT_HTML)
public String submitData(#PathParam("id") final String id) {
String welcomeScreen = manager.getWelcomeScreen();
return "This is" + welcomeScreen + id;
}
}
I used Constructor Injection for injecting classes.
And this is how my application looks
public class SimpleApplication extends Application {
#Override
public Set<Class<?>> getClasses() {
return ImmutableSet.<Class<?>>builder()
.add(SimpleResource.class)
.build();
}
}
I am deploying this on Tomcat 7 and when I try to hit the application endpoint, I am getting 404,
http://localhost:9999/simple-0.0.1-SNAPSHOT/service/stuff/id
I can see in the logs that all the classes are instantiated successfully.
These are the dependencies in my pom.
<dependency>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-server</artifactId>
<version>1.18.6</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.sun.jersey.contribs</groupId>
<artifactId>jersey-guice</artifactId>
<scope>compile</scope>
<version>1.18.6</version>
</dependency>
<dependency>
<groupId>com.google.inject.extensions</groupId>
<artifactId>guice-servlet</artifactId>
<scope>compile</scope>
<version>3.0</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<scope>provided</scope>
<version>2.5</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<scope>compile</scope>
<version>14.0</version>
</dependency>
</dependencies>
Is there anything that I am missing as far as Tomcat with guice?
You're using ServletModule. Use JerseyServletModule instead.
public class SimpleServlet extends JerseyServletModule
General Cleanup options:
When I used Guice/Tomcat/Jersey 1.x I did it a bit differently. To promote flexibility you can take the path param off SimpleResource's path param and move it to the method. Also can add Singleton Annotation to the resource directly.
#Singleton
#Path("/stuff")
public class SimpleResource {
private final ManagerImpl manager;
#Inject
public SimpleResource(final ManagerImpl manager) {
this.manager = manager;
}
#GET
#Path("{id}")
#Produces(MediaType.TEXT_HTML)
public String submitData(#PathParam("id") final String id) {
String welcomeScreen = manager.getWelcomeScreen();
return "This is" + welcomeScreen + id;
}
}
Remove SimpleApplication and in configureGuiceServlet remove reflection and options map and instead do:
public class SimpleServlet extends JerseyServletModule{
#Override
protected void configureServlets() {
configureGuiceServlet();
}
private void configureGuiceServlet() {
serve("/service/*").with(GuiceContainer.class, new HashMap<String, String>());
bind(Manager.class).to(ManagerImpl.class);
}
}
I am implementing a restful service in java using JAX-RS, and when testing the service it works only for one of my methods, when I add a new method with a different #PATH annotation the test web page is just blank without errors
My resource class
#Path("beer")
public class BeerResources {
#Context
private UriInfo context;
/**
* Creates a new instance of BeerResources
*/
public BeerResources() {
}
#GET
#Path("/costliest")
#Produces(MediaType.TEXT_PLAIN)
public String getCostliest() {
//TODO return proper representation object
return new BusinessLayer().getCostliest();
}
#GET
#Path("/cheapest")
#Produces(MediaType.APPLICATION_XML)
public String getCheapest() {
//TODO return proper representation object
return new BusinessLayer().getCheapest();
}
}
Application config class
#javax.ws.rs.ApplicationPath("/webresources")
public class ApplicationConfig extends Application {
#Override
public Set<Class<?>> getClasses() {
Set<Class<?>> resources = new java.util.HashSet<>();
addRestResourceClasses(resources);
return resources;
}
/**
* Do not modify addRestResourceClasses() method.
* It is automatically populated with
* all resources defined in the project.
* If required, comment out calling this method in getClasses().
*/
private void addRestResourceClasses(Set<Class<?>> resources) {
resources.add(beerrestful.BeerResources.class);
}
}
You can try to use Spring Boot + JAX-RS approach or Spring Boot + Spring MVC. Here are both of them on my Github page.
Also there is Spring Boot + JAX-RS source code:
Application.java:
#SpringBootApplication
public class Application extends SpringBootServletInitializer {
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
public static void main(String[] args) {
SpringApplicationBuilder builder = new SpringApplicationBuilder(Application.class);
new Application().configure(builder).run(args);
}
}
DrinkEndpoint.java:
#Component
#Path("/drinks")
public class DrinkEndpoint {
#Autowired
private DrinkService drinkService;
#GET
#Path("/list")
#Produces(MediaType.APPLICATION_JSON)
public Iterable<Drink> getCostliest() {
return drinkService.getDrinks();
}
#GET
#Path("/{drink}")
#Produces(MediaType.APPLICATION_JSON)
public Response getCheapest(#PathParam("drink") String name) {
Optional<Drink> drink = drinkService.getDrink(name);
if (drink.isPresent()) {
return Response.status(201).entity(drink.get().getName()).build();
} else {
return Response.status(201).entity("NOT_FOUND").build();
}
}
}
DrinkService.java:
public interface DrinkService {
Iterable<Drink> getDrinks();
Optional<Drink> getDrink(String name);
}
DrinkServiceImpl.java:
#Component
public class DrinkServiceImpl implements DrinkService {
private List<Drink> drinks = new ArrayList<Drink>() {{
add(new Drink("Coca Cola", 1886));
add(new Drink("Pepsi", 1903));
}};
public Iterable<Drink> getDrinks() {
return drinks;
}
public Optional<Drink> getDrink(String name) {
for (Drink drink : drinks) {
if (drink.getName().equalsIgnoreCase(name)) {
return Optional.of(drink);
}
}
return Optional.empty();
}
}
ApplicationConfig.java:
#Component
public class ApplicationConfig extends ResourceConfig {
public ApplicationConfig() {
register(DrinkEndpoint.class);
}
}
Drink.java:
public class Drink {
private String name;
private int since;
public Drink(String name, int since) {
this.name = name;
this.since = since;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getSince() {
return since;
}
public void setSince(int since) {
this.since = since;
}
}
pom.xml:
<project>
....
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>1.3.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jersey</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
To start application run:
mvn spring-boot:run
Then open in browser:
http://localhost:8080/drinks/list
http://localhost:8080/drinks/pepsi
Make sure you are returning proper XML format
Example:
#Produces(MediaType.APPLICATION_XML)
public String getCheapest() {
return "<abc>xyz</abc>";
}
If BusinessLayer().getCheapest() function is returning String and you have used tag #Produces(MediaType.APPLICATION_XML) it will just show you a blank page.
Use appropriate MediaType in #Produces tag according to the value returned from BusinessLayer().getCheapest() function