Package net.thucydides.core.steps

Source Code of net.thucydides.core.steps.BaseStepListener

package net.thucydides.core.steps;

import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.inject.Injector;
import net.thucydides.core.PendingStepException;
import net.thucydides.core.annotations.TestAnnotations;
import net.thucydides.core.guice.Injectors;
import net.thucydides.core.model.*;
import net.thucydides.core.pages.Pages;
import net.thucydides.core.pages.SystemClock;
import net.thucydides.core.screenshots.*;
import net.thucydides.core.webdriver.Configuration;
import net.thucydides.core.webdriver.WebDriverFacade;
import net.thucydides.core.webdriver.WebdriverManager;
import net.thucydides.core.webdriver.WebdriverProxyFactory;
import org.apache.commons.lang3.StringUtils;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.remote.SessionId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.*;

import static net.thucydides.core.model.Stories.findStoryFrom;
import static net.thucydides.core.model.TestResult.*;
import static net.thucydides.core.steps.BaseStepListener.ScreenshotType.MANDATORY_SCREENSHOT;
import static net.thucydides.core.steps.BaseStepListener.ScreenshotType.OPTIONAL_SCREENSHOT;

/**
* Observes the test run and stores test run details for later reporting.
* Observations are recorded in an TestOutcome object. This includes
* recording the names and results of each test, and taking and storing
* screenshots at strategic points during the tests.
*/
public class BaseStepListener implements StepListener, StepPublisher {

    /**
     * Used to build the test outcome structure as the test step results come in.
     */
    private final List<TestOutcome> testOutcomes;

    /**
     * Keeps track of what steps have been started but not finished, in order to structure nested steps.
     */
    private final Stack<TestStep> currentStepStack;

    /**
     * Keeps track of the current step group, if any.
     */
    private final Stack<TestStep> currentGroupStack;

    private StepEventBus eventBus;
    /**
     * Clock used to pause test execution.
     */
    private final SystemClock clock;

    private ScreenshotPermission screenshots;
    /**
     * The Java class (if any) containing the tests.
     */
    private Class<?> testSuite;

    private static final Logger LOGGER = LoggerFactory.getLogger(BaseStepListener.class);

    private WebDriver driver;

    private WebdriverManager webdriverManager;

    private File outputDirectory;

    private WebdriverProxyFactory proxyFactory;

    private Story testedStory;

    private Configuration configuration;

    ScreenshotProcessor screenshotProcessor;

    private boolean inFluentStepSequence;

    private List<String> storywideIssues;

    private List<TestTag> storywideTags;

    public void setEventBus(StepEventBus eventBus) {
        this.eventBus = eventBus;
    }

    public StepEventBus getEventBus() {
        if (eventBus == null) {
            eventBus = StepEventBus.getEventBus();
        }
        return eventBus;
    }

    public Optional<TestStep> cloneCurrentStep() {
        return (Optional<TestStep>) ((currentStepExists()) ? getCurrentStep().clone() : Optional.absent());
    }

    public void setAllStepsTo(TestResult result) {
        getCurrentTestOutcome().setAnnotatedResult(result);
        getCurrentTestOutcome().setAllStepsTo(result);
    }

    protected enum ScreenshotType {
        OPTIONAL_SCREENSHOT,
        MANDATORY_SCREENSHOT
    }

    public BaseStepListener(final File outputDirectory) {
        this(outputDirectory, Injectors.getInjector());
    }

    public BaseStepListener(final File outputDirectory, Injector injector) {
        this.proxyFactory = WebdriverProxyFactory.getFactory();
        this.testOutcomes = Lists.newArrayList();
        this.currentStepStack = new Stack<TestStep>();
        this.currentGroupStack = new Stack<TestStep>();
        this.outputDirectory = outputDirectory;
        this.inFluentStepSequence = false;
        this.storywideIssues = Lists.newArrayList();
        this.storywideTags = Lists.newArrayList();
        this.webdriverManager = injector.getInstance(WebdriverManager.class);
        this.clock = injector.getInstance(SystemClock.class);
        this.configuration = injector.getInstance(Configuration.class);
        this.screenshotProcessor = injector.getInstance(ScreenshotProcessor.class);
    }

