From 316c542fa39132032ee7e1e1da98962ae0112955 Mon Sep 17 00:00:00 2001 From: ivanmaker <43081738+ivanmaker@users.noreply.github.com> Date: Sun, 21 Oct 2018 17:00:04 -0700 Subject: [PATCH] Is this how I should be uploading my weeks work? I only added the files that I changed or created instead of uploading the entire project that includes all of the files I didn't have to change. --- BasicStockQuoteApplication.java | 177 ++++++++++++++++++++++++++++++++ InvalidXMLException.java | 20 ++++ XMLUtils.java | 106 +++++++++++++++++++ XMLUtilsTest.java | 94 +++++++++++++++++ xml/Price.java | 99 ++++++++++++++++++ xml/Stock.java | 129 +++++++++++++++++++++++ xml/Symbol.java | 98 ++++++++++++++++++ xml/Time.java | 100 ++++++++++++++++++ xml/XMLDomainObject.java | 8 ++ xml/stock_info.fxml | 52 ++++++++++ xml/stock_info.xsd | 20 ++++ 11 files changed, 903 insertions(+) create mode 100644 BasicStockQuoteApplication.java create mode 100644 InvalidXMLException.java create mode 100644 XMLUtils.java create mode 100644 XMLUtilsTest.java create mode 100644 xml/Price.java create mode 100644 xml/Stock.java create mode 100644 xml/Symbol.java create mode 100644 xml/Time.java create mode 100644 xml/XMLDomainObject.java create mode 100644 xml/stock_info.fxml create mode 100644 xml/stock_info.xsd diff --git a/BasicStockQuoteApplication.java b/BasicStockQuoteApplication.java new file mode 100644 index 0000000..66188de --- /dev/null +++ b/BasicStockQuoteApplication.java @@ -0,0 +1,177 @@ +package com.origamisoftware.teach.advanced.apps.stockquote; + +import com.origamisoftware.teach.advanced.model.StockQuery; +import com.origamisoftware.teach.advanced.model.StockQuote; +import com.origamisoftware.teach.advanced.services.StockService; +import com.origamisoftware.teach.advanced.services.StockServiceException; +import com.origamisoftware.teach.advanced.services.ServiceFactory; +import com.origamisoftware.teach.advanced.util.Interval; +import com.origamisoftware.teach.advanced.xml.Stock; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; +import java.io.StringReader; +import java.text.ParseException; +import java.util.List; + +/** + * A simple application that shows the StockService in action. + */ +public class BasicStockQuoteApplication { + + private StockService stockService; + + // an example of how to use enum - not part of assignment 3 but useful for assignment 4 + + /** + * An enumeration that indicates how the program terminates (ends) + */ + private enum ProgramTerminationStatusEnum { + + // for now, we just have normal or abnormal but could more specific ones as needed. + NORMAL(0), + ABNORMAL(-1); + + // when the program exits, this value will be reported to underlying OS + private int statusCode; + + /** + * Create a new ProgramTerminationStatusEnum + * + * @param statusCodeValue the value to return the OS. A value of 0 + * indicates success or normal termination. + * non 0 numbers indicate abnormal termination. + */ + private ProgramTerminationStatusEnum(int statusCodeValue) { + this.statusCode = statusCodeValue; + } + + /** + * @return The value sent to OS when the program ends. + */ + private int getStatusCode() { + return statusCode; + } + } + + /** + * Create a new Application. + * + * @param stockService the StockService this application instance should use for + * stock queries. + *

