style="display:inline-block;width:728px;height:90px"
data-ad-client="ca-pub-5164839828746352"
data-ad-slot="7563230308">

Payara, Web content

Del post anterior pueden observar que tenemos 2 servicios que la idea es usarlos en al menos una pequeña aplicación web, esta estará creada con las siguientes dependencias:

  • Preact
  • Redux
  • Ramda
  • Less

Para ello tomaremos como base la plantilla que he creado para una aplicación React. https://github.com/betotto/react-template. Las instrucciones de como usarla están en el README de ese repo.

Configurando el ambiente de desarrollo

La plantilla usa webpack como herramienta de construcción del sitio, pero solo nos enfocaremos a la parte puramente web, los servicios no estarán en Nodejs sino en nuestra aplicación que desarrollamos en Payara, pero no vamos a estar haciendo dos cambios en el código compilando y desplegando para ver que el cambio funciona, ya tenemos los servicios, entonces los dejamos corriendo en el server como si fuera un servidor de servicios cualquiera y crearemos la aplicación con webpack-dev-server.

Pero oh sorpresa, dos cosas importantes:

- La primera es que el servidor Payara hace un binding a la ip del servidor en el que inicias la aplicación
- Necesitaríamos CORS si quisiéramos usar los servicios desde el web-dev-server hacia el server java.

Pero aqui estar las soluciones, Payara nos permite ejecutar comandos antes y después de inicializar el server, especificados en archivos de texto. Por tanto vamos a crear un archivo de texto llamado pre-commands.txt
Y pegamos este texto:

set configs.config.server-config.network-config.network-listeners.network-listener.http-listener.address=localhost

Al iniciar el server solo usa la propiedad correspondiente "java -jar payara-micro-5.183.jar --deploy PayaraMicro-0.0.1.war --prebootcommandfile pre-commands.txt" listo tenemos bindings hacia localhost, ya podremos usar http://localhost:8080/etc/etc. Ahora el segundo problema, para no habilitar CORS en nuestros servicios, web-pack-devserver nos permite crear un proxy-server (a este punto debes tener instalado Nodejs, npm, clonado el repo de la plantilla React y haber hecho npm install).

Busca en la plantilla el archivo webpack.config.js y modifica la propiedad devServer, reemplaza su contenido por:

devServer: {
  contentBase: './dist',
  proxy: {
    '/PayaraMicro-0.0.1': {
      target: 'http://localhost:8080’
    }
  },
  port: 8081
}

Listo hemos configurado un proxy para web-pack-devserver que consumira nuestros servicios como si tuviéramos instalada la app dentro del servidor payara.

Desarrollando la app

Como dije en lugar de usar React vamos a usar Preact, que es como que lo mismo pero mas barato (mas pequeño solo 3Kb gziped). Para eso vamos a necesitar los siguientes cambios:

En el archivo .babelrc reemplaza el contenido por esto:

{
  "presets": ["env", "react", "es2015"],
  "plugins": [
    ["transform-react-jsx", { "pragma":"h" }]
  ]
}

En la consola ejecuta:

npm remove react react-dom react-redux prop-types && npm i -D preact preact-redux  

Y listo podemos comenzar con la app, Básicamente serán dos componentes que muestran la lista de preguntas y uno para capturar las preguntas. La estructura del proyecto sera:

dist
|----index.html
src
|----question
|----store
styles

El primer archivo contiene el punto de entrada y la inyección de redux en el componente base estará en src y se llama app.js

import '../styles/app.less';
import { h, render } from 'preact';
import { Provider } from 'preact-redux';
import store from './store';

import AppContainer from './AppContainer';

render(<Provider store={store}>
  <AppContainer />
</Provider>, document.getElementById('app'));

Ahora vamos a crear el componente base de la aplicación, este sirve para mostrar diferentes modulos de la aplicación, modificar el estado general, mostrar cosas como loaders, alertas o cosas que afecten toda la app. Este es un container de redux y por tanto estará conectado a redux es un statefull component y estará igual en src con nombre AppContainer.js

import { h, Component } from 'preact';
import { connect } from 'preact-redux';
import QuestionContainer from './question/QuestionContainer';
import { getAllQuestionsAction } from './question/questionModule';

class AppContainer extends Component {
  constructor(props) {
    super(props);
  }

  componentDidMount() {
    this.props.getAllQuestions();
  }

  render() {
    return <QuestionContainer />;
  }
}

const mapStateToProps = () => ({ });

const mapDispatchToProps = dispatch => ({
  getAllQuestions: () => dispatch(getAllQuestionsAction())
});

export default connect(mapStateToProps, mapDispatchToProps)(AppContainer);

Ahora vamos a crear el reducer del Container principal que viene siendo el reducer de la app en general, de momento solo tenemos un control para decir si hay llamadas ajax o si no hay, este archivo estará igual en src de nombre appModule.js

import initialState from './store/initialState';
import R_merge from 'ramda/src/merge';