    /**
     * Create a step listener with a given web driver type.
     *
     * @param driverClass     a driver of this type will be used
     * @param outputDirectory reports and screenshots are generated here
     */
    public BaseStepListener(final Class<? extends WebDriver> driverClass, final File outputDirectory) {
        this(outputDirectory);
        this.driver = getProxyFactory().proxyFor(driverClass);
    }

    public BaseStepListener(final Class<? extends WebDriver> driverClass,
                            final File outputDirectory,
                            final Configuration configuration) {
        this(outputDirectory);
        this.driver = getProxyFactory().proxyFor(driverClass);
        this.configuration = configuration;
    }

    public BaseStepListener(final File outputDirectory,
                            final WebdriverManager webdriverManager) {
        this(outputDirectory);
        this.webdriverManager = webdriverManager;
    }

    /**
     * Create a step listener using the driver from a given page factory.
     * If the pages factory is null, a new driver will be created based on the default system values.
     *
     * @param outputDirectory reports and screenshots are generated here
     * @param pages           a pages factory.
     */
    public BaseStepListener(final File outputDirectory, final Pages pages) {
        this(outputDirectory);
        if (pages != null) {
            setDriverUsingPagesDriverIfDefined(pages);
        } else {
            createNewDriver();
        }
    }

    protected ScreenshotPermission screenshots() {
        if (screenshots == null) {
            screenshots = new ScreenshotPermission(configuration);
        }
        return screenshots;
    }

    private void createNewDriver() {
        setDriver(getProxyFactory().proxyDriver());
    }

    private void setDriverUsingPagesDriverIfDefined(final Pages pages) {
        if (pages.getDriver() != null) {
            setDriver(pages.getDriver());
        } else {
            createNewDriver();
            pages.setDriver(getDriver());
        }
    }

    protected WebdriverProxyFactory getProxyFactory() {
        return proxyFactory;
    }

    protected TestOutcome getCurrentTestOutcome() {
        Preconditions.checkState(!testOutcomes.isEmpty());
        return testOutcomes.get(testOutcomes.size() - 1);
    }

    protected SystemClock getClock() {
        return clock;
    }

    /**
     * A test suite (containing a series of tests) starts.
     *
     * @param startedTestSuite the class implementing the test suite (e.g. a JUnit test case)
     */
    public void testSuiteStarted(final Class<?> startedTestSuite) {
        testSuite = startedTestSuite;
        testedStory = findStoryFrom(startedTestSuite);
        clearStorywideTagsAndIssues();
    }

    private void clearStorywideTagsAndIssues() {
        storywideIssues.clear();
        storywideTags.clear();
    }

    private boolean suiteStarted = false;

    public void testSuiteStarted(final Story story) {
        testSuite = null;
        testedStory = story;
        suiteStarted = true;
        clearStorywideTagsAndIssues();
    }

    public boolean testSuiteRunning() {
        return suiteStarted;
    }

    public void addIssuesToCurrentStory(List<String> issues) {
        storywideIssues.addAll(issues);
    }

    public void addTagsToCurrentStory(List<TestTag> tags) {
        storywideTags.addAll(tags);
    }

    public void testSuiteFinished() {
        screenshotProcessor.waitUntilDone();
        clearStorywideTagsAndIssues();
        suiteStarted = false;
    }


    /**
     * An individual test starts.
     *
     * @param testMethod the name of the test method in the test suite class.
     */
    public void testStarted(final String testMethod) {
        TestOutcome newTestOutcome = TestOutcome.forTestInStory(testMethod, testSuite, testedStory);
        testOutcomes.add(newTestOutcome);
        updateSessionIdIfKnown();
        setAnnotatedResult(testMethod);
    }

    private void updateSessionIdIfKnown() {
        SessionId sessionId = webdriverManager.getSessionId();
        if (sessionId != null) {
            getCurrentTestOutcome().setSessionId(sessionId.toString());
        }
    }

    public void updateCurrentStepTitle(String updatedStepTitle) {
        if (currentStepExists()) {
            getCurrentStep().setDescription(updatedStepTitle);
        } else {
            stepStarted(ExecutedStepDescription.withTitle(updatedStepTitle));
        }
    }

