├── .gitignore ├── src └── main │ ├── java │ └── com │ │ └── xavidop │ │ └── alexa │ │ ├── configuration │ │ ├── Constants.java │ │ └── WebConfig.java │ │ ├── interceptors │ │ ├── request │ │ │ ├── LogRequestInterceptor.java │ │ │ └── LocalizationRequestInterceptor.java │ │ └── response │ │ │ └── LogResponseInterceptor.java │ │ ├── properties │ │ └── PropertiesUtils.java │ │ ├── AlexaSkillAppStarter.java │ │ ├── handlers │ │ ├── SessionEndedRequestHandler.java │ │ ├── ErrorHandler.java │ │ ├── MyExceptionHandler.java │ │ ├── HelloWorldIntentHandler.java │ │ ├── HelpIntentHandler.java │ │ ├── CancelandStopIntentHandler.java │ │ ├── LaunchRequestHandler.java │ │ └── FallbackIntentHandler.java │ │ ├── servlet │ │ └── AlexaServlet.java │ │ └── localization │ │ └── LocalizationManager.java │ └── resources │ ├── application.properties │ ├── locales │ ├── strings.properties │ └── strings_es_ES.properties │ └── log4j2.xml ├── .github └── FUNDING.yml ├── pom.xml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Eclipse 2 | .classpath 3 | .project 4 | .settings/ 5 | 6 | # Intellij 7 | .idea/ 8 | *.iml 9 | *.iws 10 | 11 | # Mac 12 | .DS_Store 13 | 14 | # Maven 15 | log/ 16 | target/ 17 | 18 | #sam builds 19 | .aws-sam/ -------------------------------------------------------------------------------- /src/main/java/com/xavidop/alexa/configuration/Constants.java: -------------------------------------------------------------------------------- 1 | package com.xavidop.alexa.configuration; 2 | 3 | public final class Constants { 4 | 5 | public static final String SSL_KEYSTORE_FILE_PATH_KEY = "javax.net.ssl.keyStore"; 6 | public static final String SSL_KEYSTORE_PASSWORD_KEY = "javax.net.ssl.keyStorePassword"; 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # Server Context Root and Port 2 | server.port=8080 3 | 4 | # Logging 5 | 6 | logging.level.root=INFO 7 | 8 | com.amazon.ask.servlet.disableRequestSignatureCheck=true 9 | com.amazon.speech.speechlet.servlet.timestampTolerance=3600000 10 | #javax.net.ssl.keyStore=YOUR-KEYSTORE-FILE-PATH-HERE 11 | #javax.net.ssl.keyStorePassword=YOUR-KEYSTOREPASSWORD-HERE -------------------------------------------------------------------------------- /src/main/resources/locales/strings.properties: -------------------------------------------------------------------------------- 1 | WELCOME_MSG=Bienvenido, puedes decir Hola o Ayuda. Cual prefieres? 2 | HELLO_MSG=Hola Mundo! 3 | HELP_MSG=Puedes decirme hola. Cómo te puedo ayudar? 4 | GOODBYE_MSG=Hasta luego! 5 | REFLECTOR_MSG=Acabas de activar {0} 6 | FALLBACK_MSG=Lo siento, no se nada sobre eso. Por favor inténtalo otra vez. 7 | ERROR_MSG=Lo siento, ha habido un error. Por favor inténtalo otra vez. -------------------------------------------------------------------------------- /src/main/resources/locales/strings_es_ES.properties: -------------------------------------------------------------------------------- 1 | WELCOME_MSG=Bienvenido, puedes decir Hola o Ayuda. Cual prefieres? 2 | HELLO_MSG=Hola Mundo! 3 | HELP_MSG=Puedes decirme hola. Cómo te puedo ayudar? 4 | GOODBYE_MSG=Hasta luego! 5 | REFLECTOR_MSG=Acabas de activar {0} 6 | FALLBACK_MSG=Lo siento, no se nada sobre eso. Por favor inténtalo otra vez. 7 | ERROR_MSG=Lo siento, ha habido un error. Por favor inténtalo otra vez. -------------------------------------------------------------------------------- /src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main/java/com/xavidop/alexa/interceptors/request/LogRequestInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.xavidop.alexa.interceptors.request; 2 | 3 | import com.amazon.ask.dispatcher.request.handler.HandlerInput; 4 | import com.amazon.ask.dispatcher.request.interceptor.RequestInterceptor; 5 | import org.apache.logging.log4j.LogManager; 6 | import org.apache.logging.log4j.Logger; 7 | 8 | public class LogRequestInterceptor implements RequestInterceptor { 9 | 10 | static final Logger logger = LogManager.getLogger(LogRequestInterceptor.class); 11 | @Override 12 | public void process(HandlerInput input) { 13 | logger.info(input.getRequestEnvelope().toString()); 14 | } 15 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: xavidop 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /src/main/java/com/xavidop/alexa/properties/PropertiesUtils.java: -------------------------------------------------------------------------------- 1 | package com.xavidop.alexa.properties; 2 | 3 | import java.io.IOException; 4 | import java.util.Properties; 5 | 6 | public final class PropertiesUtils { 7 | 8 | private PropertiesUtils() { 9 | 10 | } 11 | 12 | public static String getPropertyValue(String Key) { 13 | Properties prop = new Properties(); 14 | 15 | try { 16 | prop.load(PropertiesUtils.class.getResourceAsStream("/application.properties")); 17 | return prop.getProperty(Key); 18 | } catch (IOException e) { 19 | e.printStackTrace(); 20 | return null; 21 | } 22 | 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/xavidop/alexa/interceptors/response/LogResponseInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.xavidop.alexa.interceptors.response; 2 | 3 | import com.amazon.ask.dispatcher.request.handler.HandlerInput; 4 | import com.amazon.ask.dispatcher.request.interceptor.ResponseInterceptor; 5 | import com.amazon.ask.model.Response; 6 | import org.apache.logging.log4j.LogManager; 7 | import org.apache.logging.log4j.Logger; 8 | 9 | import java.util.Optional; 10 | 11 | public class LogResponseInterceptor implements ResponseInterceptor { 12 | 13 | static final Logger logger = LogManager.getLogger(LogResponseInterceptor.class); 14 | @Override 15 | public void process(HandlerInput input, Optional output) { 16 | logger.info(output.toString()); 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/java/com/xavidop/alexa/interceptors/request/LocalizationRequestInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.xavidop.alexa.interceptors.request; 2 | 3 | import com.amazon.ask.dispatcher.request.handler.HandlerInput; 4 | import com.amazon.ask.dispatcher.request.interceptor.RequestInterceptor; 5 | import com.xavidop.alexa.localization.LocalizationManager; 6 | 7 | import java.util.Locale; 8 | 9 | public class LocalizationRequestInterceptor implements RequestInterceptor { 10 | 11 | @Override 12 | public void process(HandlerInput input) { 13 | String localeString = input.getRequestEnvelope().getRequest().getLocale(); 14 | Locale locale = new Locale.Builder().setLanguageTag(localeString).build(); 15 | LocalizationManager.getInstance(locale); 16 | } 17 | } -------------------------------------------------------------------------------- /src/main/java/com/xavidop/alexa/AlexaSkillAppStarter.java: -------------------------------------------------------------------------------- 1 | package com.xavidop.alexa; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.builder.SpringApplicationBuilder; 6 | import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; 7 | 8 | @SpringBootApplication 9 | public class AlexaSkillAppStarter extends SpringBootServletInitializer { 10 | 11 | @Override 12 | protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { 13 | return application.sources(AlexaSkillAppStarter.class); 14 | } 15 | 16 | public static void main(String[] args) { 17 | SpringApplication.run(AlexaSkillAppStarter.class, args); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/xavidop/alexa/handlers/SessionEndedRequestHandler.java: -------------------------------------------------------------------------------- 1 | package com.xavidop.alexa.handlers; 2 | 3 | 4 | import com.amazon.ask.dispatcher.request.handler.HandlerInput; 5 | import com.amazon.ask.dispatcher.request.handler.RequestHandler; 6 | import com.amazon.ask.model.Response; 7 | import com.amazon.ask.model.SessionEndedRequest; 8 | 9 | import java.util.Optional; 10 | 11 | import static com.amazon.ask.request.Predicates.requestType; 12 | 13 | public class SessionEndedRequestHandler implements RequestHandler { 14 | 15 | @Override 16 | public boolean canHandle(HandlerInput input) { 17 | return input.matches(requestType(SessionEndedRequest.class)); 18 | } 19 | 20 | @Override 21 | public Optional handle(HandlerInput input) { 22 | // any cleanup logic goes here 23 | return input.getResponseBuilder().build(); 24 | } 25 | 26 | } -------------------------------------------------------------------------------- /src/main/java/com/xavidop/alexa/handlers/ErrorHandler.java: -------------------------------------------------------------------------------- 1 | package com.xavidop.alexa.handlers; 2 | 3 | import com.amazon.ask.dispatcher.request.handler.HandlerInput; 4 | import com.amazon.ask.dispatcher.request.handler.RequestHandler; 5 | import com.amazon.ask.model.Response; 6 | import com.xavidop.alexa.localization.LocalizationManager; 7 | 8 | import java.util.Optional; 9 | 10 | public class ErrorHandler implements RequestHandler { 11 | 12 | @Override 13 | public boolean canHandle(HandlerInput handlerInput) { 14 | return true; 15 | } 16 | 17 | @Override 18 | public Optional handle(HandlerInput handlerInput) { 19 | final String speechOutput = LocalizationManager.getInstance().getMessage("ERROR_MSG"); 20 | return handlerInput.getResponseBuilder() 21 | .withSpeech(speechOutput) 22 | .withReprompt(speechOutput) 23 | .build(); 24 | } 25 | } -------------------------------------------------------------------------------- /src/main/java/com/xavidop/alexa/handlers/MyExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.xavidop.alexa.handlers; 2 | 3 | import com.amazon.ask.dispatcher.exception.ExceptionHandler; 4 | import com.amazon.ask.dispatcher.request.handler.HandlerInput; 5 | import com.amazon.ask.exception.AskSdkException; 6 | import com.amazon.ask.model.Response; 7 | import com.xavidop.alexa.localization.LocalizationManager; 8 | 9 | import java.util.Optional; 10 | 11 | public class MyExceptionHandler implements ExceptionHandler { 12 | @Override 13 | public boolean canHandle(HandlerInput input, Throwable throwable) { 14 | return throwable instanceof AskSdkException; 15 | } 16 | 17 | @Override 18 | public Optional handle(HandlerInput input, Throwable throwable) { 19 | return input.getResponseBuilder() 20 | .withSpeech(LocalizationManager.getInstance().getMessage("ERROR_MSG")) 21 | .build(); 22 | } 23 | } -------------------------------------------------------------------------------- /src/main/java/com/xavidop/alexa/handlers/HelloWorldIntentHandler.java: -------------------------------------------------------------------------------- 1 | package com.xavidop.alexa.handlers; 2 | 3 | import com.amazon.ask.dispatcher.request.handler.HandlerInput; 4 | import com.amazon.ask.dispatcher.request.handler.RequestHandler; 5 | import com.amazon.ask.model.Response; 6 | import com.xavidop.alexa.localization.LocalizationManager; 7 | 8 | import java.util.Optional; 9 | 10 | import static com.amazon.ask.request.Predicates.intentName; 11 | 12 | public class HelloWorldIntentHandler implements RequestHandler { 13 | 14 | @Override 15 | public boolean canHandle(HandlerInput input) { 16 | return input.matches(intentName("HelloWorldIntent")); 17 | } 18 | 19 | @Override 20 | public Optional handle(HandlerInput input) { 21 | String speechText = LocalizationManager.getInstance().getMessage("HELLO_MSG");; 22 | return input.getResponseBuilder() 23 | .withSpeech(speechText) 24 | .withSimpleCard("HelloWorld", speechText) 25 | .build(); 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /src/main/java/com/xavidop/alexa/handlers/HelpIntentHandler.java: -------------------------------------------------------------------------------- 1 | package com.xavidop.alexa.handlers; 2 | 3 | import com.amazon.ask.dispatcher.request.handler.HandlerInput; 4 | import com.amazon.ask.dispatcher.request.handler.RequestHandler; 5 | import com.amazon.ask.model.Response; 6 | import com.xavidop.alexa.localization.LocalizationManager; 7 | 8 | import java.util.Optional; 9 | 10 | import static com.amazon.ask.request.Predicates.intentName; 11 | 12 | public class HelpIntentHandler implements RequestHandler { 13 | 14 | @Override 15 | public boolean canHandle(HandlerInput input) { 16 | return input.matches(intentName("AMAZON.HelpIntent")); 17 | } 18 | 19 | @Override 20 | public Optional handle(HandlerInput input) { 21 | String speechText = LocalizationManager.getInstance().getMessage("HELP_MSG"); 22 | return input.getResponseBuilder() 23 | .withSpeech(speechText) 24 | .withSimpleCard("HelloWorld", speechText) 25 | .withReprompt(speechText) 26 | .build(); 27 | } 28 | } -------------------------------------------------------------------------------- /src/main/java/com/xavidop/alexa/handlers/CancelandStopIntentHandler.java: -------------------------------------------------------------------------------- 1 | package com.xavidop.alexa.handlers; 2 | 3 | import com.amazon.ask.dispatcher.request.handler.HandlerInput; 4 | import com.amazon.ask.dispatcher.request.handler.RequestHandler; 5 | import com.amazon.ask.model.Response; 6 | import com.xavidop.alexa.localization.LocalizationManager; 7 | 8 | import java.util.Optional; 9 | 10 | import static com.amazon.ask.request.Predicates.intentName; 11 | 12 | public class CancelandStopIntentHandler implements RequestHandler { 13 | @Override 14 | public boolean canHandle(HandlerInput input) { 15 | return input.matches(intentName("AMAZON.StopIntent").or(intentName("AMAZON.CancelIntent"))); 16 | } 17 | 18 | @Override 19 | public Optional handle(HandlerInput input) { 20 | String speechText = LocalizationManager.getInstance().getMessage("GOODBYE_MSG");; 21 | return input.getResponseBuilder() 22 | .withSpeech(speechText) 23 | .withSimpleCard("HelloWorld", speechText) 24 | .build(); 25 | } 26 | } -------------------------------------------------------------------------------- /src/main/java/com/xavidop/alexa/handlers/LaunchRequestHandler.java: -------------------------------------------------------------------------------- 1 | package com.xavidop.alexa.handlers; 2 | 3 | import com.amazon.ask.dispatcher.request.handler.HandlerInput; 4 | import com.amazon.ask.dispatcher.request.handler.RequestHandler; 5 | import com.amazon.ask.model.LaunchRequest; 6 | import com.amazon.ask.model.Response; 7 | import com.xavidop.alexa.localization.LocalizationManager; 8 | 9 | import java.util.Optional; 10 | 11 | import static com.amazon.ask.request.Predicates.requestType; 12 | 13 | public class LaunchRequestHandler implements RequestHandler { 14 | 15 | @Override 16 | public boolean canHandle(HandlerInput input) { 17 | return input.matches(requestType(LaunchRequest.class)); 18 | } 19 | 20 | @Override 21 | public Optional handle(HandlerInput input) { 22 | String speechText = LocalizationManager.getInstance().getMessage("WELCOME_MSG"); 23 | return input.getResponseBuilder() 24 | .withSpeech(speechText) 25 | .withSimpleCard("HelloWorld", speechText) 26 | .withReprompt(speechText) 27 | .build(); 28 | } 29 | 30 | } -------------------------------------------------------------------------------- /src/main/java/com/xavidop/alexa/handlers/FallbackIntentHandler.java: -------------------------------------------------------------------------------- 1 | package com.xavidop.alexa.handlers; 2 | 3 | 4 | import com.amazon.ask.dispatcher.request.handler.HandlerInput; 5 | import com.amazon.ask.dispatcher.request.handler.RequestHandler; 6 | import com.amazon.ask.model.Response; 7 | import com.xavidop.alexa.localization.LocalizationManager; 8 | 9 | import java.util.Optional; 10 | 11 | import static com.amazon.ask.request.Predicates.intentName; 12 | 13 | // 2018-July-09: AMAZON.FallackIntent is only currently available in en-US locale. 14 | // This handler will not be triggered except in that locale, so it can be 15 | // safely deployed for any locale. 16 | public class FallbackIntentHandler implements RequestHandler { 17 | 18 | @Override 19 | public boolean canHandle(HandlerInput input) { 20 | return input.matches(intentName("AMAZON.FallbackIntent")); 21 | } 22 | 23 | @Override 24 | public Optional handle(HandlerInput input) { 25 | String speechText = LocalizationManager.getInstance().getMessage("FALLBACK_MSG"); 26 | return input.getResponseBuilder() 27 | .withSpeech(speechText) 28 | .withSimpleCard("HelloWorld", speechText) 29 | .withReprompt(speechText) 30 | .build(); 31 | } 32 | } -------------------------------------------------------------------------------- /src/main/java/com/xavidop/alexa/servlet/AlexaServlet.java: -------------------------------------------------------------------------------- 1 | package com.xavidop.alexa.servlet; 2 | 3 | import com.amazon.ask.Skill; 4 | import com.amazon.ask.Skills; 5 | import com.amazon.ask.servlet.SkillServlet; 6 | import com.xavidop.alexa.handlers.*; 7 | import com.xavidop.alexa.interceptors.request.LocalizationRequestInterceptor; 8 | import com.xavidop.alexa.interceptors.request.LogRequestInterceptor; 9 | import com.xavidop.alexa.interceptors.response.LogResponseInterceptor; 10 | 11 | public class AlexaServlet extends SkillServlet { 12 | 13 | public AlexaServlet() { 14 | super(getSkill()); 15 | } 16 | 17 | private static Skill getSkill() { 18 | return Skills.standard() 19 | .addRequestHandlers( 20 | new CancelandStopIntentHandler(), 21 | new HelloWorldIntentHandler(), 22 | new HelpIntentHandler(), 23 | new LaunchRequestHandler(), 24 | new SessionEndedRequestHandler(), 25 | new FallbackIntentHandler(), 26 | new ErrorHandler()) 27 | .addExceptionHandler(new MyExceptionHandler()) 28 | .addRequestInterceptors( 29 | new LogRequestInterceptor(), 30 | new LocalizationRequestInterceptor()) 31 | .addResponseInterceptors(new LogResponseInterceptor()) 32 | // Add your skill id below 33 | //.withSkillId("[unique-value-here]") 34 | .build(); 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /src/main/java/com/xavidop/alexa/configuration/WebConfig.java: -------------------------------------------------------------------------------- 1 | package com.xavidop.alexa.configuration; 2 | 3 | import com.amazon.ask.servlet.ServletConstants; 4 | import com.xavidop.alexa.properties.PropertiesUtils; 5 | import com.xavidop.alexa.servlet.AlexaServlet; 6 | import org.springframework.boot.web.servlet.ServletRegistrationBean; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | 10 | import javax.servlet.http.HttpServlet; 11 | 12 | @Configuration 13 | public class WebConfig { 14 | @Bean 15 | public ServletRegistrationBean alexaServlet() { 16 | 17 | loadProperties(); 18 | 19 | ServletRegistrationBean servRegBean = new ServletRegistrationBean<>(); 20 | servRegBean.setServlet(new AlexaServlet()); 21 | servRegBean.addUrlMappings("/alexa/*"); 22 | servRegBean.setLoadOnStartup(1); 23 | return servRegBean; 24 | } 25 | 26 | private void loadProperties() { 27 | System.setProperty(ServletConstants.TIMESTAMP_TOLERANCE_SYSTEM_PROPERTY, PropertiesUtils.getPropertyValue(ServletConstants.TIMESTAMP_TOLERANCE_SYSTEM_PROPERTY)); 28 | System.setProperty(ServletConstants.DISABLE_REQUEST_SIGNATURE_CHECK_SYSTEM_PROPERTY, PropertiesUtils.getPropertyValue(ServletConstants.DISABLE_REQUEST_SIGNATURE_CHECK_SYSTEM_PROPERTY)); 29 | System.setProperty(Constants.SSL_KEYSTORE_FILE_PATH_KEY, Constants.SSL_KEYSTORE_FILE_PATH_KEY); 30 | System.setProperty(Constants.SSL_KEYSTORE_PASSWORD_KEY, Constants.SSL_KEYSTORE_PASSWORD_KEY); 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /src/main/java/com/xavidop/alexa/localization/LocalizationManager.java: -------------------------------------------------------------------------------- 1 | package com.xavidop.alexa.localization; 2 | 3 | import java.text.MessageFormat; 4 | import java.util.Locale; 5 | import java.util.ResourceBundle; 6 | 7 | public class LocalizationManager { 8 | // static variable single_instance of type Singleton 9 | private static LocalizationManager instance = null; 10 | 11 | // variable of type String 12 | private Locale currentLocale; 13 | 14 | // private constructor restricted to this class itself 15 | private LocalizationManager(Locale locale) 16 | { 17 | this.setCurrentLocale(locale); 18 | } 19 | 20 | // private constructor restricted to this class itself 21 | private LocalizationManager() 22 | { 23 | } 24 | 25 | private ResourceBundle bundle; 26 | 27 | public String getMessage(String key) { 28 | if(bundle == null) { 29 | bundle = ResourceBundle.getBundle("locales/strings", this.getCurrentLocale()); 30 | } 31 | return bundle.getString(key); 32 | } 33 | 34 | public String getMessage(String key, Object ... arguments) { 35 | return MessageFormat.format(getMessage(key), arguments); 36 | } 37 | 38 | // static method to create instance of Singleton class 39 | public static LocalizationManager getInstance(Locale locale) 40 | { 41 | if (instance == null){ 42 | instance = new LocalizationManager(locale); 43 | } 44 | 45 | return instance; 46 | } 47 | 48 | // static method to create instance of Singleton class 49 | public static LocalizationManager getInstance() 50 | { 51 | if (instance == null){ 52 | instance = new LocalizationManager(); 53 | } 54 | 55 | return instance; 56 | } 57 | 58 | public Locale getCurrentLocale() { 59 | return this.currentLocale; 60 | } 61 | 62 | public void setCurrentLocale(Locale currentLocale) { 63 | this.currentLocale = currentLocale; 64 | } 65 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | cam.xavidop.alexa 8 | alexa-java-springboot-helloworld 9 | jar 10 | 1.0-SNAPSHOT 11 | 12 | 13 | org.springframework.boot 14 | spring-boot-starter-parent 15 | 2.2.5.RELEASE 16 | 17 | 18 | UTF-8 19 | 1.8 20 | 2.17.1 21 | 2.29.0 22 | 23 | 24 | 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter-web 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-devtools 33 | true 34 | 35 | 36 | com.amazon.alexa 37 | ask-sdk 38 | ${alexa.ask.version} 39 | 40 | 41 | org.apache.logging.log4j 42 | log4j-api 43 | ${log4j.version} 44 | 45 | 46 | org.apache.logging.log4j 47 | log4j-core 48 | ${log4j.version} 49 | 50 | 51 | 52 | 53 | 54 | org.springframework.boot 55 | spring-boot-maven-plugin 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Alexa Skill with Spring Boot 2 | 3 | You can build a custom skill for Alexa by extending a servlet that accepts requests from and sends responses to the Alexa service in the cloud. 4 | 5 | This project will walk through how to build a Alexa Skill with Spring Boot and Http Servlet mapping example. 6 | 7 | Servlet mapping can be achieved either by using `ServletRegistrationBean` in Spring Boot as well as using Spring annotations. 8 | In this example we are going to use the `ServletRegistrationBean` class to register the Alexa Servlet as a Spring bean. 9 | 10 | ## Prerequisites 11 | 12 | Here you have the technologies used in this project 13 | 1. Java 1.8 14 | 2. Alexa Skill Kit 2.29.0 15 | 3. Spring Boot 2.5.0.RELEASE 16 | 4. Maven 3.6.1 17 | 5. IntelliJ IDEA 18 | 6. ngrok 19 | 20 | ## Structure of the project 21 | Find below the structure of this project: 22 | 23 | ```bash 24 | ├────src 25 | │ └───main 26 | │ ├───java 27 | │ │ └───com 28 | │ │ └───xavidop 29 | │ │ └───alexa 30 | │ │ ├───configuration 31 | │ │ ├───handlers 32 | │ │ ├───interceptors 33 | │ │ ├───localization 34 | │ │ ├───properties 35 | │ │ └───servlet 36 | │ │ 37 | │ └───resources 38 | │ └───locales 39 | │ application.properties 40 | │ log4j2.xml 41 | └── pom.xml 42 | ``` 43 | 44 | These are the main folders and files of this project: 45 | * **configuration**: this folder has the `WebConfig` class which will register the Alexa Http Servlet. 46 | * **handlers**: all the Alexa handlers. They will be process all Alexa requests. 47 | * **properties**: here you can find the `PropertiesUtils` class that read Spring `application.propeties` file. 48 | * **interceptors**: loggers and localization interceptors. 49 | * **localization**: Manager that will manage i18n. 50 | * **servlet**: the entry point of POST Requests is here. This is the `AlexaServlet`. 51 | * **resources**: Alexa, Spring and Log4j2 configurations. 52 | * **pom.xml**: dependencies of this project. 53 | 54 | ## Maven dependencies 55 | 56 | These are the dependencies used in this example. You can find them in `pom.xml` file: 57 | 58 | * Spring Boot: 59 | ```xml 60 | 61 | org.springframework.boot 62 | spring-boot-starter-parent 63 | 2.2.5.RELEASE 64 | 65 | ``` 66 | 67 | * Alexa Skill Kit: 68 | ```xml 69 | 70 | com.amazon.alexa 71 | ask-sdk 72 | 2.29.0 73 | 74 | ``` 75 | 76 | * Spring Boot starter web: 77 | ```xml 78 | 79 | org.springframework.boot 80 | spring-boot-starter-web 81 | 82 | ``` 83 | 84 | * Log4j2: 85 | ```xml 86 | 87 | org.apache.logging.log4j 88 | log4j-api 89 | 2.13.1 90 | 91 | 92 | org.apache.logging.log4j 93 | log4j-core 94 | 2.13.1 95 | 96 | ``` 97 | 98 | * Spring Boot Maven build plug-in: 99 | ```xml 100 | 101 | 102 | 103 | org.springframework.boot 104 | spring-boot-maven-plugin 105 | 106 | 107 | 108 | ``` 109 | 110 | ## The Alexa Http Servlet 111 | 112 | Thanks to Alexa Http Servlet Support that you can find in the [Alexa official GitHub repository](https://github.com/alexa/alexa-skills-kit-sdk-for-java/tree/2.0.x/ask-sdk-servlet-support) and its `SkillServlet` class we can register it with Spring Boot using `ServletRegistrationBean` as following. 113 | 114 | The `SkillServlet` class registers the skill instance from the SkillBuilder object, and provides a doPost method which is responsible for deserialization of the incoming request, verification of the input request before invoking the skill, and serialization of the generated response. 115 | 116 | Our `AlexaServlet` class, located in the `servlet` folder, extends `SkillServlet` and after its registrations as a servlet, it will be our main entry point. 117 | 118 | This is how this class looks like: 119 | 120 | ```java 121 | public class AlexaServlet extends SkillServlet { 122 | 123 | public AlexaServlet() { 124 | super(getSkill()); 125 | } 126 | 127 | private static Skill getSkill() { 128 | return Skills.standard() 129 | .addRequestHandlers( 130 | new CancelandStopIntentHandler(), 131 | new HelloWorldIntentHandler(), 132 | new HelpIntentHandler(), 133 | new LaunchRequestHandler(), 134 | new SessionEndedRequestHandler(), 135 | new FallbackIntentHandler(), 136 | new ErrorHandler()) 137 | .addExceptionHandler(new MyExceptionHandler()) 138 | .addRequestInterceptors( 139 | new LogRequestInterceptor(), 140 | new LocalizationRequestInterceptor()) 141 | .addResponseInterceptors(new LogResponseInterceptor()) 142 | // Add your skill id below 143 | //.withSkillId("[unique-value-here]") 144 | .build(); 145 | } 146 | 147 | } 148 | ``` 149 | 150 | It will receive all POST requests from Alexa and will send them to a specific handler, located in `handlers` folders, that can manage those requests. 151 | 152 | ## Registering the Alexa Http Servlet as Spring Beans using ServletRegistrationBean 153 | 154 | `ServletRegistrationBean` is used to register Servlets. We need to create a bean of this class in `WebConfig`, our Spring Configuration class. 155 | The most relevant methods of the `ServletRegistrationBean` class that we used in this project are: 156 | * `setServlet`: Sets the servlet to be registered. In our case, `AlexaServlet`. 157 | * `addUrlMappings`: Add URL mappings for the Servlet. We used `/alexa`. 158 | * `setLoadOnStartup`: Sets priority to load Servlet on startup. It is not as important as the two methods above because we only have one http Servlet in this example. 159 | 160 | The `WebConfig` class is where we register the Alexa Http Servlet. This is how we register the servlet: 161 | 162 | ```Java 163 | @Bean 164 | public ServletRegistrationBean alexaServlet() { 165 | 166 | loadProperties(); 167 | 168 | ServletRegistrationBean servRegBean = new ServletRegistrationBean<>(); 169 | servRegBean.setServlet(new AlexaServlet()); 170 | servRegBean.addUrlMappings("/alexa/*"); 171 | servRegBean.setLoadOnStartup(1); 172 | return servRegBean; 173 | } 174 | ``` 175 | ## Setting properties 176 | 177 | The servlet must meet certain requirements to handle requests sent by Alexa and adhere to the Alexa Skills Kit interface standards. 178 | For more information, see Host a Custom Skill as a Web Service in the [Alexa Skills Kit technical documentation](https://developer.amazon.com/es-ES/docs/alexa/custom-skills/host-a-custom-skill-as-a-web-service.html). 179 | 180 | In this example you have 4 properties that you can set in `application.properties` file: 181 | 182 | * server.port: the port that the Spring Boot app will be use. 183 | * com.amazon.ask.servlet.disableRequestSignatureCheck: disable/enable security. 184 | * com.amazon.speech.speechlet.servlet.timestampTolerance: the maximum gap between the timestamps of the request and the current local time of execution. In miliseconds. 185 | * javax.net.ssl.keyStore: if the first property is set to `false` then you have to specify the path of your keystore file. 186 | * javax.net.ssl.keyStorePassword: if the first property is set to `false` then you have to specify the password of your keystore. 187 | 188 | ## Build the Skill with Spring Boot 189 | 190 | As it is a maven project, you can build the Spring Boot application running this command: 191 | 192 | ```bash 193 | mvn clean package 194 | ``` 195 | 196 | ## Run the Skill with Spring Boot 197 | 198 | Run the AlexaSkillAppStarter.java class as Java application i.e. go to Run→ Run as → Java Application 199 | 200 | Or, you can use 201 | ```bash 202 | mvn spring-boot:run 203 | ``` 204 | 205 | After executing the main class, you can send Alexa POST requests to http://localhost:8080/alexa. 206 | 207 | ## Debug the Skill with Spring Boot 208 | 209 | For debugging the Spring boot app as Java application i.e. go to Debug→ Debug as → Java Application 210 | 211 | Or, if you use IntelliJ IDEA, you can do a right click in Main method of `AlexaSkillAppStarter` class: 212 | 213 | ![image](https://xavidop.github.io/assets/img/blog/tutorials/alexa-springboot/debug.png) 214 | 215 | After executing the main class in debug mode, you can send Alexa POST requests to http://localhost:8080/alexa and debug the Skill. 216 | 217 | ## Test requests locally 218 | 219 | I'm sure you already know the famous tool call [Postman](https://www.postman.com/). REST APIs have become the new standard in providing a public and secure interface for your service. Though REST has become ubiquitous, it's not always easy to test. Postman, makes it easier to test and manage HTTP REST APIs. Postman gives us multiple features to import, test and share APIs, which will help you and your team be more productive in the long run. 220 | 221 | After run your application you will have an endpoint available at http://localhost:8080/alexa. With Postman you can emulate any Alexa Request. 222 | 223 | For example, you can test a `LaunchRequest`: 224 | 225 | ```json 226 | { 227 | "version": "1.0", 228 | "session": { 229 | "new": true, 230 | "sessionId": "amzn1.echo-api.session.[unique-value-here]", 231 | "application": { 232 | "applicationId": "amzn1.ask.skill.[unique-value-here]" 233 | }, 234 | "user": { 235 | "userId": "amzn1.ask.account.[unique-value-here]" 236 | }, 237 | "attributes": {} 238 | }, 239 | "context": { 240 | "AudioPlayer": { 241 | "playerActivity": "IDLE" 242 | }, 243 | "System": { 244 | "application": { 245 | "applicationId": "amzn1.ask.skill.[unique-value-here]" 246 | }, 247 | "user": { 248 | "userId": "amzn1.ask.account.[unique-value-here]" 249 | }, 250 | "device": { 251 | "supportedInterfaces": { 252 | "AudioPlayer": {} 253 | } 254 | } 255 | } 256 | }, 257 | "request": { 258 | "type": "LaunchRequest", 259 | "requestId": "amzn1.echo-api.request.[unique-value-here]", 260 | "timestamp": "2020-03-22T17:24:44Z", 261 | "locale": "en-US" 262 | } 263 | } 264 | 265 | ``` 266 | 267 | Pay attention with the timestamp field of the request to accomplish with the property `com.amazon.speech.speechlet.servlet.timestampTolerance`. 268 | 269 | ## Test requests directly from Alexa 270 | 271 | ngrok is a very cool, lightweight tool that creates a secure tunnel on your local machine along with a public URL you can use for browsing your local site or APIs. 272 | 273 | When ngrok is running, it listens on the same port that you’re local web server is running on and proxies external requests to your local machine 274 | 275 | From there, it’s a simple step to get it to listen to your web server. Say you’re running your local web server on port 8080. In terminal, you’d type in: `ngrok http 8080`. This starts ngrok listening on port 8080 and creates the secure tunnel: 276 | 277 | ![image](https://xavidop.github.io/assets/img/blog/tutorials/alexa-springboot/tunnel.png) 278 | 279 | So now you have to go to [Alexa Developer console](https://developer.amazon.com/alexa/console/ask), go to your skill > endpoints > https, add the https url generated above followed by /alexa. Eg: https://fe8ee91c.ngrok.io/alexa. 280 | 281 | Select the My development endpoint is a sub-domain.... option from the dropdown and click save endpoint at the top of the page. 282 | 283 | Go to Test tab in the Alexa Developer Console and launch your skill. 284 | 285 | The Alexa Developer Console will send a HTTPS request to the ngrok endpoint (https://fe8ee91c.ngrok.io/alexa) which will route it to your skill running on Spring Boot server at http://localhost:8080/alexa. 286 | 287 | 288 | ## Conclusion 289 | 290 | This example can be useful for all those developers who do not want to host their code in the cloud or do not want to use AWS Lambda functions. This is not a problem since, as you have seen in this example, Alexa gives you the possibility to create skills in different ways. I hope this example project is useful to you. 291 | 292 | That's all folks! 293 | 294 | Happy coding! 295 | --------------------------------------------------------------------------------