+ * NOTE: this is a example of Dependency Injection in action. + */ + public BasicStockQuoteApplication(StockService stockService) { + this.stockService = stockService; + } + + /** + * Given a stockQuery get back a the info about the stock to display to th user. + * + * @param stockQuery the stock to get data for. + * @return a String with the stock data in it. + * @throws StockServiceException If data about the stock can't be retrieved. This is a + * fatal error. + */ + public String displayStockQuotes(StockQuery stockQuery) throws StockServiceException { + StringBuilder stringBuilder = new StringBuilder(); + + List stockQuotes = + stockService.getQuote(stockQuery.getSymbol(), + stockQuery.getFrom(), + stockQuery.getUntil(), + Interval.DAY); // get one quote for each day in the from until date range. + + stringBuilder.append("Stock quotes for: " + stockQuery.getSymbol() + "\n"); + for (StockQuote stockQuote : stockQuotes) { + stringBuilder.append(stockQuote.toString()); + } + + return stringBuilder.toString(); + } + + /** + * Terminate the application. + * + * @param statusCode an enum value that indicates if the program terminated ok or not. + * @param diagnosticMessage A message to display to the user when the program ends. + * This should be an error message in the case of abnormal termination + *

+ * NOTE: This is an example of DRY in action. + * A program should only have one exit point. This makes it easy to do any clean up + * operations before a program quits from just one place in the code. + * It also makes for a consistent user experience. + */ + private static void exit(ProgramTerminationStatusEnum statusCode, String diagnosticMessage) { + if (statusCode == ProgramTerminationStatusEnum.NORMAL) { + System.out.println(diagnosticMessage); + } else if (statusCode == ProgramTerminationStatusEnum.ABNORMAL) { + System.err.println(diagnosticMessage); + } else { + throw new IllegalStateException("Unknown ProgramTerminationStatusEnum."); + } + System.exit(statusCode.getStatusCode()); + } + + /** + * Run the StockTicker application. + *

+ * + * @param args one or more stock symbols + */ + + private static String xmlInstance = + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + public static void main(String[] args) throws JAXBException { + + // be optimistic init to positive values + ProgramTerminationStatusEnum exitStatus = ProgramTerminationStatusEnum.NORMAL; + String programTerminationMessage = "Normal program termination."; + if (args.length != 3) { + exit(ProgramTerminationStatusEnum.ABNORMAL, + "Please supply 3 arguments a stock symbol, a start date (MM/DD/YYYY) and end date (MM/DD/YYYY)"); + } + try { + + StockQuery stockQuery = new StockQuery(args[0], args[1], args[2]); + StockService stockService = ServiceFactory.getStockService(); + BasicStockQuoteApplication basicStockQuoteApplication = + new BasicStockQuoteApplication(stockService); + basicStockQuoteApplication.displayStockQuotes(stockQuery); + + } catch (ParseException e) { + exitStatus = ProgramTerminationStatusEnum.ABNORMAL; + programTerminationMessage = "Invalid date data: " + e.getMessage(); + } catch (StockServiceException e) { + exitStatus = ProgramTerminationStatusEnum.ABNORMAL; + programTerminationMessage = "StockService failed: " + e.getMessage(); + } catch (Throwable t) { + exitStatus = ProgramTerminationStatusEnum.ABNORMAL; + programTerminationMessage = "General application error: " + t.getMessage(); + } + + // here is how to go from XML to Java + JAXBContext jaxbContext = JAXBContext.newInstance(Stock.class); + Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); + Stock stock = (Stock) unmarshaller.unmarshal(new StringReader(xmlInstance)); + System.out.println(stock.toString()); + + // here is how to go from Java to XML + JAXBContext context = JAXBContext.newInstance(Stock.class); + Marshaller marshaller = context.createMarshaller(); + //for pretty-print XML in JAXB + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); + marshaller.marshal(stock, System.out); + + exit(exitStatus, programTerminationMessage); + System.out.println("Oops could not parse a date"); + } +} diff --git a/InvalidXMLException.java b/InvalidXMLException.java new file mode 100644 index 0000000..1566cf4 --- /dev/null +++ b/InvalidXMLException.java @@ -0,0 +1,20 @@ +package com.origamisoftware.teach.advanced.util; + +/** + * Used to signal invalid XML or other JAXB related issues. + */ +public class InvalidXMLException extends Exception{ + + /** + * Constructs a new exception with the specified detail message, + * cause, suppression enabled or disabled, and writable stack + * trace enabled or disabled. + * + * @param message the detail message. + * @param cause the cause. (A {@code null} value is permitted, + * and indicates that the cause is nonexistent or unknown.) + */ + protected InvalidXMLException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/XMLUtils.java b/XMLUtils.java new file mode 100644 index 0000000..7d7c6a5 --- /dev/null +++ b/XMLUtils.java @@ -0,0 +1,106 @@ +package com.origamisoftware.teach.advanced.util; + +import com.origamisoftware.teach.advanced.xml.XMLDomainObject; +import org.xml.sax.SAXException; + +import javax.xml.XMLConstants; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; +import javax.xml.transform.Source; +import javax.xml.transform.stream.StreamSource; +import javax.xml.validation.Schema; +import javax.xml.validation.SchemaFactory; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.StringReader; + +/** + * A collection of helper methods for marshaling and unmarshaling XML instances. + */ +public class XMLUtils { + + /** + * Put the provided XML String into the specified XML Domain Object using JAXB without using + * schema validation. + * + * @param xmlInstance an XML instance that matched the XML Domain object specified by T + * @param T a XML Domain object class which corresponds the XML instance + * @return XML Domain Object of type T populated with values in the provided String. + * @throws InvalidXMLException if the provided xmlInstance cannot be successfully parsed. + + */ + public static T unmarshall(String xmlInstance, Class T) + throws InvalidXMLException { + T returnValue; + try { + Unmarshaller unmarshaller = createUnmarshaller(T); + returnValue = (T) unmarshaller.unmarshal(new StringReader(xmlInstance)); + } catch (JAXBException e) { + throw new InvalidXMLException("JAXBException issue: " +e.getMessage(),e); + } + return returnValue; + } + + /** + * Put the provided XML String into the specified XML Domain Object using JAXB using + * schema validation. + * + * @param xmlInstance an XML instance that matched the XML Domain object specified by T + * @param T a XML Domain object class which corresponds the XML instance + * @param schemaName the name of the .xsd schema which must be on the classpath - used for validation. + * @return XML Domain Object of type T populated with values in the provided String. + * @throws InvalidXMLException if the provided xmlInstance cannot be successfully parsed. + */ + public static T unmarshall(String xmlInstance, Class T, String schemaName) + throws InvalidXMLException { + + T returnValue; + try { + InputStream resourceAsStream = XMLUtils.class.getResourceAsStream(schemaName); + Source schemaSource = new StreamSource(resourceAsStream); + if (resourceAsStream == null) { + throw new IllegalStateException("Schema: " + schemaName + " on classpath. " + + "Could not validate input XML"); + } + SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); + Schema schema = schemaFactory.newSchema(schemaSource); + Unmarshaller unmarshaller = createUnmarshaller(T); + unmarshaller.setSchema(schema); + + returnValue = (T) unmarshaller.unmarshal(new StringReader(xmlInstance)); + } catch (JAXBException | SAXException e) { + throw new InvalidXMLException(e.getMessage(),e); + } + return returnValue; + } + + /** + * Serializes the domainClass into an XML instance which is returned as a String. + * @param domainClass the XML model class. + * @return a String which is a valid XML instance for the domain class provided. + * @throws InvalidXMLException is the object can't be parsed into XML. + */ + public static String marshall(XMLDomainObject domainClass) throws InvalidXMLException { + try { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + JAXBContext context = JAXBContext.newInstance(domainClass.getClass()); + Marshaller marshaller = context.createMarshaller(); + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); + marshaller.marshal(domainClass, byteArrayOutputStream); + return byteArrayOutputStream.toString(); + } catch (JAXBException e) { + throw new InvalidXMLException(e.getMessage(),e); + } + + } + + + + private static Unmarshaller createUnmarshaller(Class T) throws JAXBException { + JAXBContext jaxbContext = JAXBContext.newInstance(T); + return jaxbContext.createUnmarshaller(); + } + +} diff --git a/XMLUtilsTest.java b/XMLUtilsTest.java new file mode 100644 index 0000000..5b218f0 --- /dev/null +++ b/XMLUtilsTest.java @@ -0,0 +1,94 @@ +package com.origamisoftware.teach.advanced.util; + +import com.origamisoftware.teach.advanced.xml.Stock; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Unit tests for XML utils. + */ +public class XMLUtilsTest { + + private static String xmlStocks = "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " F\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " L\n" + + " \n" + + " F\n" + + " \n" + + " \n" + + " \n" + + " Y\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + + @Test + public void testUnmarshall() throws Exception { + Stock stock = XMLUtils.unmarshall(xmlStocks, Stock.class); + validateStock(stock); + } + + @Test + public void testUnmarshallWithSchemaValidation()throws Exception { + Stock stock = XMLUtils.unmarshall(xmlStocks, Stock.class, "/xml/stock_info.xsd"); + validateStock(stock); + } + + @Test + public void testMarshall() throws Exception { + Stock stock = XMLUtils.unmarshall(xmlStocks, Stock.class, "/xml/stock_info.xsd"); + String xml = XMLUtils.marshall(stock); + // input xml should be the same as output xml + assertEquals("XML out is correct", xml.trim() ,xmlStocks.trim()); + } + + private void validateStock(Stock stock) { + assertTrue("Symbol name is correct", stock.getSymbol().getContent().equals("")); + assertTrue("Price name is correct", stock.getPrice().getContent().equals("")); + assertTrue("Time is not null", stock.getTime().getContent().equals("")); + } + + +} diff --git a/xml/Price.java b/xml/Price.java new file mode 100644 index 0000000..7685a5f --- /dev/null +++ b/xml/Price.java @@ -0,0 +1,99 @@ + +package com.origamisoftware.teach.advanced.xml; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlSchemaType; +import javax.xml.bind.annotation.XmlType; +import javax.xml.bind.annotation.XmlValue; +import java.math.BigDecimal; + + +/** + *

Java class for anonymous complex type. + * + *

The following schema fragment specifies the expected content contained within this class. + * + *

+ * <complexType>
+ *   <complexContent>
+ *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+ *       <attribute name="price" use="required" type="{http://www.w3.org/2001/XMLSchema}anySimpleType" />
+ *     </restriction>
+ *   </complexContent>
+ * </complexType>
+ * 
+ * + * + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "", propOrder = { + "content" +}) +@XmlRootElement(name = "price") +public class Price implements XMLDomainObject { + + @XmlValue + protected String content; + @XmlAttribute(name = "price", required = true) + @XmlSchemaType(name = "anySimpleType") + protected BigDecimal price; + + /** + * Gets the value of the content property. + * + * @return + * possible object is + * {@link String } + * + */ + public String getContent() { + return content; + } + + /** + * Sets the value of the content property. + * + * @param value + * allowed object is + * {@link String } + * + */ + public void setContent(String value) { + this.content = value; + } + + /** + * Gets the value of the price property. + * + * @return + * possible object is + * {@link BigDecimal } + * + */ + public BigDecimal getPrice() { + return price; + } + + /** + * Sets the value of the price property. + * + * @param value + * allowed object is + * {@link BigDecimal } + * + */ + public void setPrice(BigDecimal value) { + this.price = value; + } + + @Override + public String toString() { + return "Price{" + + "content='" + content + '\'' + + ", price='" + price + '\'' + + '}'; + } +} diff --git a/xml/Stock.java b/xml/Stock.java new file mode 100644 index 0000000..416748e --- /dev/null +++ b/xml/Stock.java @@ -0,0 +1,129 @@ + +package com.origamisoftware.teach.advanced.xml; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlType; + + +/** + *

Java class for anonymous complex type. + * + *

The following schema fragment specifies the expected content contained within this class. + * + *

+ * <complexType>
+ *   <complexContent>
+ *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+ *       <sequence>
+ *         <element ref="{}symbol"/>
+ *         <element ref="{}price"/>
+ *         <element ref="{}time"/>
+ *       </sequence>
+ *     </restriction>
+ *   </complexContent>
+ * </complexType>
+ * 
+ * + * + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "", propOrder = { + "symbol", + "price", + "time" +}) +@XmlRootElement(name = "stock") +public class Stock implements XMLDomainObject { + + @XmlElement(required = true) + protected Symbol symbol; + @XmlElement(required = true) + protected Price price; + @XmlElement(required = true) + protected Time time; + + /** + * Gets the value of the symbol property. + * + * @return + * possible object is + * {@link Symbol } + * + */ + public Symbol getSymbol() { + return symbol; + } + + /** + * Sets the value of the symbol property. + * + * @param value + * allowed object is + * {@link Symbol } + * + */ + public void setSymbol(Symbol value) { + this.symbol = value; + } + + /** + * Gets the value of the price property. + * + * @return + * possible object is + * {@link Price } + * + */ + public Price getPrice() { + return price; + } + + /** + * Sets the value of the price property. + * + * @param value + * allowed object is + * {@link Price } + * + */ + public void setPrice(Price value) { + this.price = value; + } + + /** + * Gets the value of the time property. + * + * @return + * possible object is + * {@link Time } + * + */ + public Time getTime() { + return time; + } + + /** + * Sets the value of the time property. + * + * @param value + * allowed object is + * {@link Time } + * + */ + public void setTime(Time value) { + this.time = value; + } + + + @Override + public String toString() { + return "stock{" + + "symbol=" + symbol + + ", price=" + price + + ", time=" + time + + '}'; + } +} diff --git a/xml/Symbol.java b/xml/Symbol.java new file mode 100644 index 0000000..0f7f9e1 --- /dev/null +++ b/xml/Symbol.java @@ -0,0 +1,98 @@ + +package com.origamisoftware.teach.advanced.xml; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlSchemaType; +import javax.xml.bind.annotation.XmlType; +import javax.xml.bind.annotation.XmlValue; + + +/** + *

Java class for anonymous complex type. + * + *

The following schema fragment specifies the expected content contained within this class. + * + *

+ * <complexType>
+ *   <complexContent>
+ *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+ *       <attribute name="symbol" use="required" type="{http://www.w3.org/2001/XMLSchema}anySimpleType" />
+ *     </restriction>
+ *   </complexContent>
+ * </complexType>
+ * 
+ * + * + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "", propOrder = { + "content" +}) +@XmlRootElement(name = "symbol") +public class Symbol implements XMLDomainObject{ + + @XmlValue + protected String content; + @XmlAttribute(name = "symbol", required = true) + @XmlSchemaType(name = "anySimpleType") + protected String symbol; + + /** + * Gets the value of the content property. + * + * @return + * possible object is + * {@link String } + * + */ + public String getContent() { + return content; + } + + /** + * Sets the value of the content property. + * + * @param value + * allowed object is + * {@link String } + * + */ + public void setContent(String value) { + this.content = value; + } + + /** + * Gets the value of the symbol property. + * + * @return + * possible object is + * {@link String } + * + */ + public String getSymbol() { + return symbol; + } + + /** + * Sets the value of the symbol property. + * + * @param value + * allowed object is + * {@link String } + * + */ + public void setSymbol (String value) { + this.symbol = value; + } + + @Override + public String toString() { + return "Symbol{" + + "content='" + content + '\'' + + ", symbol='" + symbol + '\'' + + '}'; + } +} diff --git a/xml/Time.java b/xml/Time.java new file mode 100644 index 0000000..3b068da --- /dev/null +++ b/xml/Time.java @@ -0,0 +1,100 @@ + +package com.origamisoftware.teach.advanced.xml; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlSchemaType; +import javax.xml.bind.annotation.XmlType; +import javax.xml.bind.annotation.XmlValue; +import java.math.BigDecimal; +import java.util.Date; + + +/** + *

Java class for anonymous complex type. + * + *

The following schema fragment specifies the expected content contained within this class. + * + *

+ * <complexType>
+ *   <complexContent>
+ *     <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
+ *       <attribute name="time" use="required" type="{http://www.w3.org/2001/XMLSchema}anySimpleType" />
+ *     </restriction>
+ *   </complexContent>
+ * </complexType>
+ * 
+ * + * + */ +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType(name = "", propOrder = { + "content" +}) +@XmlRootElement(name = "time") +public class Time implements XMLDomainObject { + + @XmlValue + protected String content; + @XmlAttribute(name = "time", required = true) + @XmlSchemaType(name = "anySimpleType") + protected Date time; + + /** + * Gets the value of the content property. + * + * @return + * possible object is + * {@link String } + * + */ + public String getContent() { + return content; + } + + /** + * Sets the value of the content property. + * + * @param value + * allowed object is + * {@link String } + * + */ + public void setContent(String value) { + this.content = value; + } + + /** + * Gets the value of the time property. + * + * @return + * possible object is + * {@link Date } + * + */ + public Date getTime() { + return time; + } + + /** + * Sets the value of the time property. + * + * @param value + * allowed object is + * {@link Date } + * + */ + public void setTime(Date value) { + this.time = value; + } + + @Override + public String toString() { + return "Time{" + + "content='" + content + '\'' + + ", time='" + time + '\'' + + '}'; + } +} diff --git a/xml/XMLDomainObject.java b/xml/XMLDomainObject.java new file mode 100644 index 0000000..0d51f7e --- /dev/null +++ b/xml/XMLDomainObject.java @@ -0,0 +1,8 @@ +package com.origamisoftware.teach.advanced.xml; + +/** + * A marker class the indicates the class was created from an XML instance + * + */ +public interface XMLDomainObject { +} diff --git a/xml/stock_info.fxml b/xml/stock_info.fxml new file mode 100644 index 0000000..28cee94 --- /dev/null +++ b/xml/stock_info.fxml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/xml/stock_info.xsd b/xml/stock_info.xsd new file mode 100644 index 0000000..8bd3e0f --- /dev/null +++ b/xml/stock_info.xsd @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file