    private void setAnnotatedResult(String testMethod) {
        if (TestAnnotations.forClass(testSuite).isIgnored(testMethod)) {
            getCurrentTestOutcome().setAnnotatedResult(IGNORED);
        }
        if (TestAnnotations.forClass(testSuite).isPending(testMethod)) {
            getCurrentTestOutcome().setAnnotatedResult(PENDING);
        }
    }

    /**
     * A test has finished.
     *
     * @param outcome the result of the test that just finished.
     */
    public void testFinished(final TestOutcome outcome) {
        recordTestDuration();
        getCurrentTestOutcome().addIssues(storywideIssues);
        // TODO: Disable when run from an IDE
        getCurrentTestOutcome().addTags(storywideTags);
        currentStepStack.clear();
    }

    public void testRetried() {
        currentStepStack.clear();
        testOutcomes.remove(getCurrentTestOutcome());
    }

    private void recordTestDuration() {
        if (!testOutcomes.isEmpty()) {
            getCurrentTestOutcome().recordDuration();
        }
    }

    /**
     * A step within a test is called.
     * This step might be nested in another step, in which case the original step becomes a group of steps.
     *
     * @param description the description of the test that is about to be run
     */
    public void stepStarted(final ExecutedStepDescription description) {
        recordStep(description);
        takeInitialScreenshot();
        updateSessionIdIfKnown();
    }

    public void skippedStepStarted(final ExecutedStepDescription description) {
        recordStep(description);
    }

    private void recordStep(ExecutedStepDescription description) {
        String stepName = AnnotatedStepDescription.from(description).getName();

        updateFluentStepStatus(description, stepName);

        if (justStartedAFluentSequenceFor(description) || notInAFluentSequence()) {

            TestStep step = new TestStep(stepName);

            startNewGroupIfNested();
            setDefaultResultFromAnnotations(step, description);

            currentStepStack.push(step);
            recordStepToCurrentTestOutcome(step);
        }
        inFluentStepSequence = AnnotatedStepDescription.from(description).isFluent();
    }

    private void recordStepToCurrentTestOutcome(TestStep step) {
        getCurrentTestOutcome().recordStep(step);
    }

    private void updateFluentStepStatus(ExecutedStepDescription description, String stepName) {
        if (currentlyInAFluentSequenceFor(description) || justFinishedAFluentSequenceFor(description)) {
            addToFluentStepName(stepName);
        }
    }

    private void addToFluentStepName(String stepName) {
        String updatedStepName = getCurrentStep().getDescription() + " " + StringUtils.uncapitalize(stepName);
        getCurrentStep().setDescription(updatedStepName);
    }

    private boolean notInAFluentSequence() {
        return !inFluentStepSequence;
    }

    private boolean justFinishedAFluentSequenceFor(ExecutedStepDescription description) {
        boolean thisStepIsFluent = AnnotatedStepDescription.from(description).isFluent();
        return (inFluentStepSequence && !thisStepIsFluent);
    }

    private boolean justStartedAFluentSequenceFor(ExecutedStepDescription description) {
        boolean thisStepIsFluent = AnnotatedStepDescription.from(description).isFluent();
        return (!inFluentStepSequence && thisStepIsFluent);
    }

    private boolean currentlyInAFluentSequenceFor(ExecutedStepDescription description) {
        boolean thisStepIsFluent = AnnotatedStepDescription.from(description).isFluent();
        return (inFluentStepSequence && thisStepIsFluent);
    }

    private void setDefaultResultFromAnnotations(final TestStep step, final ExecutedStepDescription description) {
        if (TestAnnotations.isPending(description.getTestMethod())) {
            step.setResult(TestResult.PENDING);
        }
        if (TestAnnotations.isIgnored(description.getTestMethod())) {
            step.setResult(TestResult.IGNORED);
        }
    }

    private void startNewGroupIfNested() {
        if (thereAreUnfinishedSteps()) {
            if (getCurrentStep() != getCurrentGroup()) {
                startNewGroup();
            }
        }
    }

    private void startNewGroup() {
        getCurrentTestOutcome().startGroup();
        currentGroupStack.push(getCurrentStep());
    }

    private TestStep getCurrentStep() {
        return currentStepStack.peek();
    }