const BEGIN_AJAX_CALL = 'BEGIN_AJAX_CALL';
const END_AJAX_CALL = 'END_AJAX_CALL';

export default (state = initialState.appModule, action) => {
  switch(action.type) {
    case BEGIN_AJAX_CALL: {
      return R_merge(state, { currentAjaxCalls: state.currentAjaxCalls + 1 });
    }
    case END_AJAX_CALL: {
      return R_merge(state, { currentAjaxCalls: state.currentAjaxCalls - 1 });
    }
    default:
      return state;
  }
};

export const beginAjaxCallAction = () => ({ type: BEGIN_AJAX_CALL });

export const endAjaxCallAction = () => ({ type: END_AJAX_CALL });

Ahora las piezas centrales de redux en nuestra app, el store y el estado inicial. Creamos un archivo en la carpeta store de nombre index.js :

import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import appModule from '../appModule';
import questionModule from '../question/questionModule';
import initialState from './initialState';

const appReducer = combineReducers({
  appModule,
  questionModule
});

let middlewares;

if(process.env.NODE_ENV === 'production') {
  middlewares = applyMiddleware(thunk);
} else {
  middlewares = compose(applyMiddleware(thunk), window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__());
}

const store = createStore(
  appReducer,
  initialState,
  middlewares
);

export default store;

El siguiente archivo es initialState.js en la misma carpeta y como su nombre lo dice es el estado inicial de la aplicación, aquí veremos la estructura principal de datos de toda la app sirve también como diccionario para saber que modulo controla que cosa:

export default {
  appModule: {
    currentAjaxCalls: 0
  },
  questionModule: {
    addingQuestion: false,
    questions: []
  }
};

Como vez tiene dos modulos, preguntas y el modulo general de la aplicacion que solo controla las llamadas Ajax. Ahora vamos a construir los elementos de las preguntas. Primero el modulo de las preguntas, para saber que tantas cosas puede hacer el modulo de preguntas, creamos un archivo de nombre questionModule.js en la carpeta question:

import initialState from '../store/initialState';
import R_merge from 'ramda/src/merge';
import { beginAjaxCallAction, endAjaxCallAction } from '../appModule';
import { getAllQuestions, addQuestion } from './questionApi';

const GET_ALL_QUESTIONS_SUCCESS = 'GET_ALL_QUESTIONS_SUCCESS';
const SHOW_NEW_QUESTION_FORM = 'SHOW_NEW_QUESTION_FORM';

export default (state = initialState.questionModule, action) => {
  switch(action.type) {
    case GET_ALL_QUESTIONS_SUCCESS:
      return R_merge(state, { questions: action.questions });
    case SHOW_NEW_QUESTION_FORM:
      return R_merge(state, { addingQuestion: action.shouldShow });
    default:
      return state;
  }
};

export const getAllQuestionsAction = () => dispatch => {
  dispatch(beginAjaxCallAction());
  getAllQuestions().then(resp => {
    dispatch(endAjaxCallAction());
    dispatch({
      type: GET_ALL_QUESTIONS_SUCCESS,
      questions: resp.questions
    });
  });
};

export const addQuestionAction = newQuestion => dispatch => {
  dispatch(beginAjaxCallAction());
  addQuestion(newQuestion).then(() => {
    dispatch(endAjaxCallAction());
    dispatch(getAllQuestionsAction());
    dispatch(showQuestionFormAction(false));
  });
};

export const showQuestionFormAction = shouldShow => ({
  type: SHOW_NEW_QUESTION_FORM,
  shouldShow
});

Y pues solo podemos obtener las preguntas, agregar una pregunta y mostrar o no el formulario de preguntas. Ahora el contenedor que estará conectado a esta información, creamos un archivo llamado QuestionContainer.js igual en la carpeta question:

import { h, Component } from 'preact';
import { connect } from 'preact-redux';
import QuestionList from './QuestionList';
import QuestionForm from './QuestionForm';
import { addQuestionAction, showQuestionFormAction } from './questionModule';

class QuestionContainer extends Component {
  render(props) {
    return (
      <section id="questions">
        <h1>{'Questions'}</h1>
        {!props.addingQuestion && (
          <div>
            <QuestionList questions={props.questions} />
            <br />
            <br />
            <button
              type="button"
              className="pure-button pure-button-primary"
              onClick={() => props.showQuestionForm(true)}>{'New Question'}</button>
          </div>
        )}
        {props.addingQuestion && (
          <QuestionForm
            addQuestion={props.addQuestion}
            cancelAddForm={() => props.showQuestionForm(false)} />
        )}
      </section>
    );
  }
}

const mapStateToProps = state => ({
  questions: state.questionModule.questions,
  addingQuestion: state.questionModule.addingQuestion
});

const mapDispatchToProps = dispatch => ({
  addQuestion: newQuestion => dispatch(addQuestionAction(newQuestion)),
  showQuestionForm: shouldShow => dispatch(showQuestionFormAction(shouldShow))
});

