Imagine there are 2 micro-services built using Spring-Boot and secured using Keycloak (OpenID + OAuth 2.0 compliant Authx Server).
from Pocket https://ift.tt/3ch0PyF
via IFTTT
Imagine there are 2 micro-services built using Spring-Boot and secured using Keycloak (OpenID + OAuth 2.0 compliant Authx Server).
from Pocket https://ift.tt/3ch0PyF
via IFTTT
Async/await was introduced in NodeJS 7.6 and is currently supported in all modern browsers. I believe it has been the single greatest addition to JS since 2017. If you are not convinced, here are a bunch of reasons with examples why you should adopt it immediately and never look back.
from Pocket https://ift.tt/2LNB5jo
via IFTTT
In this post, some of the new features of JAVA 8…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
package pt.joaobrito.java8; import java.time.Duration; import java.time.Instant; import java.time.LocalDate; import java.time.Month; import java.time.Period; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; import java.util.Optional; public class Main { public static void main(String[] args) throws InterruptedException { // simple lambda example print("Hello Lambda", s -> s.length()); // another lambda example new Thread(() -> System.out.println("Hello Runnable")).start(); List<Person> people = new ArrayList<>(Arrays.asList( new Person("John Doe", LocalDate.of(1978, Month.JUNE, 14)), new Person("Jane Doe", LocalDate.of(1983, Month.FEBRUARY, 16)), new Person("Steven Smith", LocalDate.of(2008, Month.SEPTEMBER, 23)), new Person("Martin", LocalDate.of(2010, Month.NOVEMBER, 27)), new Person("Mike", LocalDate.of(2012, Month.NOVEMBER, 3)), new Person("Louis", LocalDate.of(2014, Month.NOVEMBER, 6)), new Person("Mary", LocalDate.of(2016, Month.FEBRUARY, 16)) )); System.out.println("---- All people sorted by name -----"); people.sort((p1, p2) -> p1.getName().compareTo(p2.getName())); people.forEach(p -> System.out.println(p.getName())); System.out.println("---- All people -----"); people = new ArrayList<>(Arrays.asList( new Person("John Doe", LocalDate.of(1978, Month.JUNE, 14)), new Person("Jane Doe", LocalDate.of(1983, Month.FEBRUARY, 16)), new Person("Steven Smith", LocalDate.of(2008, Month.SEPTEMBER, 23)), new Person("Martin", LocalDate.of(2010, Month.NOVEMBER, 27)), new Person("Mike", LocalDate.of(2012, Month.NOVEMBER, 3)), new Person("Louis", LocalDate.of(2014, Month.NOVEMBER, 6)), new Person("Mary", LocalDate.of(2016, Month.FEBRUARY, 16)) )); people.stream().map(Person::getName).forEach(System.out::println); // using method reference System.out.println("---- People older than 9 yrs -----"); people .stream() // convert to stream .filter(p -> Period.between(p.getDob(), LocalDate.now()).getYears() > 9) // get only the people thaht matches the given condition .map(Person::getName) // get a new stream with only the names => returns Stream<String> .forEach(System.out::println); // iterates the entire stream and prints out each element System.out.println("---- Remove items from list -----"); people.removeIf(i -> i.getAge() < 2); // remove if the given condition is true people. stream() .map(Person::getName) .sorted(Comparator.reverseOrder()) // order the list in reverse order .forEach(System.out::println); // calculate time between two dates LocalDate dob = LocalDate.of(1978, 6, 14); LocalDate now = LocalDate.now(); Period p = Period.between(dob, now); System.out.println("Priod of " + p.getYears() + " years; " + p.getMonths() + " months, " + p.getDays() + " days."); // calculate time between two instants Instant start = Instant.now(); Thread.sleep(1000); Instant end = Instant.now(); System.out.println("The time is... " + Duration.between(start, end).toMillis()); // Optional<T> Optional<String> opt = Optional.ofNullable("some string"); // not null value System.out.println(opt.orElse("or else")); opt.orElseThrow(UnsupportedOperationException::new); opt = Optional.ofNullable(null); // notice that here we pass a null value System.out.println(opt.orElse("or else")); opt.orElseThrow(UnsupportedOperationException::new); } public static void print(String s, MyLambda l) { System.out.println(l.getLength(s)); } } @FunctionalInterface interface MyLambda { int getLength(String s); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
package pt.joaobrito.java8; import java.time.LocalDate; import java.time.Period; public class Person { private String name; private LocalDate dob; public Person(String name, LocalDate dob) { this.name = name; this.dob = dob; } public String getName() { return name; } public void setName(String name) { this.name = name; } public LocalDate getDob() { return dob; } public void setDob(LocalDate dob) { this.dob = dob; } public int getAge(){ return Period.between(dob, LocalDate.now()).getYears(); } } |
…and here are the results from running the file above
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
------------------------------------------------------------------------ Building Java8 0.0.1-SNAPSHOT ------------------------------------------------------------------------ --- exec-maven-plugin:1.2.1:exec (default-cli) @ Java8 --- 12 Hello Runnable ---- All people sorted by name ----- Jane Doe John Doe Louis Martin Mary Mike Steven Smith ---- All people ----- John Doe Jane Doe Steven Smith Martin Mike Louis Mary ---- People older than 9 yrs ----- John Doe Jane Doe ---- Remove items from list ----- Steven Smith Mike Mary Martin Louis John Doe Jane Doe Priod of 39 years; 9 months, 9 days. The time is... 1014 some string or else Exception in thread "main" java.lang.UnsupportedOperationException at java.util.Optional.orElseThrow(Optional.java:290) at pt.joaobrito.java8.Main.main(Main.java:83) ------------------------------------------------------------------------ BUILD FAILURE ------------------------------------------------------------------------ Total time: 1.649 s Finished at: 2018-03-23T00:07:33+00:00 Final Memory: 8M/309M ------------------------------------------------------------------------ |
Note that the build fails because we pass null to the Optional and we have a condition to throw an exception if null is passed to it (this is the reason why this line is the last statement of this example).
Thank you! 🙂
In this article we will explain two JAVA design patterns:
Command and State.
In the main method we:
So lets start with the code:
Here we have the client class (the one that have the main method)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
package pt.joaobrito.client; import pt.joaobrito.command.Command; import pt.joaobrito.command.LightCommand; import pt.joaobrito.command.invoker.RemoteControl; import pt.joaobrito.state.receiver.Light; /** * The client object */ public class Client { public static void main(String[] args) { // receiver Light light = new Light(); // prints to the console the current state of the light System.out.println("1st state - the light is " + (light.getCurrentState().isOn() ? "ON" : "OFF")); Command lightsCmd = new LightCommand(light); //invoker RemoteControl remoteControl = new RemoteControl(); remoteControl.setCommand(lightsCmd); //switch on remoteControl.pressButton(); System.out.println("2nd state - the light is " + (light.getCurrentState().isOn() ? "ON" : "OFF")); //switch off remoteControl.pressButton(); System.out.println("3rd state - the light is " + (light.getCurrentState().isOn() ? "ON" : "OFF")); } } |
The Command Design Pattern implementation
Here we define our Command interface
1 2 3 4 5 |
package pt.joaobrito.command; public interface Command { public void execute(); } |
This is the remote control class.
Note that in the pressButton method we actually execute the Command passed previously in the set method. Notice that this class has no knowledge at all about the receiver. Instead it just knows that it has a Command and how to interact with it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package pt.joaobrito.command.invoker; import pt.joaobrito.command.Command; /** * Invoker */ public class RemoteControl { private Command command; public void setCommand(Command command) { this.command = command; } public void pressButton() { command.execute(); } } |
Here we have the concrete light command (the implementation of the Command interface). In the execute method we specify what the command really does: in this case with the help of the State Design Pattern we are simply saying to the light to go to the next state.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package pt.joaobrito.command; import pt.joaobrito.state.receiver.Light; /** * Concrete Command: light command */ public class LightCommand implements Command { //reference to the light private Light light; public LightCommand(Light light) { this.light = light; } public void execute() { light.switchToNextState(); } } |
The next section is the State Design Pattern part
This is the Light class. From the Command point of view this is the receiver and from the point of view of the state design pattern this is the object that keeps the current state of the light (also known as a wrapper that will be passed to the concrete states).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
package pt.joaobrito.state.receiver; /** * This class is responsible to keep the state of the light */ public class Light { private State currentState; public Light() { // 1st state of light currentState = new LightOff(); } public State getCurrentState() { return currentState; } public void setState(State state) { currentState = state; } /** * light switch */ public void switchToNextState() { currentState.next(this); } } |
This is the state interface. Notice that here we have two methods that have to be implemented by the classes that implement the state interface.
1 2 3 4 5 6 |
package pt.joaobrito.state.receiver; public interface State { void next(Light wrapper); boolean isOn(); } |
The next class represents one of the possible state of the light (the concrete state that represents the ON state of the light)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package pt.joaobrito.state.receiver; /** * Receiver */ public class LightOn implements State{ @Override public void next(Light wrapper) { wrapper.setState(new LightOff()); } @Override public boolean isOn() { return true; } } |
This is the other possible state (the concrete state that represents the OFF state of the light)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package pt.joaobrito.state.receiver; /** * Receiver */ public class LightOff implements State{ @Override public void next(Light wrapper) { wrapper.setState(new LightOn()); } @Override public boolean isOn() { return false; } } |
And the results:
1 2 3 4 5 |
run: 1st state - the light is OFF 2nd state - the light is ON 3rd state - the light is OFF BUILD SUCCESSFUL (total time: 0 seconds) |
And that’s it. Feel free to comment.
Thank you!
Those who have eaten a chunk of wasabi thinking it was a chunk of avocado have learned the importance of distinguishing between two very similar things.
from Pocket https://ift.tt/2xIvCCd
via IFTTT
In this post I will show you how to decouple event producer from the event consumers (aka async events).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
@LocalBean @Stateless public class MyEJB { @Inject @InForeground private Event<BaseEvent> event; /** * fires the @InForeground event * * @throws InterruptedException */ public void sendEvent() throws InterruptedException { event.fire(new MyEvent("Hello world event", new Date())); Thread.sleep(5000); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
@Stateless public class BackgroundEventSender implements BackgroundEventSenderItf { @Resource(mappedName = "myConnectionFactory") // the JNDI name for this connection factory private ConnectionFactory connectionFactory; @Resource(mappedName = "myQueue") // JNDI name for this queue private Queue backgroundEventQueue; private Connection connection; private Session session; private MessageProducer producer; @PostConstruct public void init() { try { connection = connectionFactory.createConnection(); session = connection.createSession(true, Session.AUTO_ACKNOWLEDGE); producer = session.createProducer(backgroundEventQueue); } catch (JMSException ex) { throw new RuntimeException(ex.getMessage(), ex); } } @PreDestroy public void destroy() { try { if (connection != null) { connection.close(); } } catch (Exception ex) { // todo: handle the exception } } /** * sends the foreground event to the queue with the help of the producer object * * @param event */ @Override public void event(@Observes @InForeground BaseEvent event) { try { ObjectMessage msg = session.createObjectMessage(); msg.setObject(event); producer.send(msg); } catch (JMSException ex) { throw new RuntimeException(ex.getMessage(), ex); } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
@MessageDriven(mappedName = "myQueue", activationConfig = { @ActivationConfigProperty(propertyName = "acknowledgeMode", propertyValue = "Auto-acknowledge"), @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue") }) public class BackgroundEventDispatcher implements MessageListener { public BackgroundEventDispatcher() { } @Inject @InBackground private Event<BaseEvent> event; /** * dispatches the message received from the queue and fires a new event (now with the @InBackground qualifier) * * @param message */ public void onMessage(Message message) { if (!(message instanceof ObjectMessage)) { throw new RuntimeException("Invalid message type received"); } ObjectMessage msg = (ObjectMessage) message; try { Serializable eventObject = msg.getObject(); if (!(eventObject instanceof BaseEvent)) { throw new RuntimeException("Unknown event type received"); } BaseEvent evt = (BaseEvent) eventObject; this.event.fire(evt); } catch (JMSException ex) { throw new RuntimeException(ex.getMessage(), ex); } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@Stateless public class EventConsumer implements EventConsumerItf { /** * consumes the event sent by the JMS queue * * @param event */ @Override public void afterMyEvent(@Observes @InBackground MyEvent event) { System.out.println("Event message: " + event.getData()); System.out.println("Event time: " + new SimpleDateFormat("yyyy.MM.dd @ HH:mm:ss").format(event.getEventTime())); } } |
The event object it’s just a POJO like this one:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
public class MyEvent extends BaseEvent { private String data; private Date eventTime; public String getData() { return data; } public MyEvent(String data, Date eventTime) { this.data = data; this.eventTime = eventTime; } public void setData(String data) { this.data = data; } public Date getEventTime() { return eventTime; } public void setEventTime(Date eventTime) { this.eventTime = eventTime; } } |
You can get the full code from github. Thank you!
Feel free to comment.
In this post I will show you how to validate date ranges with JAVA annotation.
XHTML page:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html" xmlns:f="http://xmlns.jcp.org/jsf/core"> <h:head> <title>Facelet Title</title> </h:head> <h:body> <h1>Simple Date Range Validation with Annotation</h1> <h:form> <h:messages style="color: red" /> <h:outputText value="First Date" /><br /> <h:inputText value="#{simpleDateRangeController.dto.startDate}" > <f:convertDateTime pattern="yyyy-MM-dd" /> </h:inputText><br /> <h:outputText value="Second Date" /><br /> <h:inputText value="#{simpleDateRangeController.dto.endDate}" > <f:convertDateTime pattern="yyyy-MM-dd" /> </h:inputText><br /> <h:commandButton value="submit" action="#{simpleDateRangeController.action()}" /> <br /> <h:commandLink value="next" action="multipledaterange" immediate="true"/> </h:form> </h:body> </html> |
This is a simple JSF page with two date inputs and a submit button. When we press the button a the validation process will start via the backing bean that call a super class method.
Backing Bean:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
@Named @RequestScoped public class SimpleDateRangeController extends AbstractController { private SimpleDateRangeDto dto; @PostConstruct public void init() { dto = new SimpleDateRangeDto(); } public SimpleDateRangeController() { } public void action() { super.validate(dto); } // getters and setters } </pre> |
Note that in line 3 we have the super class named: AbstractController. In line 16, in the action method, we call a validate method and we pass as a parameter the dto wich has the data itself.
Abstract Controller
1 2 3 4 5 6 |
public class AbstractController implements Serializable { protected <T extends DateRangeValidation> void validate(T dto) { dto.validate(); } } |
In this abstract class we simple call the validate() method in the received DTO.
Data Transfer Object(DTO):
1 2 3 4 5 6 7 8 9 |
@ValidDateRange(endDesc = "end date", startDesc = "start date") public class SimpleDateRangeDto extends DateRangeValidation { private Date startDate; private Date endDate; // getters and setters } |
Here is the point where we annotate at the class-level with our special annotation. We simple use the annotation with 2 parameters. This parameters are optional and it’s purpose is only to personalize the error message. In case that your date fields are different from those in this example you must provide the annotation with more parameters, as you will see in the next class.
Annotation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
@Documented @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE}) @Retention(RUNTIME) @Constraint(validatedBy = {DateIntervalValidator.class}) public @interface ValidDateRange { @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE}) @Retention(RUNTIME) @Documented public @interface List { ValidDateRange[] value(); } String message() default "{endDesc} must be after {startDesc}."; String start() default "startDate"; String end() default "endDate"; String startDesc() default "start date"; String endDesc() default "end date"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; class DateIntervalValidator implements ConstraintValidator<ValidDateRange, Object> { private static final String GET = "get"; private String startDate; private String endDate; @Override public void initialize(ValidDateRange constraintAnnotation) { startDate = constraintAnnotation.start(); endDate = constraintAnnotation.end(); } @Override public boolean isValid(Object value, ConstraintValidatorContext context) { Class<?> clazz = value.getClass(); try { Method m1 = clazz.getMethod(getMethodByField(startDate), (Class<?>[]) null); Method m2 = clazz.getMethod(getMethodByField(endDate), (Class<?>[]) null); final Date d1 = (Date) m1.invoke(value, (Object[]) null); final Date d2 = (Date) m2.invoke(value, (Object[]) null); if (d2 == null) { return true; } if (d1 != null && d1.after(d2)) { return false; } } catch (Exception e) { } return true; } private String getMethodByField(String field) { StringBuilder sb = new StringBuilder(GET) .append(field.substring(0, 1).toUpperCase()) .append(field.substring(1)); return sb.toString(); } } } |
Here is where the magic happens. I will not discuss how annotations work, but I will cover the essential part: the constraint validation. The inner class DateIntervalValidator is responsible for the validation of the date fields with the help of reflection. We get the get methods of the two date fields and then we verify if endDate is after startDate. If that condition is false, we return false, otherwise we return true.
The message
is required if you want personalize your message.
The start
is required if your first date field is different from startDate. It indicates the name of the first field.
The end
is required if your end date field is different from endDate. It indicates the name of the second date field.
The purpose the startDesc
and endDesc
is to use as a placeholder in the message parameter.
Validator:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public abstract class DateRangeValidation implements Serializable { private static final long serialVersionUID = 7083856980011157895L; public void validate() { ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory(); Validator validator = validatorFactory.getValidator(); Set<ConstraintViolation<DateRangeValidation>> result = validator.validate(this); if (!result.isEmpty()) { Iterator<ConstraintViolation<DateRangeValidation>> it = result.iterator(); while (it.hasNext()) { String mesg = it.next().getMessage(); FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, mesg, mesg)); } FacesContext.getCurrentInstance().validationFailed(); } } } |
This class is the trigger of all the validation process. When in the DTO we call the validate method, is this method we are calling, because this class is the superclass of the DTO. It calls the annotation and validates the fields. If there are any erros it will collect them in a set, here called as result. Then we iterate all over the set and add the error messages to faces context.
The next few files demonstrate the same logic applied to 2 or more groups of date validations.
XHTML:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<?xml version='1.0' encoding='UTF-8' ?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://xmlns.jcp.org/jsf/html" xmlns:f="http://xmlns.jcp.org/jsf/core"> <h:head> <title>Facelet Title</title> </h:head> <h:body> <h1>Multiple Date Range Validation with Annotations</h1> <h:form> <h:messages style="color: red" /> <h:outputText value="First Date" /><br /> <h:inputText value="#{multipleDateRangeController.dto.date1}" > <f:convertDateTime pattern="yyyy-MM-dd" /> </h:inputText><br /> <h:outputText value="Second Date" /><br /> <h:inputText value="#{multipleDateRangeController.dto.date2}" > <f:convertDateTime pattern="yyyy-MM-dd" /> </h:inputText><br /> <h:outputText value="Third Date" /><br /> <h:inputText value="#{multipleDateRangeController.dto.date3}" > <f:convertDateTime pattern="yyyy-MM-dd" /> </h:inputText><br /> <h:outputText value="Fourth Date" /><br /> <h:inputText value="#{multipleDateRangeController.dto.date4}" > <f:convertDateTime pattern="yyyy-MM-dd" /> </h:inputText><br /> <h:commandButton value="submit" action="#{multipleDateRangeController.action()}" /><br /> <h:commandLink value="back" action="simpledaterange" immediate="true" /> </h:form> </h:body> </html> |
Backing Bean:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
@Named @RequestScoped public class MultipleDateRangeController extends AbstractController { private MultipleDateRangeDto dto; @PostConstruct public void init() { dto = new MultipleDateRangeDto(); } public MultipleDateRangeController() { } public void action() { super.validate(dto); } // getters and setters } |
DTO:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@ValidDateRange.List({ @ValidDateRange(start = "date1", end = "date2", message = "{end} must be after {start}"), @ValidDateRange(start = "date3", end = "date4", message = "{end} must be after {start}") }) public class MultipleDateRangeDto extends DateRangeValidation { private Date date1; private Date date2; private Date date3; private Date date4; // getters and setters } |
Plase feel free to comment or to suggest new features.
You can read more about this topic here
Download source code.
Thanks.
https://play.google.com/store/apps/details?id=pt.joaobrito.android.tides
This is not actually a new app. This is in update to an existing app that I have forgot the signing key password. So, as I’m not able anymore to update the other app, I’ve decided to publish this new one.
Of course I apologise for all of this but it’s nothing more I can do…
Please comment or suggest new features. Thanks in advance and I hope you comprehend…
Posted from WordPress for Android
Check this app: https://play.google.com/store/apps/details?id=pt.joaobrito.wcb
Now with auto refresh every 30 seconds! Try it today!
Posted from WordPress for Android