    private Optional<TestStep> getPreviousStep() {
        if (getCurrentTestOutcome().getTestSteps().size() > 1) {
            List<TestStep> currentTestSteps = getCurrentTestOutcome().getTestSteps();
            return Optional.of(currentTestSteps.get(currentTestSteps.size() - 2));
        } else {
            return Optional.absent();
        }
    }

    private TestStep getCurrentGroup() {
        if (currentGroupStack.isEmpty()) {
            return null;
        } else {
            return currentGroupStack.peek();// findLastChildIn(currentGroupStack.peek());
        }
    }

    private boolean thereAreUnfinishedSteps() {
        return !currentStepStack.isEmpty();
    }

    public void stepFinished() {
        updateSessionIdIfKnown();
        takeEndOfStepScreenshotFor(SUCCESS);
        currentStepDone(SUCCESS);
//        markCurrentStepAs(SUCCESS);
        pauseIfRequired();
    }

    private void updateExampleTableIfNecessary(TestResult result) {
        if (getCurrentTestOutcome().isDataDriven()) {
            getCurrentTestOutcome().updateCurrentRowResult(result);
        }
    }

    private void finishGroup() {
        currentGroupStack.pop();
        getCurrentTestOutcome().endGroup();
    }

    private void pauseIfRequired() {
        int delay = configuration.getStepDelay();
        if (delay > 0) {
            getClock().pauseFor(delay);
        }
    }

    private void markCurrentStepAs(final TestResult result) {
        getCurrentTestOutcome().currentStep().setResult(result);
        updateExampleTableIfNecessary(result);
    }

    FailureAnalysis failureAnalysis = new FailureAnalysis();

    public void stepFailed(StepFailure failure) {
        takeEndOfStepScreenshotFor(FAILURE);
        getCurrentTestOutcome().determineTestFailureCause(failure.getException());
//        markCurrentStepAs(failureAnalysis.resultFor(failure));
        recordFailureDetailsInFailingTestStep(failure);
        currentStepDone(failureAnalysis.resultFor(failure));
    }

    public void lastStepFailed(StepFailure failure) {
        takeEndOfStepScreenshotFor(FAILURE);
        getCurrentTestOutcome().lastStepFailedWith(failure);
    }


    private void recordFailureDetailsInFailingTestStep(final StepFailure failure) {
        if (currentStepExists()) {
            getCurrentStep().failedWith(new StepFailureException(failure.getMessage(), failure.getException()));
        }
    }

    public void stepIgnored() {
        if (aStepHasFailed()) {
            markCurrentStepAs(SKIPPED);
            currentStepDone(SKIPPED);
        } else {
//            markCurrentStepAs(IGNORED);
            currentStepDone(IGNORED);
        }
    }

    public void stepPending() {
//        markCurrentStepAs(PENDING);
        currentStepDone(PENDING);
    }

    public void stepPending(String message) {
        getCurrentStep().testAborted(new PendingStepException(message));
        stepPending();
    }

    public void assumptionViolated(String message) {
        if (thereAreUnfinishedSteps()) {
            getCurrentStep().testAborted(new PendingStepException(message));
            stepIgnored();
        }
        testIgnored();
    }

    private void currentStepDone(TestResult result) {
        if ((!inFluentStepSequence) && currentStepExists()) {
            TestStep finishedStep = currentStepStack.pop();
            finishedStep.recordDuration();
            if (result != null) {
                finishedStep.setResult(result);
            }
            if ((finishedStep == getCurrentGroup())) {
                finishGroup();
            }
        }
        updateExampleTableIfNecessary(result);
    }

    private boolean currentStepExists() {
        return !currentStepStack.isEmpty();
    }

    private void takeEndOfStepScreenshotFor(final TestResult result) {
        if (shouldTakeEndOfStepScreenshotFor(result)) {
            take(OPTIONAL_SCREENSHOT);
        }
    }

    private void take(final ScreenshotType screenshotType) {
        if (currentStepExists() && browserIsOpen()) {
            try {
                Optional<ScreenshotAndHtmlSource> screenshotAndHtmlSource = grabScreenshot();
                if (screenshotAndHtmlSource.isPresent()) {
                    takeScreenshotIfRequired(screenshotType, screenshotAndHtmlSource.get());
                }
                removeDuplicatedInitalScreenshotsIfPresent();
            } catch (ScreenshotException e) {
                LOGGER.warn("Failed to take screenshot", e);
            }
        }
    }