export default connect(mapStateToProps, mapDispatchToProps)(QuestionContainer);

Este muestra o el formulario de preguntas o la lista de preguntas dependiendo de en que estado estemos. Ahora vamos a agregar la lista de Preguntas, creamos un archivo de nombre QuestionList.js igual en la carpeta question:

import { h } from 'preact';
import Question from './Question';

const ComponentName = props => {
  let questionList = props.questions.map((q, i) =>
    <Question key={i} idQuestion={q.idQuestion} text={q.text} />
  );
  return (
    <table className="pure-table">
      <thead>
        <tr>
          <th>
            {'Id'}
          </th>
          <th>
            {'Text'}
          </th>
        </tr>
      </thead>
      <tbody>
        {questionList}
      </tbody>
    </table>
  );
};

export default ComponentName;

Este ya es un componente stateless, y solo recibe la lista de preguntas para crear una tabla y cada fila de la tabla es un component pregunta, así que debemos crear ese componente en la misma carpeta question, porque seguimos haciendo cosas del modulo preguntas, agregamos un archivo llamado Question.js con ese contenido:

import { h } from 'preact';

const Question = props => {
  const { idQuestion, text } = props;
  return (
    <tr>
      <td>{idQuestion}</td>
      <td>{text}</td>
    </tr>
  );
};

export default Question;

Otro componente tonto que solo pinta un row de una tabla por cada pregunta. Finalmente necesitamos agregar el componente que nos permita agregar una nueva pregunta, en la misma carpeta question agregamos un archivo de nombre QuestionForm.js:

import { h } from 'preact';

const QuestionForm = props => {
  const { addQuestion, cancelAddForm } = props;
  const submitFun = e => {
    e.preventDefault();
    const inputQuestion = document.getElementById('questionText');
    const text = inputQuestion.value;
    if(text !== '') {
      addQuestion({ text });
    }
    inputQuestion.value = '';
  };
  return (
    <div>
      <form className="pure-form" onSubmit={submitFun}>
        <fieldset>
          <legend>{'Write the text for the new Queston'}</legend>

          <label htmlFor="questionText">{'Question Text: '}</label>
          <input id="questionText" type="text" placeholder="Question text" required />

          <button type="submit" className="pure-button pure-button-primary">{'Add Question'}</button>
        </fieldset>
      </form>
      <button
        type="button"
        className="pure-button pure-button-primary"
        onClick={cancelAddForm}>{'Cancel'}</button>
    </div>
  );
};

export default QuestionForm;

Ahora nos falta crear el api que envíe las peticiones al back para poder obtener y guardar la información. Es un api de preguntas por lo que estará en la carpeta question y tendrá por nombre questionApi.js:

const servicesLocation = '/PayaraMicro-0.0.1';

const validateJsonResponse = statusOk => response => {
  const contentType = response.headers.get('content-type');
  const status = response.status;
  if(!contentType || !contentType.includes('application/json')) {
    throw new TypeError('No json response');
  }
  if(status !== statusOk) {
    throw new TypeError('Invalid response');
  }
  return response.json();
};

export const getAllQuestions = () => fetch(`${servicesLocation}/services/question/getAll`, {
  method: 'GET',
  cache: 'no-cache',
  credentials: 'same-origin',
  headers: {
    'Content-Type': 'application/json; charset=utf-8',
  },
  referrer: 'no-referrer'
}).then(validateJsonResponse(200));

export const addQuestion = data => fetch(`${servicesLocation}/services/question/add`, {
  method: 'POST',
  cache: 'no-cache',
  credentials: 'same-origin',
  headers: {
    'Content-Type': 'application/json; charset=utf-8',
  },
  referrer: 'no-referrer',
  body: JSON.stringify(data)
}).then(validateJsonResponse(201));

De momento como te darás cuenta los errores los ignoro, pero eso cambiara conforme la app crezca, otra cosa a notar que estamos usando los estilos de pure.css, pero ya no tenemos la todoApp de la aplicación en el template. Entonces en la carpeta less borra la carpeta todoModule y cambia el nombre del archivo pure.css por pure.less y dentro de app.less coloca este contenido:

@import "./pure.less";

#app {
 
}

Listo, hemos terminado el ejemplo. Solo nos falta ejecutar en la consola “npm run dev-server” e ir a la pagina http://localhost:8081

Ver las preguntas:

Agregar pregunta:

Pregunta agregada:

Siguientes pasos

Lo que sigue es hacer la version “productiva” de la aplicación, es decir sin web-devserver sino con Payara entregando tanto el html como el css y el js. Eso lo veremos en el siguiente post. Si quieres comparar archivos contra lo que yo escribí, estos son los repositorios:

https://github.com/betotto/payara-client

https://github.com/betotto/payara-kotlin

style="display:inline-block;width:728px;height:90px"
data-ad-client="ca-pub-5164839828746352"
data-ad-slot="7563230308">