    private void removeDuplicatedInitalScreenshotsIfPresent() {
        if (currentStepHasMoreThanOneScreenshot() && getPreviousStep().isPresent() && getPreviousStep().get().hasScreenshots()) {
            ScreenshotAndHtmlSource lastScreenshotOfPreviousStep = lastScreenshotOf(getPreviousStep().get());
            ScreenshotAndHtmlSource firstScreenshotOfThisStep = getCurrentStep().getFirstScreenshot();
            if (firstScreenshotOfThisStep.hasIdenticalScreenshotsAs(lastScreenshotOfPreviousStep)) {
                removeFirstScreenshotOfCurrentStep();
            }
        }
    }

    private void removeFirstScreenshotOfCurrentStep() {
        getCurrentStep().removeScreenshot(0);
    }

    private boolean currentStepHasMoreThanOneScreenshot() {
        return getCurrentStep().getScreenshotCount() > 1;
    }

    private ScreenshotAndHtmlSource lastScreenshotOf(TestStep testStep) {
        return testStep.getScreenshots().get(testStep.getScreenshots().size() - 1);
    }

    private void takeScreenshotIfRequired(ScreenshotType screenshotType, ScreenshotAndHtmlSource screenshotAndHtmlSource) {
        if (shouldTakeScreenshot(screenshotType, screenshotAndHtmlSource) && screenshotWasTaken(screenshotAndHtmlSource)) {
            getCurrentStep().addScreenshot(screenshotAndHtmlSource);
        }
    }

    private boolean screenshotWasTaken(ScreenshotAndHtmlSource screenshotAndHtmlSource) {
        return screenshotAndHtmlSource.getScreenshotFile() != null;
    }


    private boolean shouldTakeScreenshot(ScreenshotType screenshotType,
                                         ScreenshotAndHtmlSource screenshotAndHtmlSource) {
        return (screenshotType == MANDATORY_SCREENSHOT)
                || getCurrentStep().getScreenshots().isEmpty()
                || shouldTakeOptionalScreenshot(screenshotAndHtmlSource);
    }

    private boolean shouldTakeOptionalScreenshot(ScreenshotAndHtmlSource screenshotAndHtmlSource) {
        return (screenshotAndHtmlSource.wasTaken() && previousScreenshot().isPresent()
                && (!screenshotAndHtmlSource.hasIdenticalScreenshotsAs(previousScreenshot().get())));
    }

    private Optional<ScreenshotAndHtmlSource> previousScreenshot() {
        List<ScreenshotAndHtmlSource> screenshotsToDate = getCurrentTestOutcome().getScreenshotAndHtmlSources();
        if (screenshotsToDate.isEmpty()) {
            return Optional.absent();
        } else {
            return Optional.of(screenshotsToDate.get(screenshotsToDate.size() - 1));
        }
    }

    private boolean browserIsOpen() {
        if (driver == null) {
            return false;
        }
        if (driver instanceof WebDriverFacade) {
            return (((WebDriverFacade) driver).isInstantiated());
        } else {
            return (driver.getCurrentUrl() != null);
        }
    }

    private void takeInitialScreenshot() {
        if ((currentStepExists()) && (screenshots().areAllowed(TakeScreenshots.BEFORE_AND_AFTER_EACH_STEP))) {
            take(OPTIONAL_SCREENSHOT);
        }
    }

    private Optional<ScreenshotAndHtmlSource> grabScreenshot() {
        Optional<File> screenshot = getPhotographer().takeScreenshot();
        if (screenshot.isPresent()) {
            if (shouldStoreSourcecode()) {
                File sourcecodeFile = sourcecodeForScreenshot(screenshot.get(), getPageSource());
                return Optional.of(new ScreenshotAndHtmlSource(screenshot.get(), sourcecodeFile));
            } else {
                return Optional.of(new ScreenshotAndHtmlSource(screenshot.get()));
            }
        }
        return Optional.absent();
    }

    public String getPageSource() {
        return getPhotographer().getPageSource();
    }

    private File sourcecodeForScreenshot(File screenshotFile, String pageSource) {
        File pageSourceFile = new File(screenshotFile.getAbsolutePath() + ".html");

        try {
            Files.write(pageSourceFile.toPath(), pageSource.getBytes());
        } catch (IOException e) {
            LOGGER.warn("Failed to write screen source code",e);
        }
        return pageSourceFile;
    }

    private boolean shouldStoreSourcecode() {
        return configuration.storeHtmlSourceCode();
    }

    public Photographer getPhotographer() {
        ScreenshotBlurCheck blurCheck = new ScreenshotBlurCheck();
        if (blurCheck.blurLevel().isPresent()) {
            return new Photographer(driver, outputDirectory, blurCheck.blurLevel().get());
        } else {
            return new Photographer(driver, outputDirectory);
        }
    }

    private boolean shouldTakeEndOfStepScreenshotFor(final TestResult result) {
        if (result == FAILURE) {
            return screenshots().areAllowed(TakeScreenshots.FOR_FAILURES);
        } else {
            return screenshots().areAllowed(TakeScreenshots.AFTER_EACH_STEP);
        }
    }

    public List<TestOutcome> getTestOutcomes() {
        List<TestOutcome> sortedOutcomes = Lists.newArrayList(testOutcomes);
        Collections.sort(sortedOutcomes, byStartTimeAndName());
        return ImmutableList.copyOf(sortedOutcomes);
    }

    private Comparator<? super TestOutcome> byStartTimeAndName() {
        return new Comparator<TestOutcome>() {
            public int compare(TestOutcome testOutcome1, TestOutcome testOutcome2) {
                String creationTimeAndName1 = testOutcome1.getStartTime().getMillis() + "_" + testOutcome1.getMethodName();
                String creationTimeAndName2 = testOutcome2.getStartTime().getMillis() + "_" + testOutcome2.getMethodName();
                return creationTimeAndName1.compareTo(creationTimeAndName2);
            }
        };
    }


    public void setDriver(final WebDriver driver) {
        this.driver = driver;
    }

    public WebDriver getDriver() {
        return driver;
    }

    @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
    public boolean aStepHasFailed() {
        return ((!getTestOutcomes().isEmpty()) &&
                (getCurrentTestOutcome().getResult() == TestResult.FAILURE || getCurrentTestOutcome().getResult() == TestResult.ERROR));
    }

    public FailureCause getTestFailureCause() {
        return getCurrentTestOutcome().getTestFailureCause();
    }

    public void testFailed(TestOutcome testOutcome, final Throwable cause) {
        getCurrentTestOutcome().determineTestFailureCause(cause);
    }

    public void testIgnored() {
        getCurrentTestOutcome().setAnnotatedResult(IGNORED);
    }

    public void testSkipped() {
        getCurrentTestOutcome().setAnnotatedResult(SKIPPED);
    }

    public void testPending() {
        getCurrentTestOutcome().setAnnotatedResult(PENDING);
    }

    public void notifyScreenChange() {
        if (screenshots().areAllowed(TakeScreenshots.FOR_EACH_ACTION)) {
            take(OPTIONAL_SCREENSHOT);
        }
    }

    /**
     * Take a screenshot now.
     */
    public void takeScreenshot() {
        take(MANDATORY_SCREENSHOT);
    }

    int currentExample = 0;

    /**
     * The current scenario is a data-driven scenario using test data from the specified table.
     */
    public void useExamplesFrom(DataTable table) {
        getCurrentTestOutcome().useExamplesFrom(table);
        currentExample = 0;
    }

    public void exampleStarted(Map<String, String> data) {
        if (getCurrentTestOutcome().isDataDriven() && !getCurrentTestOutcome().dataIsPredefined()) {
            getCurrentTestOutcome().addRow(data);
        }
        currentExample++;
        getEventBus().stepStarted(ExecutedStepDescription.withTitle(exampleTitle(currentExample, data)));
    }

    private String exampleTitle(int exampleNumber, Map<String, String> data) {
        return String.format("[%s] %s", exampleNumber, data);
    }

    public void exampleFinished() {
        currentStepDone(null);
        getCurrentTestOutcome().moveToNextRow();
    }
}
TOP

Related Classes of net.thucydides.core.steps.BaseStepListener

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.